diff --git a/extensions/positron-python/.devcontainer/Dockerfile b/extensions/positron-python/.devcontainer/Dockerfile index 5fbf068de65f..3e7e9e9cf091 100644 --- a/extensions/positron-python/.devcontainer/Dockerfile +++ b/extensions/positron-python/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/devcontainers/typescript-node:16-bookworm +FROM mcr.microsoft.com/devcontainers/typescript-node:18-bookworm RUN apt-get install -y wget bzip2 diff --git a/extensions/positron-python/.github/actions/build-vsix/action.yml b/extensions/positron-python/.github/actions/build-vsix/action.yml index 6c4621c7eb9b..ae7b8fddba69 100644 --- a/extensions/positron-python/.github/actions/build-vsix/action.yml +++ b/extensions/positron-python/.github/actions/build-vsix/action.yml @@ -16,16 +16,16 @@ runs: using: 'composite' steps: - name: Install Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ inputs.node_version }} cache: 'npm' # Jedi LS depends on dataclasses which is not in the stdlib in Python 3.7. - - name: Use Python 3.7 for JediLSP - uses: actions/setup-python@v2 + - name: Use Python 3.8 for JediLSP + uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.8 cache: 'pip' cache-dependency-path: | requirements.txt @@ -84,7 +84,7 @@ runs: shell: bash - name: Upload VSIX - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ inputs.artifact_name }} path: ${{ inputs.vsix_name }} diff --git a/extensions/positron-python/.github/actions/lint/action.yml b/extensions/positron-python/.github/actions/lint/action.yml index 1efa6aab79a5..1d302b055bee 100644 --- a/extensions/positron-python/.github/actions/lint/action.yml +++ b/extensions/positron-python/.github/actions/lint/action.yml @@ -10,7 +10,7 @@ runs: using: 'composite' steps: - name: Install Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ inputs.node_version }} cache: 'npm' @@ -36,7 +36,7 @@ runs: shell: bash - name: Install Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.x' cache: 'pip' diff --git a/extensions/positron-python/.github/dependabot.yml b/extensions/positron-python/.github/dependabot.yml index d54cf6b74a53..de5ebfe9158b 100644 --- a/extensions/positron-python/.github/dependabot.yml +++ b/extensions/positron-python/.github/dependabot.yml @@ -7,6 +7,27 @@ updates: 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: / diff --git a/extensions/positron-python/.github/workflows/build.yml b/extensions/positron-python/.github/workflows/build.yml index 24d91b94da10..56d9c04f0cd1 100644 --- a/extensions/positron-python/.github/workflows/build.yml +++ b/extensions/positron-python/.github/workflows/build.yml @@ -9,7 +9,7 @@ on: - 'release-*' env: - NODE_VERSION: 16.17.1 + NODE_VERSION: 18.17.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. @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build VSIX uses: ./.github/actions/build-vsix @@ -64,7 +64,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Lint uses: ./.github/actions/lint @@ -82,7 +82,7 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install core Python requirements uses: brettcannon/pip-secure-install@v1 @@ -106,7 +106,45 @@ jobs: version: 1.1.308 working-directory: 'pythonFiles' - ### Non-smoke tests + 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.8', '3.x', '3.12-dev'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + path: ${{ env.special-working-directory-relative }} + + - name: Use Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Install base Python requirements + uses: brettcannon/pip-secure-install@v1 + with: + requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' + options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/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 pythonFiles/tests/run_all.py + tests: name: Tests if: github.repository == 'microsoft/vscode-python' @@ -122,10 +160,10 @@ jobs: # and we assume that Ubuntu is enough to cover the UNIX case. os: [ubuntu-latest, windows-latest] python: ['3.x'] - test-suite: [ts-unit, python-unit, venv, single-workspace, multi-workspace, debugger, functional] + test-suite: [ts-unit, venv, single-workspace, multi-workspace, debugger, functional] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: ${{ env.special-working-directory-relative }} @@ -268,10 +306,6 @@ jobs: run: npm run test:unittests if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, '3.') - - name: Run Python unit tests - run: python pythonFiles/tests/run_all.py - if: matrix.test-suite == 'python-unit' - # The virtual environment based tests use the `testSingleWorkspace` set of tests # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`, # which is set in the "Prepare environment for venv tests" step. @@ -333,7 +367,7 @@ jobs: os: [ubuntu-latest, windows-latest] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Smoke tests uses: ./.github/actions/smoke-tests diff --git a/extensions/positron-python/.github/workflows/codeql-analysis.yml b/extensions/positron-python/.github/workflows/codeql-analysis.yml index 278c2cf22e4a..5b037d5a1d0b 100644 --- a/extensions/positron-python/.github/workflows/codeql-analysis.yml +++ b/extensions/positron-python/.github/workflows/codeql-analysis.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/extensions/positron-python/.github/workflows/getLabels.js b/extensions/positron-python/.github/workflows/getLabels.js deleted file mode 100644 index 99060e7205eb..000000000000 --- a/extensions/positron-python/.github/workflows/getLabels.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * To run this file: - * * npm install @octokit/rest - * * node .github/workflows/getLabels.js - * - * This script assumes the maximum number of labels to be 100. - */ - -const { Octokit } = require('@octokit/rest'); -const github = new Octokit(); -github.rest.issues - .listLabelsForRepo({ - owner: 'microsoft', - repo: 'vscode-python', - per_page: 100, - }) - .then((result) => { - const labels = result.data.map((label) => label.name); - console.log( - '\nNumber of labels found:', - labels.length, - ", verify that it's the same as number of labels listed in https://github.com/microsoft/vscode-python/labels\n", - ); - console.log(JSON.stringify(labels), '\n'); - }); diff --git a/extensions/positron-python/.github/workflows/info-needed-closer.yml b/extensions/positron-python/.github/workflows/info-needed-closer.yml index c0b130be803b..442799cd7a16 100644 --- a/extensions/positron-python/.github/workflows/info-needed-closer.yml +++ b/extensions/positron-python/.github/workflows/info-needed-closer.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions diff --git a/extensions/positron-python/.github/workflows/issue-labels.yml b/extensions/positron-python/.github/workflows/issue-labels.yml index d54015d94e46..8b084aef409f 100644 --- a/extensions/positron-python/.github/workflows/issue-labels.yml +++ b/extensions/positron-python/.github/workflows/issue-labels.yml @@ -5,8 +5,6 @@ on: types: [opened, reopened] env: - # To update the list of labels, see `getLabels.js`. - REPO_LABELS: '["area-api","area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-repl","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux","macos","meta","needs community feedback","needs PR","needs proposal","needs spike","no-changelog","on-testplan","partner ask","regression","release-plan","reports-wanted","skip package*.json","skip tests","tensorboard","testplan-item","triage-needed","verification-found","verification-needed","verification-steps-needed","verified","windows"]' TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd","anthonykim1"]' permissions: @@ -15,11 +13,11 @@ permissions: jobs: # From https://github.com/marketplace/actions/github-script#apply-a-label-to-an-issue. add-classify-label: - name: "Add 'triage-needed' and remove unrecognizable labels & assignees" + name: "Add 'triage-needed' and remove assignees" runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable @@ -28,9 +26,8 @@ jobs: - name: Install Actions run: npm install --production --prefix ./actions - - name: "Add 'triage-needed' and remove unrecognizable labels & assignees" + - name: "Add 'triage-needed' and remove assignees" uses: ./actions/python-issue-labels with: triagers: ${{ env.TRIAGERS }} token: ${{secrets.GITHUB_TOKEN}} - repo_labels: ${{ env.REPO_LABELS }} diff --git a/extensions/positron-python/.github/workflows/pr-check.yml b/extensions/positron-python/.github/workflows/pr-check.yml index aa9cae2aa474..9229393ce5cc 100644 --- a/extensions/positron-python/.github/workflows/pr-check.yml +++ b/extensions/positron-python/.github/workflows/pr-check.yml @@ -8,7 +8,7 @@ on: - release* env: - NODE_VERSION: 16.17.1 + NODE_VERSION: 18.17.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 @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build VSIX uses: ./.github/actions/build-vsix @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Lint uses: ./.github/actions/lint @@ -56,7 +56,7 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install base Python requirements uses: brettcannon/pip-secure-install@v1 @@ -80,7 +80,45 @@ jobs: version: 1.1.308 working-directory: 'pythonFiles' - ### Non-smoke tests + 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.8', '3.x', '3.12-dev'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + path: ${{ env.special-working-directory-relative }} + + - name: Use Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Install base Python requirements + uses: brettcannon/pip-secure-install@v1 + with: + requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' + options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/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 pythonFiles/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. @@ -96,11 +134,11 @@ jobs: os: [ubuntu-latest, windows-latest] # Run the tests on the oldest and most recent versions of Python. python: ['3.x'] - test-suite: [ts-unit, python-unit, venv, single-workspace, debugger, functional] + test-suite: [ts-unit, venv, single-workspace, debugger, functional] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: ${{ env.special-working-directory-relative }} @@ -139,14 +177,12 @@ jobs: with: requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/python" --no-cache-dir --implementation py' - if: startsWith(matrix.python, 3.) - name: Install Jedi requirements uses: brettcannon/pip-secure-install@v1 with: requirements-file: '"${{ env.special-working-directory-relative }}/pythonFiles/jedilsp_requirements/requirements.txt"' options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/jedilsp" --no-cache-dir --implementation py' - if: startsWith(matrix.python, 3.) - name: Install test requirements run: python -m pip install --upgrade -r build/test-requirements.txt @@ -243,12 +279,6 @@ jobs: run: npm run test:unittests if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, 3.) - # Run the Python tests in our codebase. - - name: Run Python unit tests - run: | - python pythonFiles/tests/run_all.py - if: matrix.test-suite == 'python-unit' - # The virtual environment based tests use the `testSingleWorkspace` set of tests # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`, # which is set in the "Prepare environment for venv tests" step. @@ -302,7 +332,7 @@ jobs: steps: # Need the source to have the tests available. - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Smoke tests uses: ./.github/actions/smoke-tests @@ -323,7 +353,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Node uses: actions/setup-node@v3 diff --git a/extensions/positron-python/.github/workflows/test-plan-item-validator.yml b/extensions/positron-python/.github/workflows/test-plan-item-validator.yml index 9d0805a9db9b..17f1740345f2 100644 --- a/extensions/positron-python/.github/workflows/test-plan-item-validator.yml +++ b/extensions/positron-python/.github/workflows/test-plan-item-validator.yml @@ -12,7 +12,7 @@ jobs: 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@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions diff --git a/extensions/positron-python/.github/workflows/triage-info-needed.yml b/extensions/positron-python/.github/workflows/triage-info-needed.yml index c717d7ec94b3..24ad2ed2c480 100644 --- a/extensions/positron-python/.github/workflows/triage-info-needed.yml +++ b/extensions/positron-python/.github/workflows/triage-info-needed.yml @@ -13,7 +13,7 @@ jobs: if: contains(github.event.issue.labels.*.name, 'triage-needed') && !contains(github.event.issue.labels.*.name, 'info-needed') steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable diff --git a/extensions/positron-python/.nvmrc b/extensions/positron-python/.nvmrc index e0325e5adb60..860cc5000ae6 100644 --- a/extensions/positron-python/.nvmrc +++ b/extensions/positron-python/.nvmrc @@ -1 +1 @@ -v16.17.1 +v18.17.1 diff --git a/extensions/positron-python/.vscode/settings.json b/extensions/positron-python/.vscode/settings.json index 487b463dd252..86b34bfd81d9 100644 --- a/extensions/positron-python/.vscode/settings.json +++ b/extensions/positron-python/.vscode/settings.json @@ -72,5 +72,12 @@ "pythonFiles/tests" ], "typescript.preferences.importModuleSpecifier": "relative", - "debug.javascript.usePreview": false + "debug.javascript.usePreview": false, + // Branch name suggestion. + "git.branchProtectionPrompt": "alwaysCommitToNewBranch", + "git.branchRandomName.enable": true, + "git.branchProtection": ["main", "release/*"], + "git.pullBeforeCheckout": true, + // Open merge editor for resolving conflicts. + "git.mergeEditor": true } diff --git a/extensions/positron-python/README.md b/extensions/positron-python/README.md index 8a5df6720717..0a8766f086af 100644 --- a/extensions/positron-python/README.md +++ b/extensions/positron-python/README.md @@ -37,7 +37,7 @@ Extensions installed through the marketplace are subject to the [Marketplace Ter ## Jupyter Notebook quick start -The Python extension offers support for Jupyter notebooks via the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) to provide you a great Python notebook experience in VS Code. +The Python extension offers support for Jupyter notebooks via the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) to provide you a great Python notebook experience in VS Code. - Install the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter). @@ -60,7 +60,6 @@ Open the Command Palette (Command+Shift+P on macOS and Ctrl+Shift+P on Windows/L | `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. | @@ -82,7 +81,7 @@ Learn more about the rich features of the Python extension: - [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 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). diff --git a/extensions/positron-python/build/azure-pipeline.pre-release.yml b/extensions/positron-python/build/azure-pipeline.pre-release.yml index d4fe0ac376fb..bb52f983d02e 100644 --- a/extensions/positron-python/build/azure-pipeline.pre-release.yml +++ b/extensions/positron-python/build/azure-pipeline.pre-release.yml @@ -33,12 +33,12 @@ extends: buildSteps: - task: NodeTool@0 inputs: - versionSpec: '16.17.1' + versionSpec: '18.17.1' displayName: Select Node version - task: UsePythonVersion@0 inputs: - versionSpec: '3.7' + versionSpec: '3.8' addToPath: true architecture: 'x64' displayName: Select Python version diff --git a/extensions/positron-python/build/azure-pipeline.stable.yml b/extensions/positron-python/build/azure-pipeline.stable.yml index 05f83aa81824..02f8bd38cf81 100644 --- a/extensions/positron-python/build/azure-pipeline.stable.yml +++ b/extensions/positron-python/build/azure-pipeline.stable.yml @@ -28,12 +28,12 @@ extends: buildSteps: - task: NodeTool@0 inputs: - versionSpec: '16.17.1' + versionSpec: '18.17.1' displayName: Select Node version - task: UsePythonVersion@0 inputs: - versionSpec: '3.7' + versionSpec: '3.8' addToPath: true architecture: 'x64' displayName: Select Python version diff --git a/extensions/positron-python/build/azure-pipelines/pipeline.yml b/extensions/positron-python/build/azure-pipelines/pipeline.yml index 85b41c16efc0..adb2fa5d1c30 100644 --- a/extensions/positron-python/build/azure-pipelines/pipeline.yml +++ b/extensions/positron-python/build/azure-pipelines/pipeline.yml @@ -37,13 +37,13 @@ extends: testPlatforms: - name: Linux nodeVersions: - - 16.17.1 + - 18.17.1 - name: MacOS nodeVersions: - - 16.17.1 + - 18.17.1 - name: Windows nodeVersions: - - 16.17.1 + - 18.17.1 testSteps: - template: /build/azure-pipelines/templates/test-steps.yml@self parameters: diff --git a/extensions/positron-python/build/ci/conda_env_1.yml b/extensions/positron-python/build/ci/conda_env_1.yml index df5c917dcf4f..e9d08d0820a4 100644 --- a/extensions/positron-python/build/ci/conda_env_1.yml +++ b/extensions/positron-python/build/ci/conda_env_1.yml @@ -1,4 +1,4 @@ name: conda_env_1 dependencies: - - python=3.7 + - python=3.8 - pip diff --git a/extensions/positron-python/gulpfile.js b/extensions/positron-python/gulpfile.js index 41d8355882c4..d5579b381d3a 100644 --- a/extensions/positron-python/gulpfile.js +++ b/extensions/positron-python/gulpfile.js @@ -43,18 +43,19 @@ gulp.task('compileCore', (done) => { .on('finish', () => (failed ? done(new Error('TypeScript compilation errors')) : done())); }); -const apiTsProject = ts.createProject('./pythonExtensionApi/tsconfig.json', { typescript }); - gulp.task('compileApi', (done) => { - let failed = false; - apiTsProject - .src() - .pipe(apiTsProject()) - .on('error', () => { - failed = true; + spawnAsync('npm', ['run', 'compileApi'], undefined, true) + .then((stdout) => { + if (stdout.includes('error')) { + done(new Error(stdout)); + } else { + done(); + } }) - .js.pipe(gulp.dest('./pythonExtensionApi/out')) - .on('finish', () => (failed ? done(new Error('TypeScript compilation errors')) : done())); + .catch((ex) => { + console.log(ex); + done(new Error('TypeScript compilation errors', ex)); + }); }); gulp.task('compile', gulp.series('compileCore', 'compileApi')); diff --git a/extensions/positron-python/package.json b/extensions/positron-python/package.json index 8d5ea3882b40..37a6ff91ec10 100644 --- a/extensions/positron-python/package.json +++ b/extensions/positron-python/package.json @@ -20,7 +20,6 @@ "enabledApiProposals": [ "contribEditorContentMenu", "quickPickSortByLabel", - "envShellEvent", "testObserver", "quickPickItemTooltip", "saveEditor" @@ -44,7 +43,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.82.0-20230830" + "vscode": "^1.82.0" }, "enableTelemetry": false, "keywords": [ @@ -68,9 +67,6 @@ "onLanguage:python", "onDebugDynamicConfigurations:python", "onDebugResolve:python", - "onWalkthrough:pythonWelcome", - "onWalkthrough:pythonWelcome2", - "onWalkthrough:pythonDataScienceWelcome", "workspaceContains:mspythonconfig.json", "workspaceContains:pyproject.toml", "workspaceContains:Pipfile", @@ -369,11 +365,6 @@ "command": "python.createEnvironment-button", "title": "%python.command.python.createEnvironment.title%" }, - { - "category": "Python", - "command": "python.enableLinting", - "title": "%python.command.python.enableLinting.title%" - }, { "category": "Python", "command": "python.enableSourceMapSupport", @@ -435,26 +426,11 @@ "icon": "$(run-errors)", "title": "%python.command.testing.rerunFailedTests.title%" }, - { - "category": "Python", - "command": "python.runLinting", - "title": "%python.command.python.runLinting.title%" - }, { "category": "Python", "command": "python.setInterpreter", "title": "%python.command.python.setInterpreter.title%" }, - { - "category": "Python", - "command": "python.setLinter", - "title": "%python.command.python.setLinter.title%" - }, - { - "category": "Python Refactor", - "command": "python.sortImports", - "title": "%python.command.python.sortImports.title%" - }, { "category": "Python", "command": "python.startREPL", @@ -509,6 +485,19 @@ "experimental" ] }, + "python.createEnvironment.trigger": { + "default": "off", + "markdownDescription": "%python.createEnvironment.trigger.description%", + "scope": "machine-overridable", + "type": "string", + "enum": [ + "off", + "prompt" + ], + "tags": [ + "experimental" + ] + }, "python.condaPath": { "default": "", "description": "%python.condaPath.description%", @@ -1147,7 +1136,7 @@ "scope": "machine", "type": "string" }, - "python.missingPackage.severity":{ + "python.missingPackage.severity": { "default": "Hint", "description": "%python.missingPackage.severity.description%", "enum": [ @@ -1819,12 +1808,6 @@ "title": "%python.command.python.createTerminal.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, - { - "category": "Python", - "command": "python.enableLinting", - "title": "%python.command.python.enableLinting.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, { "category": "Python", "command": "python.enableSourceMapSupport", @@ -1896,30 +1879,12 @@ "title": "%python.command.testing.rerunFailedTests.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, - { - "category": "Python", - "command": "python.runLinting", - "title": "%python.command.python.runLinting.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, { "category": "Python", "command": "python.setInterpreter", "title": "%python.command.python.setInterpreter.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, - { - "category": "Python", - "command": "python.setLinter", - "title": "%python.command.python.setLinter.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, - { - "category": "Python Refactor", - "command": "python.sortImports", - "title": "%python.command.python.sortImports.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, { "category": "Python", "command": "python.startREPL", @@ -1958,12 +1923,6 @@ "group": "Python", "when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported && isWorkspaceTrusted" }, - { - "command": "python.sortImports", - "group": "Refactor", - "title": "%python.command.python.sortImports.title%", - "when": "editorLangId == python && !notebookEditorFocused && !virtualWorkspace && shellExecutionSupported" - }, { "submenu": "python.runFileInteractive", "group": "Jupyter2", @@ -2102,6 +2061,7 @@ "package": "gulp clean && gulp prePublishBundle && vsce package -o ms-python-insiders.vsix", "prePublish": "gulp clean && gulp prePublishNonBundle", "compile": "tsc -watch -p ./", + "compileApi": "node ./node_modules/typescript/lib/tsc.js -b ./pythonExtensionApi/tsconfig.json", "compiled": "deemon npm run compile", "kill-compiled": "deemon --kill npm run compile", "checkDependencies": "gulp checkDependencies", @@ -2137,7 +2097,7 @@ }, "dependencies": { "@iarna/toml": "^2.2.5", - "@vscode/extension-telemetry": "^0.7.7", + "@vscode/extension-telemetry": "^0.8.4", "@vscode/jupyter-lsp-middleware": "^0.2.50", "arch": "^2.1.0", "diff-match-patch": "^1.0.0", @@ -2189,7 +2149,7 @@ "@types/md5": "^2.1.32", "@types/mocha": "^9.1.0", "@types/nock": "^10.0.3", - "@types/node": "^16.17.0", + "@types/node": "^18.17.1", "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", "@types/sinon": "^10.0.11", @@ -2202,7 +2162,7 @@ "@types/xml2js": "0.4.9", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", - "@vscode/test-electron": "^2.1.3", + "@vscode/test-electron": "^2.3.4", "@vscode/vsce": "^2.18.0", "bent": "^7.3.12", "chai": "^4.1.2", diff --git a/extensions/positron-python/package.nls.json b/extensions/positron-python/package.nls.json index bfb0a2061339..0160d70e4521 100644 --- a/extensions/positron-python/package.nls.json +++ b/extensions/positron-python/package.nls.json @@ -21,15 +21,13 @@ "python.command.python.execSelectionInTerminal.title": "Run Selection/Line in Python Terminal", "python.command.python.execSelectionInDjangoShell.title": "Run Selection/Line in Django Shell", "python.command.python.reportIssue.title": "Report Issue...", - "python.command.python.setLinter.title": "Select Linter", - "python.command.python.enableLinting.title": "Enable/Disable Linting", - "python.command.python.runLinting.title": "Run Linting", "python.command.python.enableSourceMapSupport.title": "Enable Source Map Support For Extension Debugging", "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.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", @@ -60,14 +58,14 @@ "python.formatting.blackPath.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", "python.formatting.blackPath.deprecationMessage": "This setting will soon be deprecated. Please use the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.provider.description": "Provider for formatting. Possible options include 'autopep8', 'black', and 'yapf'.", - "python.formatting.provider.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8) or the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.provider.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension or the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", + "python.formatting.provider.markdownDeprecationMessage": "This setting will soon be deprecated. Please use a dedicated formatter extension.
Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.provider.deprecationMessage": "This setting will soon be deprecated. Please use a dedicated formatter extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.yapfArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.formatting.yapfArgs.markdownDeprecationMessage": "Yapf support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.yapfArgs.deprecationMessage": "Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", + "python.formatting.yapfArgs.markdownDeprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.yapfArgs.deprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.yapfPath.description": "Path to yapf, you can use a custom version of yapf by modifying this setting to include the full path.", "python.formatting.yapfPath.markdownDeprecationMessage": "Yapf support will soon be deprecated.
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.yapfPath.deprecationMessage": "Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", + "python.formatting.yapfPath.deprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.globalModuleInstallation.description": "Whether to install Python modules globally when not using an environment.", "python.languageServerDebug.description": "Whether debug should be enabled for Positron's Python language server.", "python.languageServerLogLevel.description": "Controls the [logging level](https://docs.python.org/3/library/logging.html#levels) of Positron's Python language server. Requires a restart to take effect.", diff --git a/extensions/positron-python/pythonExtensionApi/package-lock.json b/extensions/positron-python/pythonExtensionApi/package-lock.json index 9b4847457b20..ef6914e0e786 100644 --- a/extensions/positron-python/pythonExtensionApi/package-lock.json +++ b/extensions/positron-python/pythonExtensionApi/package-lock.json @@ -1,19 +1,20 @@ { "name": "@vscode/python-extension", - "version": "1.0.4", + "version": "1.0.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@vscode/python-extension", - "version": "1.0.4", + "version": "1.0.5", "license": "MIT", "devDependencies": { "@types/vscode": "^1.78.0", + "source-map": "^0.8.0-beta.0", "typescript": "5.0.4" }, "engines": { - "node": ">=16.17.1", + "node": ">=18.17.1", "vscode": "^1.78.0" } }, @@ -23,6 +24,42 @@ "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==", "dev": true }, + "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.0.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", @@ -35,6 +72,23 @@ "engines": { "node": ">=12.20" } + }, + "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": { @@ -44,11 +98,58 @@ "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==", "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.0.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "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/extensions/positron-python/pythonExtensionApi/package.json b/extensions/positron-python/pythonExtensionApi/package.json index 86ac58f42f20..9e58f1a2400c 100644 --- a/extensions/positron-python/pythonExtensionApi/package.json +++ b/extensions/positron-python/pythonExtensionApi/package.json @@ -1,7 +1,7 @@ { "name": "@vscode/python-extension", "description": "An API facade for the Python extension in VS Code", - "version": "1.0.4", + "version": "1.0.5", "author": { "name": "Microsoft Corporation" }, @@ -13,7 +13,7 @@ "main": "./out/main.js", "types": "./out/main.d.ts", "engines": { - "node": ">=16.17.1", + "node": ">=18.17.1", "vscode": "^1.78.0" }, "license": "MIT", @@ -27,7 +27,8 @@ }, "devDependencies": { "typescript": "5.0.4", - "@types/vscode": "^1.78.0" + "@types/vscode": "^1.78.0", + "source-map": "^0.8.0-beta.0" }, "scripts": { "prepublishOnly": "echo \"⛔ Can only publish from a secure pipeline ⛔\" && node ../build/fail", diff --git a/extensions/positron-python/pythonFiles/create_venv.py b/extensions/positron-python/pythonFiles/create_venv.py index cac084fd2222..68f21a38b980 100644 --- a/extensions/positron-python/pythonFiles/create_venv.py +++ b/extensions/positron-python/pythonFiles/create_venv.py @@ -3,6 +3,7 @@ import argparse import importlib.util as import_util +import json import os import pathlib import subprocess @@ -56,6 +57,12 @@ def parse_args(argv: Sequence[str]) -> argparse.Namespace: metavar="NAME", action="store", ) + parser.add_argument( + "--stdin", + action="store_true", + default=False, + help="Read arguments from stdin.", + ) return parser.parse_args(argv) @@ -152,6 +159,16 @@ def install_pip(name: str): ) +def get_requirements_from_args(args: argparse.Namespace) -> List[str]: + requirements = [] + if args.stdin: + data = json.loads(sys.stdin.read()) + requirements = data.get("requirements", []) + if args.requirements: + requirements.extend(args.requirements) + return requirements + + def main(argv: Optional[Sequence[str]] = None) -> None: if argv is None: argv = [] @@ -223,9 +240,10 @@ def main(argv: Optional[Sequence[str]] = None) -> None: print(f"VENV_INSTALLING_PYPROJECT: {args.toml}") install_toml(venv_path, args.extras) - if args.requirements: - print(f"VENV_INSTALLING_REQUIREMENTS: {args.requirements}") - install_requirements(venv_path, args.requirements) + requirements = get_requirements_from_args(args) + if requirements: + print(f"VENV_INSTALLING_REQUIREMENTS: {requirements}") + install_requirements(venv_path, requirements) if __name__ == "__main__": diff --git a/extensions/positron-python/pythonFiles/install_debugpy.py b/extensions/positron-python/pythonFiles/install_debugpy.py index cabb620ea1f2..9377d00237d7 100644 --- a/extensions/positron-python/pythonFiles/install_debugpy.py +++ b/extensions/positron-python/pythonFiles/install_debugpy.py @@ -13,7 +13,7 @@ DEBUGGER_DEST = os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "python") DEBUGGER_PACKAGE = "debugpy" DEBUGGER_PYTHON_ABI_VERSIONS = ("cp310",) -DEBUGGER_VERSION = "1.6.7" # can also be "latest" +DEBUGGER_VERSION = "1.8.0" # can also be "latest" def _contains(s, parts=()): diff --git a/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.in b/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.in index 6e99ec89fa10..ec99326f5cba 100644 --- a/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.in +++ b/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.in @@ -1,6 +1,6 @@ # This file is used to generate requirements.txt. # To update requirements.txt, run the following commands. -# Use Python 3.7 when creating the environment or using pip-tools +# Use Python 3.8 when creating the environment or using pip-tools # 1) pip install pip-tools # 2) pip-compile --generate-hashes --upgrade pythonFiles\jedilsp_requirements\requirements.in diff --git a/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt b/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt index e7c726540f45..f2f0cbf3291e 100644 --- a/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt +++ b/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt @@ -13,33 +13,32 @@ attrs==23.1.0 \ cattrs==23.1.2 \ --hash=sha256:b2bb14311ac17bed0d58785e5a60f022e5431aca3932e3fc5cc8ed8639de50a4 \ --hash=sha256:db1c821b8c537382b2c7c66678c3790091ca0275ac486c76f3c8f3920e83c657 - # via lsprotocol + # via + # jedi-language-server + # lsprotocol docstring-to-markdown==0.12 \ --hash=sha256:40004224b412bd6f64c0f3b85bb357a41341afd66c4b4896709efa56827fb2bb \ --hash=sha256:7df6311a887dccf9e770f51242ec002b19f0591994c4783be49d24cdc1df3737 # via jedi-language-server -exceptiongroup==1.1.2 \ - --hash=sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5 \ - --hash=sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f +exceptiongroup==1.1.3 \ + --hash=sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9 \ + --hash=sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3 # via cattrs -importlib-metadata==3.10.1 \ - --hash=sha256:2ec0faae539743ae6aaa84b49a169670a465f7f5d64e6add98388cc29fd1f2f6 \ - --hash=sha256:c9356b657de65c53744046fa8f7358afe0714a1af7d570c00c3835c2d724a7c1 - # via - # attrs - # jedi-language-server - # typeguard -jedi==0.18.2 \ - --hash=sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e \ - --hash=sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612 +importlib-metadata==6.8.0 \ + --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \ + --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743 + # via typeguard +jedi==0.19.1 \ + --hash=sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd \ + --hash=sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0 # via jedi-language-server -jedi-language-server==0.40.0 \ - --hash=sha256:53e590400b5cd2f6e363e77a4d824b1883798994b731cb0b4370d103748d30e2 \ - --hash=sha256:bacbae2930b6a8a0f1f284c211672fceec94b4808b0415d1c3352fa4b1ac5ad6 +jedi-language-server==0.41.1 \ + --hash=sha256:3f15ca5cc28e728564f7d63583e171b418025582447ce023512e3f2b2d71ebae \ + --hash=sha256:ca9b3e7f48b70f0988d85ffde4f01dd1ab94c8e0f69e8c6424e6657117b44f91 # via -r pythonFiles\jedilsp_requirements\requirements.in -lsprotocol==2023.0.0a2 \ - --hash=sha256:80aae7e39171b49025876a524937c10be2eb986f4be700ca22ee7d186b8488aa \ - --hash=sha256:c4f2f77712b50d065b17f9b50d2b88c480dc2ce4bbaa56eea8269dbf54bc9701 +lsprotocol==2023.0.0b1 \ + --hash=sha256:ade2cd0fa0ede7965698cb59cd05d3adbd19178fd73e83f72ef57a032fbb9d62 \ + --hash=sha256:f7a2d4655cbd5639f373ddd1789807450c543341fa0a32b064ad30dbb9f510d4 # via # jedi-language-server # pygls @@ -55,44 +54,6 @@ parso==0.8.3 \ --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \ --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75 # via jedi -pydantic==1.10.12 \ - --hash=sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303 \ - --hash=sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe \ - --hash=sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47 \ - --hash=sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494 \ - --hash=sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33 \ - --hash=sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86 \ - --hash=sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d \ - --hash=sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c \ - --hash=sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a \ - --hash=sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565 \ - --hash=sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb \ - --hash=sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62 \ - --hash=sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62 \ - --hash=sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0 \ - --hash=sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523 \ - --hash=sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d \ - --hash=sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405 \ - --hash=sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f \ - --hash=sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b \ - --hash=sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718 \ - --hash=sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed \ - --hash=sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb \ - --hash=sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5 \ - --hash=sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc \ - --hash=sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942 \ - --hash=sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe \ - --hash=sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246 \ - --hash=sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350 \ - --hash=sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303 \ - --hash=sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09 \ - --hash=sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33 \ - --hash=sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8 \ - --hash=sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a \ - --hash=sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1 \ - --hash=sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6 \ - --hash=sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d - # via jedi-language-server pygls==1.0.2 \ --hash=sha256:6d278d29fa6559b0f7a448263c85cb64ec6e9369548b02f1a7944060848b21f9 \ --hash=sha256:888ed63d1f650b4fc64d603d73d37545386ec533c0caac921aed80f80ea946a4 @@ -103,14 +64,14 @@ typeguard==3.0.2 \ --hash=sha256:bbe993854385284ab42fd5bd3bee6f6556577ce8b50696d6cb956d704f286c8e \ --hash=sha256:fee5297fdb28f8e9efcb8142b5ee219e02375509cd77ea9d270b5af826358d5a # via pygls -typing-extensions==4.7.1 \ - --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ - --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 +typing-extensions==4.8.0 \ + --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ + --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef # via # cattrs - # pydantic + # jedi-language-server # typeguard -zipp==3.15.0 \ - --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ - --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 +zipp==3.17.0 \ + --hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \ + --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0 # via importlib-metadata diff --git a/extensions/positron-python/pythonFiles/testing_tools/socket_manager.py b/extensions/positron-python/pythonFiles/testing_tools/socket_manager.py index 372a50b5e012..b2afbf0e5a17 100644 --- a/extensions/positron-python/pythonFiles/testing_tools/socket_manager.py +++ b/extensions/positron-python/pythonFiles/testing_tools/socket_manager.py @@ -23,6 +23,12 @@ def __init__(self, addr): self.socket = None def __enter__(self): + return self.connect() + + def __exit__(self, *_): + self.close() + + def connect(self): self.socket = socket.socket( socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP ) @@ -35,7 +41,7 @@ def __enter__(self): return self - def __exit__(self, *_): + def close(self): if self.socket: try: self.socket.shutdown(socket.SHUT_RDWR) diff --git a/extensions/positron-python/pythonFiles/tests/positron/test_environment.py b/extensions/positron-python/pythonFiles/tests/positron/test_environment.py index f4c1c52612d3..145eae0b0b07 100644 --- a/extensions/positron-python/pythonFiles/tests/positron/test_environment.py +++ b/extensions/positron-python/pythonFiles/tests/positron/test_environment.py @@ -3,8 +3,8 @@ # from __future__ import annotations -import asyncio +import asyncio import inspect import math import pprint @@ -28,8 +28,8 @@ EnvironmentVariable, EnvironmentVariableValueKind, ) -from positron.positron_ipkernel import PositronIPyKernel from positron.inspectors import get_inspector +from positron.positron_ipkernel import PositronIPyKernel from .conftest import DummyComm @@ -837,6 +837,7 @@ def test_numpy_assign_and_update(shell: TerminalInteractiveShell, env_comm: Dumm } +@pytest.mark.skip() def test_torch_assign_and_update(shell: TerminalInteractiveShell, env_comm: DummyComm) -> None: """ Test environment change detection for pytorch tensors. diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py index a39b7c26de9f..c4dbadc32d6e 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py @@ -15,6 +15,8 @@ def test_adding(actual, expected): # Testing pytest with parametrized tests. All three pass. # The tests ids are parametrize_tests.py::test_under_ten[1] and so on. -@pytest.mark.parametrize("num", range(1, 3)) # test_marker--test_under_ten -def test_under_ten(num): - assert num < 10 +@pytest.mark.parametrize( # test_marker--test_string + "string", ["hello", "complicated split [] ()"] +) +def test_string(string): + assert string == "hello" diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 2b2c07ab8ea7..31686d2b3b5d 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -594,46 +594,46 @@ ], }, { - "name": "test_under_ten", + "name": "test_string", "path": os.fspath(parameterize_tests_path), "type_": "function", "children": [ { - "name": "[1]", + "name": "[hello]", "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( - "test_under_ten[1]", + "test_string[hello]", parameterize_tests_path, ), "type_": "test", "id_": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[1]", + "parametrize_tests.py::test_string[hello]", parameterize_tests_path, ), "runID": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[1]", + "parametrize_tests.py::test_string[hello]", parameterize_tests_path, ), }, { - "name": "[2]", + "name": "[complicated split [] ()]", "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( - "test_under_ten[2]", + "test_string[1]", parameterize_tests_path, ), "type_": "test", "id_": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[2]", + "parametrize_tests.py::test_string[complicated split [] ()]", parameterize_tests_path, ), "runID": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[2]", + "parametrize_tests.py::test_string[complicated split [] ()]", parameterize_tests_path, ), }, ], - "id_": "parametrize_tests.py::test_under_ten", + "id_": "parametrize_tests.py::test_string", }, ], }, diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/helpers.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/helpers.py index 7195cfe43ea5..b534e950945a 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/helpers.py +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/helpers.py @@ -12,6 +12,10 @@ import uuid from typing import Any, Dict, List, Optional, Tuple +script_dir = pathlib.Path(__file__).parent.parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) + TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" from typing_extensions import TypedDict diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_discovery.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/test_discovery.py index 8d785be27c8b..674d92ac0545 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/test_discovery.py @@ -29,15 +29,26 @@ def test_import_error(tmp_path): temp_dir.mkdir() p = temp_dir / "error_pytest_import.py" shutil.copyfile(file_path, p) - actual_list: Optional[List[Dict[str, Any]]] = runner( - ["--collect-only", os.fspath(p)] - ) - assert actual_list - for actual in actual_list: - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 + actual: Optional[List[Dict[str, Any]]] = runner(["--collect-only", os.fspath(p)]) + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + assert False def test_syntax_error(tmp_path): @@ -60,13 +71,25 @@ def test_syntax_error(tmp_path): p = temp_dir / "error_syntax_discovery.py" shutil.copyfile(file_path, p) actual = runner(["--collect-only", os.fspath(p)]) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + assert False def test_parameterized_error_collect(): @@ -76,12 +99,25 @@ def test_parameterized_error_collect(): """ file_path_str = "error_parametrize_discovery.py" actual = runner(["--collect-only", file_path_str]) - if actual: - actual = actual[0] - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + assert False @pytest.mark.parametrize( @@ -146,13 +182,16 @@ def test_pytest_collect(file, expected_const): os.fspath(TEST_DATA_PATH / file), ] ) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert actual["tests"] == expected_const + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + actual_item = actual_list.pop(0) + assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + assert actual_item.get("tests") == expected_const def test_pytest_root_dir(): @@ -168,14 +207,16 @@ def test_pytest_root_dir(): ], TEST_DATA_PATH / "root", ) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + actual_item = actual_list.pop(0) + assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH / "root") assert ( - actual["tests"] + actual_item.get("tests") == expected_discovery_test_output.root_with_config_expected_output ) @@ -193,13 +234,15 @@ def test_pytest_config_file(): ], TEST_DATA_PATH / "root", ) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + actual_item = actual_list.pop(0) + assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH / "root") assert ( - actual["tests"] + actual_item.get("tests") == expected_discovery_test_output.root_with_config_expected_output ) diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py index 07354b01709b..37a392f66d4b 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import os import shutil +from typing import Any, Dict, List import pytest @@ -23,14 +24,19 @@ def test_config_file(): expected_execution_test_output.config_file_pytest_expected_execution_output ) assert actual - assert len(actual) == len(expected_const) + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + assert len(actual_list) == len(expected_const) actual_result_dict = dict() - for a in actual: - assert all(item in a for item in ("status", "cwd", "result")) - assert a["status"] == "success" - assert a["cwd"] == os.fspath(new_cwd) - actual_result_dict.update(a["result"]) - assert actual_result_dict == expected_const + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "result") + ) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(new_cwd) + actual_result_dict.update(actual_item["result"]) + assert actual_result_dict == expected_const def test_rootdir_specified(): @@ -43,14 +49,19 @@ def test_rootdir_specified(): expected_execution_test_output.config_file_pytest_expected_execution_output ) assert actual - assert len(actual) == len(expected_const) + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + assert len(actual_list) == len(expected_const) actual_result_dict = dict() - for a in actual: - assert all(item in a for item in ("status", "cwd", "result")) - assert a["status"] == "success" - assert a["cwd"] == os.fspath(new_cwd) - actual_result_dict.update(a["result"]) - assert actual_result_dict == expected_const + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "result") + ) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(new_cwd) + actual_result_dict.update(actual_item["result"]) + assert actual_result_dict == expected_const def test_syntax_error_execution(tmp_path): @@ -73,13 +84,23 @@ def test_syntax_error_execution(tmp_path): p = temp_dir / "error_syntax_discovery.py" shutil.copyfile(file_path, p) actual = runner(["error_syntax_discover.py::test_function"]) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 1 + assert actual + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 1 + else: + assert False def test_bad_id_error_execution(): @@ -88,13 +109,23 @@ def test_bad_id_error_execution(): The json should still be returned but the errors list should be present. """ actual = runner(["not/a/real::test_id"]) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 1 + assert actual + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 1 + else: + assert False @pytest.mark.parametrize( @@ -195,7 +226,8 @@ def test_pytest_execution(test_ids, expected_const): 3. uf_single_method_execution_expected_output: test run on a single method in a file. 4. uf_non_adjacent_tests_execution_expected_output: test run on unittests in two files with single selection in test explorer. 5. unit_pytest_same_file_execution_expected_output: test run on a file with both unittest and pytest tests. - 6. dual_level_nested_folder_execution_expected_output: test run on a file with one test file at the top level and one test file in a nested folder. + 6. dual_level_nested_folder_execution_expected_output: test run on a file with one test file + at the top level and one test file in a nested folder. 7. double_nested_folder_expected_execution_output: test run on a double nested folder. 8. parametrize_tests_expected_execution_output: test run on a parametrize test with 3 inputs. 9. single_parametrize_tests_expected_execution_output: test run on single parametrize test. @@ -205,18 +237,22 @@ def test_pytest_execution(test_ids, expected_const): Keyword arguments: test_ids -- an array of test_ids to run. expected_const -- a dictionary of the expected output from running pytest discovery on the files. - """ # noqa: E501 + """ args = test_ids actual = runner(args) assert actual - print(actual) - assert len(actual) == len(expected_const) + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + assert len(actual_list) == len(expected_const) actual_result_dict = dict() - for a in actual: - assert all(item in a for item in ("status", "cwd", "result")) - assert a["status"] == "success" - assert a["cwd"] == os.fspath(TEST_DATA_PATH) - actual_result_dict.update(a["result"]) + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "result") + ) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + actual_result_dict.update(actual_item["result"]) for key in actual_result_dict: if ( actual_result_dict[key]["outcome"] == "failure" diff --git a/extensions/positron-python/pythonFiles/tests/test_create_venv.py b/extensions/positron-python/pythonFiles/tests/test_create_venv.py index ae3f18be6f3c..772fc02708f8 100644 --- a/extensions/positron-python/pythonFiles/tests/test_create_venv.py +++ b/extensions/positron-python/pythonFiles/tests/test_create_venv.py @@ -1,7 +1,11 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import argparse +import contextlib import importlib +import io +import json import os import sys @@ -224,3 +228,50 @@ def run_process(args, error_message): create_venv.run_process = run_process create_venv.main([]) + + +@contextlib.contextmanager +def redirect_io(stream: str, new_stream): + """Redirect stdio streams to a custom stream.""" + old_stream = getattr(sys, stream) + setattr(sys, stream, new_stream) + yield + setattr(sys, stream, old_stream) + + +class CustomIO(io.TextIOWrapper): + """Custom stream object to replace stdio.""" + + name: str = "customio" + + def __init__(self, name: str, encoding="utf-8", newline=None): + self._buffer = io.BytesIO() + self._buffer.name = name + super().__init__(self._buffer, encoding=encoding, newline=newline) + + def close(self): + """Provide this close method which is used by some tools.""" + # This is intentionally empty. + + def get_value(self) -> str: + """Returns value from the buffer as string.""" + self.seek(0) + return self.read() + + +def test_requirements_from_stdin(): + importlib.reload(create_venv) + + cli_requirements = [f"cli-requirement{i}.txt" for i in range(3)] + args = argparse.Namespace() + args.__dict__.update({"stdin": True, "requirements": cli_requirements}) + + stdin_requirements = [f"stdin-requirement{i}.txt" for i in range(20)] + text = json.dumps({"requirements": stdin_requirements}) + str_input = CustomIO("", encoding="utf-8", newline="\n") + with redirect_io("stdin", str_input): + str_input.write(text) + str_input.seek(0) + actual = create_venv.get_requirements_from_args(args) + + assert actual == stdin_requirements + cli_requirements diff --git a/extensions/positron-python/pythonFiles/tests/test_data/missing-deps.data b/extensions/positron-python/pythonFiles/tests/test_data/missing-deps.data index c42d23c7dd67..c8c911f218a8 100644 --- a/extensions/positron-python/pythonFiles/tests/test_data/missing-deps.data +++ b/extensions/positron-python/pythonFiles/tests/test_data/missing-deps.data @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.7 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # pip-compile --generate-hashes --resolver=backtracking requirements-test.in diff --git a/extensions/positron-python/pythonFiles/tests/test_data/no-missing-deps.data b/extensions/positron-python/pythonFiles/tests/test_data/no-missing-deps.data index 5c2f1178bbdf..d5d04476dec0 100644 --- a/extensions/positron-python/pythonFiles/tests/test_data/no-missing-deps.data +++ b/extensions/positron-python/pythonFiles/tests/test_data/no-missing-deps.data @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.7 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # pip-compile --generate-hashes --resolver=backtracking requirements-test.in diff --git a/extensions/positron-python/pythonFiles/tests/test_data/pyproject-missing-deps.data b/extensions/positron-python/pythonFiles/tests/test_data/pyproject-missing-deps.data index f217a0bdade6..e4d6f9eb10d3 100644 --- a/extensions/positron-python/pythonFiles/tests/test_data/pyproject-missing-deps.data +++ b/extensions/positron-python/pythonFiles/tests/test_data/pyproject-missing-deps.data @@ -5,5 +5,5 @@ build-backend = "flit_core.buildapi" [project] name = "something" version = "2023.0.0" -requires-python = ">=3.7" +requires-python = ">=3.8" dependencies = ["pytest==7.3.1", "flake8-csv"] diff --git a/extensions/positron-python/pythonFiles/tests/test_data/pyproject-no-missing-deps.data b/extensions/positron-python/pythonFiles/tests/test_data/pyproject-no-missing-deps.data index 729bc9169e6f..64dadf6fdf2e 100644 --- a/extensions/positron-python/pythonFiles/tests/test_data/pyproject-no-missing-deps.data +++ b/extensions/positron-python/pythonFiles/tests/test_data/pyproject-no-missing-deps.data @@ -5,5 +5,5 @@ build-backend = "flit_core.buildapi" [project] name = "something" version = "2023.0.0" -requires-python = ">=3.7" +requires-python = ">=3.8" dependencies = [jedi-language-server"] diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py b/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py index c4778aa85852..67e52f43b70c 100644 --- a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py +++ b/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py @@ -6,39 +6,13 @@ from typing import List import pytest -from unittestadapter.discovery import ( - DEFAULT_PORT, - discover_tests, - parse_discovery_cli_args, -) +from unittestadapter.discovery import discover_tests from unittestadapter.utils import TestNodeTypeEnum, parse_unittest_args + from . import expected_discovery_test_output from .helpers import TEST_DATA_PATH, is_same_tree -@pytest.mark.parametrize( - "args, expected", - [ - (["--port", "6767", "--uuid", "some-uuid"], (6767, "some-uuid")), - (["--foo", "something", "--bar", "another"], (int(DEFAULT_PORT), None)), - (["--port", "4444", "--foo", "something", "--port", "9999"], (9999, None)), - ( - ["--uuid", "first-uuid", "--bar", "other", "--uuid", "second-uuid"], - (int(DEFAULT_PORT), "second-uuid"), - ), - ], -) -def test_parse_cli_args(args: List[str], expected: List[str]) -> None: - """The parse_cli_args function should parse and return the port and uuid passed as command-line options. - - If there were no --port or --uuid command-line option, it should return default values). - If there are multiple options, the last one wins. - """ - actual = parse_discovery_cli_args(args) - - assert expected == actual - - @pytest.mark.parametrize( "args, expected", [ diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_execution.py b/extensions/positron-python/pythonFiles/tests/unittestadapter/test_execution.py index 057f64d7396a..f7306e37662e 100644 --- a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_execution.py +++ b/extensions/positron-python/pythonFiles/tests/unittestadapter/test_execution.py @@ -4,55 +4,17 @@ import os import pathlib import sys -from typing import List import pytest script_dir = pathlib.Path(__file__).parent.parent sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -from unittestadapter.execution import parse_execution_cli_args, run_tests +from unittestadapter.execution import run_tests TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" -@pytest.mark.parametrize( - "args, expected", - [ - ( - [ - "--port", - "111", - "--uuid", - "fake-uuid", - ], - (111, "fake-uuid"), - ), - ( - ["--port", "111", "--uuid", "fake-uuid"], - (111, "fake-uuid"), - ), - ( - [ - "--port", - "111", - "--uuid", - "fake-uuid", - "-v", - "-s", - ], - (111, "fake-uuid"), - ), - ], -) -def test_parse_execution_cli_args(args: List[str], expected: List[str]) -> None: - """The parse_execution_cli_args function should return values for the port, uuid, and testids arguments - when passed as command-line options, and ignore unrecognized arguments. - """ - actual = parse_execution_cli_args(args) - assert actual == expected - - def test_no_ids_run() -> None: """This test runs on an empty array of test_ids, therefore it should return an empty dict for the result. diff --git a/extensions/positron-python/pythonFiles/unittestadapter/discovery.py b/extensions/positron-python/pythonFiles/unittestadapter/discovery.py index cbad40ad1838..7e07e45d1202 100644 --- a/extensions/positron-python/pythonFiles/unittestadapter/discovery.py +++ b/extensions/positron-python/pythonFiles/unittestadapter/discovery.py @@ -1,46 +1,27 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import argparse import json import os import pathlib import sys import traceback import unittest -from typing import List, Optional, Tuple, Union +from typing import List, Optional, Union script_dir = pathlib.Path(__file__).parent.parent sys.path.append(os.fspath(script_dir)) sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) from testing_tools import socket_manager +from typing_extensions import Literal, NotRequired, TypedDict # If I use from utils then there will be an import error in test_discovery.py. from unittestadapter.utils import TestNode, build_test_tree, parse_unittest_args -from typing_extensions import NotRequired, TypedDict, Literal - DEFAULT_PORT = "45454" -def parse_discovery_cli_args(args: List[str]) -> Tuple[int, Union[str, None]]: - """Parse command-line arguments that should be processed by the script. - - So far this includes the port number that it needs to connect to, and the uuid passed by the TS side. - The port is passed to the discovery.py script when it is executed, and - defaults to DEFAULT_PORT if it can't be parsed. - The uuid should be passed to the discovery.py script when it is executed, and defaults to None if it can't be parsed. - If the arguments appear several times, the value returned by parse_cli_args will be the value of the last argument. - """ - arg_parser = argparse.ArgumentParser() - arg_parser.add_argument("--port", default=DEFAULT_PORT) - arg_parser.add_argument("--uuid") - parsed_args, _ = arg_parser.parse_known_args(args) - - return int(parsed_args.port), parsed_args.uuid - - class PayloadDict(TypedDict): cwd: str status: Literal["success", "error"] @@ -48,6 +29,13 @@ class PayloadDict(TypedDict): error: NotRequired[List[str]] +class EOTPayloadDict(TypedDict): + """A dictionary that is used to send a end of transmission post request to the server.""" + + command_type: Union[Literal["discovery"], Literal["execution"]] + eot: bool + + def discover_tests( start_dir: str, pattern: str, top_level_dir: Optional[str], uuid: Optional[str] ) -> PayloadDict: @@ -106,17 +94,9 @@ def discover_tests( return payload -if __name__ == "__main__": - # Get unittest discovery arguments. - argv = sys.argv[1:] - index = argv.index("--udiscovery") - - start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) - - # Perform test discovery. - port, uuid = parse_discovery_cli_args(argv[:index]) - payload = discover_tests(start_dir, pattern, top_level_dir, uuid) - +def post_response( + payload: Union[PayloadDict, EOTPayloadDict], port: int, uuid: str +) -> None: # Build the request data (it has to be a POST request or the Node side will not process it), and send it. addr = ("localhost", port) data = json.dumps(payload) @@ -132,3 +112,26 @@ def discover_tests( except Exception as e: print(f"Error sending response: {e}") print(f"Request data: {request}") + + +if __name__ == "__main__": + # Get unittest discovery arguments. + argv = sys.argv[1:] + index = argv.index("--udiscovery") + + start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) + + # Perform test discovery. + testPort = int(os.environ.get("TEST_PORT", DEFAULT_PORT)) + testUuid = os.environ.get("TEST_UUID") + # Post this discovery payload. + if testUuid is not None: + payload = discover_tests(start_dir, pattern, top_level_dir, testUuid) + post_response(payload, testPort, testUuid) + # Post EOT token. + eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} + post_response(eot_payload, testPort, testUuid) + else: + print("Error: no uuid provided or parsed.") + eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} + post_response(eot_payload, testPort, "") diff --git a/extensions/positron-python/pythonFiles/unittestadapter/execution.py b/extensions/positron-python/pythonFiles/unittestadapter/execution.py index f239f81c2d87..0684ada8e44b 100644 --- a/extensions/positron-python/pythonFiles/unittestadapter/execution.py +++ b/extensions/positron-python/pythonFiles/unittestadapter/execution.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import argparse +import atexit import enum import json import os @@ -17,40 +17,17 @@ sys.path.append(os.fspath(script_dir)) sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -from typing_extensions import NotRequired, TypeAlias, TypedDict - from testing_tools import process_json_util, socket_manager +from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict from unittestadapter.utils import parse_unittest_args DEFAULT_PORT = "45454" - -def parse_execution_cli_args( - args: List[str], -) -> Tuple[int, Union[str, None]]: - """Parse command-line arguments that should be processed by the script. - - So far this includes the port number that it needs to connect to, the uuid passed by the TS side, - and the list of test ids to report. - The port is passed to the execution.py script when it is executed, and - defaults to DEFAULT_PORT if it can't be parsed. - The list of test ids is passed to the execution.py script when it is executed, and defaults to an empty list if it can't be parsed. - The uuid should be passed to the execution.py script when it is executed, and defaults to None if it can't be parsed. - If the arguments appear several times, the value returned by parse_cli_args will be the value of the last argument. - """ - arg_parser = argparse.ArgumentParser() - arg_parser.add_argument("--port", default=DEFAULT_PORT) - arg_parser.add_argument("--uuid") - parsed_args, _ = arg_parser.parse_known_args(args) - - return (int(parsed_args.port), parsed_args.uuid) - - ErrorType = Union[ Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None] ] -PORT = 0 -UUID = 0 +testPort = 0 +testUuid = 0 START_DIR = "" @@ -147,9 +124,9 @@ def formatResult( "subtest": subtest.id() if subtest else None, } self.formatted[test_id] = result - if PORT == 0 or UUID == 0: + if testPort == 0 or testUuid == 0: print("Error sending response, port or uuid unknown to python server.") - send_run_data(result, PORT, UUID) + send_run_data(result, testPort, testUuid) class TestExecutionStatus(str, enum.Enum): @@ -168,6 +145,13 @@ class PayloadDict(TypedDict): error: NotRequired[str] +class EOTPayloadDict(TypedDict): + """A dictionary that is used to send a end of transmission post request to the server.""" + + command_type: Union[Literal["discovery"], Literal["execution"]] + eot: bool + + # Args: start_path path to a directory or a file, list of ids that may be empty. # Edge cases: # - if tests got deleted since the VS Code side last ran discovery and the current test run, @@ -225,8 +209,11 @@ def run_tests( return payload +__socket = None +atexit.register(lambda: __socket.close() if __socket else None) + + def send_run_data(raw_data, port, uuid): - # Build the request data (it has to be a POST request or the Node side will not process it), and send it. status = raw_data["outcome"] cwd = os.path.abspath(START_DIR) if raw_data["subtest"]: @@ -236,7 +223,22 @@ def send_run_data(raw_data, port, uuid): test_dict = {} test_dict[test_id] = raw_data payload: PayloadDict = {"cwd": cwd, "status": status, "result": test_dict} + post_response(payload, port, uuid) + + +def post_response( + payload: Union[PayloadDict, EOTPayloadDict], port: int, uuid: str +) -> None: + # Build the request data (it has to be a POST request or the Node side will not process it), and send it. addr = ("localhost", port) + global __socket + if __socket is None: + try: + __socket = socket_manager.SocketManager(addr) + __socket.connect() + except Exception as error: + print(f"Plugin error connection error[vscode-pytest]: {error}") + __socket = None data = json.dumps(payload) request = f"""Content-Length: {len(data)} Content-Type: application/json @@ -244,11 +246,10 @@ def send_run_data(raw_data, port, uuid): {data}""" try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Error sending response: {e}") + if __socket is not None and __socket.socket is not None: + __socket.socket.sendall(request.encode("utf-8")) + except Exception as ex: + print(f"Error sending response: {ex}") print(f"Request data: {request}") @@ -297,11 +298,12 @@ def send_run_data(raw_data, port, uuid): print(f"Error: Could not connect to runTestIdsPort: {e}") print("Error: Could not connect to runTestIdsPort") - PORT, UUID = parse_execution_cli_args(argv[:index]) + testPort = int(os.environ.get("TEST_PORT", DEFAULT_PORT)) + testUuid = os.environ.get("TEST_UUID") if test_ids_from_buffer: # Perform test execution. payload = run_tests( - start_dir, test_ids_from_buffer, pattern, top_level_dir, UUID + start_dir, test_ids_from_buffer, pattern, top_level_dir, testUuid ) else: cwd = os.path.abspath(start_dir) @@ -312,3 +314,9 @@ def send_run_data(raw_data, port, uuid): "error": "No test ids received from buffer", "result": None, } + eot_payload: EOTPayloadDict = {"command_type": "execution", "eot": True} + if testUuid is None: + print("Error sending response, uuid unknown to python server.") + post_response(eot_payload, testPort, "unknown") + else: + post_response(eot_payload, testPort, testUuid) diff --git a/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py b/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py index adf72c134119..2fab4d77c2f8 100644 --- a/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py +++ b/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py @@ -1,7 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import atexit import json import os import pathlib import sys +import time import traceback import pytest @@ -126,7 +131,6 @@ def get_absolute_test_id(test_id: str, testPath: pathlib.Path) -> str: """ split_id = test_id.split("::")[1:] absolute_test_id = "::".join([str(testPath), *split_id]) - print("absolute path", absolute_test_id) return absolute_test_id @@ -196,8 +200,9 @@ def pytest_report_teststatus(report, config): elif report.failed: report_value = "failure" message = report.longreprtext - node_path = map_id_to_path[report.nodeid] - if not node_path: + try: + node_path = map_id_to_path[report.nodeid] + except KeyError: node_path = cwd # Calculate the absolute test id and use this as the ID moving forward. absolute_node_id = get_absolute_test_id(report.nodeid, node_path) @@ -302,12 +307,6 @@ def pytest_sessionfinish(session, exitstatus): 4: Pytest encountered an internal error or exception during test execution. 5: Pytest was unable to find any tests to run. """ - print( - "pytest session has finished, exit status: ", - exitstatus, - "in discovery? ", - IS_DISCOVERY, - ) cwd = pathlib.Path.cwd() if IS_DISCOVERY: if not (exitstatus == 0 or exitstatus == 1 or exitstatus == 5): @@ -353,6 +352,10 @@ def pytest_sessionfinish(session, exitstatus): exitstatus_bool, None, ) + # send end of transmission token + command_type = "discovery" if IS_DISCOVERY else "execution" + payload: EOTPayloadDict = {"command_type": command_type, "eot": True} + send_post_request(payload) def build_test_tree(session: pytest.Session) -> TestNode: @@ -393,9 +396,9 @@ def build_test_tree(session: pytest.Session) -> TestNode: elif hasattr(test_case, "callspec"): # This means it is a parameterized test. function_name: str = "" # parameterized test cases cut the repetitive part of the name off. - name_split = test_node["name"].split("[") - test_node["name"] = "[" + name_split[1] - parent_path = os.fspath(get_node_path(test_case)) + "::" + name_split[0] + parent_part, parameterized_section = test_node["name"].split("[", 1) + test_node["name"] = "[" + parameterized_section + parent_path = os.fspath(get_node_path(test_case)) + "::" + parent_part try: function_name = test_case.originalname # type: ignore function_test_case = function_nodes_dict[parent_path] @@ -604,44 +607,60 @@ class ExecutionPayloadDict(Dict): error: Union[str, None] # Currently unused need to check +class EOTPayloadDict(TypedDict): + """A dictionary that is used to send a end of transmission post request to the server.""" + + command_type: Union[Literal["discovery"], Literal["execution"]] + eot: bool + + def get_node_path(node: Any) -> pathlib.Path: + """A function that returns the path of a node given the switch to pathlib.Path.""" return getattr(node, "path", pathlib.Path(node.fspath)) +__socket = None +atexit.register(lambda: __socket.close() if __socket else None) + + def execution_post( - cwd: str, - status: Literal["success", "error"], - tests: Union[testRunResultDict, None], + cwd: str, status: Literal["success", "error"], tests: Union[testRunResultDict, None] ): """ - Sends a post request to the server after the tests have been executed. - Keyword arguments: - cwd -- the current working directory. - session_node -- the status of running the tests - tests -- the tests that were run and their status. + Sends a POST request with execution payload details. + + Args: + cwd (str): Current working directory. + status (Literal["success", "error"]): Execution status indicating success or error. + tests (Union[testRunResultDict, None]): Test run results, if available. """ - testPort = os.getenv("TEST_PORT", 45454) - testuuid = os.getenv("TEST_UUID") + payload: ExecutionPayloadDict = ExecutionPayloadDict( cwd=cwd, status=status, result=tests, not_found=None, error=None ) if ERRORS: payload["error"] = ERRORS + send_post_request(payload) - addr = ("localhost", int(testPort)) - data = json.dumps(payload) - request = f"""Content-Length: {len(data)} -Content-Type: application/json -Request-uuid: {testuuid} -{data}""" - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Plugin error connection error[vscode-pytest]: {e}") - print(f"[vscode-pytest] data: {request}") +def post_response(cwd: str, session_node: TestNode) -> None: + """ + Sends a POST request with test session details in payload. + + Args: + cwd (str): Current working directory. + session_node (TestNode): Node information of the test session. + """ + + payload: DiscoveryPayloadDict = { + "cwd": cwd, + "status": "success" if not ERRORS else "error", + "tests": session_node, + "error": [], + } + if ERRORS is not None: + payload["error"] = ERRORS + send_post_request(payload, cls_encoder=PathEncoder) class PathEncoder(json.JSONEncoder): @@ -653,35 +672,55 @@ def default(self, obj): return super().default(obj) -def post_response(cwd: str, session_node: TestNode) -> None: - """Sends a post request to the server. +def send_post_request( + payload: Union[ExecutionPayloadDict, DiscoveryPayloadDict, EOTPayloadDict], + cls_encoder=None, +): + """ + Sends a post request to the server. Keyword arguments: - cwd -- the current working directory. - session_node -- the session node, which is the top of the testing tree. - errors -- a list of errors that occurred during test collection. + payload -- the payload data to be sent. + cls_encoder -- a custom encoder if needed. """ - payload: DiscoveryPayloadDict = { - "cwd": cwd, - "status": "success" if not ERRORS else "error", - "tests": session_node, - "error": [], - } - if ERRORS is not None: - payload["error"] = ERRORS - test_port: Union[str, int] = os.getenv("TEST_PORT", 45454) - test_uuid: Union[str, None] = os.getenv("TEST_UUID") - addr = "localhost", int(test_port) - data = json.dumps(payload, cls=PathEncoder) + testPort = os.getenv("TEST_PORT", 45454) + testUuid = os.getenv("TEST_UUID") + addr = ("localhost", int(testPort)) + global __socket + + if __socket is None: + try: + __socket = socket_manager.SocketManager(addr) + __socket.connect() + except Exception as error: + print(f"Plugin error connection error[vscode-pytest]: {error}") + __socket = None + + data = json.dumps(payload, cls=cls_encoder) request = f"""Content-Length: {len(data)} Content-Type: application/json -Request-uuid: {test_uuid} +Request-uuid: {testUuid} {data}""" - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Plugin error connection error[vscode-pytest]: {e}") - print(f"[vscode-pytest] data: {request}") + + max_retries = 3 + retries = 0 + while retries < max_retries: + try: + if __socket is not None and __socket.socket is not None: + __socket.socket.sendall(request.encode("utf-8")) + # print("Post request sent successfully!") + # print("data sent", payload, "end of data") + break # Exit the loop if the send was successful + else: + print("Plugin error connection error[vscode-pytest]") + print(f"[vscode-pytest] data: {request}") + except Exception as error: + print(f"Plugin error connection error[vscode-pytest]: {error}") + print(f"[vscode-pytest] data: {request}") + retries += 1 # Increment retry counter + if retries < max_retries: + print(f"Retrying ({retries}/{max_retries}) in 2 seconds...") + time.sleep(2) # Wait for a short duration before retrying + else: + print("Maximum retry attempts reached. Cannot send post request.") diff --git a/extensions/positron-python/pythonFiles/vscode_pytest/run_pytest_script.py b/extensions/positron-python/pythonFiles/vscode_pytest/run_pytest_script.py index ffb4d0c55b16..0fca8208a406 100644 --- a/extensions/positron-python/pythonFiles/vscode_pytest/run_pytest_script.py +++ b/extensions/positron-python/pythonFiles/vscode_pytest/run_pytest_script.py @@ -53,7 +53,7 @@ buffer = b"" # Process the JSON data - print(f"Received JSON data: {test_ids_from_buffer}") + print("Received JSON data in run script") break except json.JSONDecodeError: # JSON decoding error, the complete JSON object is not yet received diff --git a/extensions/positron-python/requirements.txt b/extensions/positron-python/requirements.txt index f2af0ca4204b..205b9fc4804c 100644 --- a/extensions/positron-python/requirements.txt +++ b/extensions/positron-python/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.7 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # pip-compile --generate-hashes requirements.in @@ -12,9 +12,9 @@ microvenv==2023.2.0 \ --hash=sha256:5b46296d6a65992946da504bd9e724a5becf5c256091f2f9383e5b4e9f567f23 \ --hash=sha256:a07e88a8fb5ee90219b86dd90095cb5646462d45d30285ea3b1a3c7cf33616d3 # via -r requirements.in -packaging==23.1 \ - --hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \ - --hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f +packaging==23.2 \ + --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ + --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 # via -r requirements.in tomli==2.0.1 \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ @@ -23,9 +23,7 @@ tomli==2.0.1 \ typing-extensions==4.7.1 \ --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 - # via - # -r requirements.in - # importlib-metadata + # via -r requirements.in zipp==3.15.0 \ --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 diff --git a/extensions/positron-python/scripts/onCreateCommand.sh b/extensions/positron-python/scripts/onCreateCommand.sh index a90a5366417d..6303d21ef486 100644 --- a/extensions/positron-python/scripts/onCreateCommand.sh +++ b/extensions/positron-python/scripts/onCreateCommand.sh @@ -12,15 +12,15 @@ command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH" source ~/.bashrc # Install Python via pyenv . -pyenv install 3.7:latest 3.8:latest 3.9:latest 3.10:latest 3.11:latest +pyenv install 3.8:latest 3.9:latest 3.10:latest 3.11:latest -# Set default Python version to 3.7 . -pyenv global 3.7.17 +# Set default Python version to 3.8 . +pyenv global 3.8.18 npm ci # Create Virutal environment. -pyenv exec python3.7 -m venv .venv +pyenv exec python3.8 -m venv .venv # Activate Virtual environment. source /workspaces/vscode-python/.venv/bin/activate diff --git a/extensions/positron-python/src/client/application/diagnostics/checks/macPythonInterpreter.ts b/extensions/positron-python/src/client/application/diagnostics/checks/macPythonInterpreter.ts index 19ccc2f8beb9..d44876acd4b9 100644 --- a/extensions/positron-python/src/client/application/diagnostics/checks/macPythonInterpreter.ts +++ b/extensions/positron-python/src/client/application/diagnostics/checks/macPythonInterpreter.ts @@ -40,7 +40,9 @@ export const InvalidMacPythonInterpreterServiceId = 'InvalidMacPythonInterpreter export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { protected changeThrottleTimeout = 1000; - private timeOut?: NodeJS.Timer | number; + // --- Start Positron --- + private timeOut?: NodeJS.Timeout | number; + // --- End Positron --- constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer, diff --git a/extensions/positron-python/src/client/common/application/commands.ts b/extensions/positron-python/src/client/common/application/commands.ts index 2567eca4a69d..b408aae52181 100644 --- a/extensions/positron-python/src/client/common/application/commands.ts +++ b/extensions/positron-python/src/client/common/application/commands.ts @@ -23,8 +23,6 @@ interface ICommandNameWithoutArgumentTypeMapping { [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']: []; @@ -35,7 +33,6 @@ interface ICommandNameWithoutArgumentTypeMapping { ['editor.action.formatDocument']: []; ['editor.action.rename']: []; [Commands.ViewOutput]: []; - [Commands.Set_Linter]: []; [Commands.Start_REPL]: []; [Commands.Enable_SourceMap_Support]: []; [Commands.Exec_Selection_In_Terminal]: []; diff --git a/extensions/positron-python/src/client/common/constants.ts b/extensions/positron-python/src/client/common/constants.ts index 9239e5fec4bf..07a283011281 100644 --- a/extensions/positron-python/src/client/common/constants.ts +++ b/extensions/positron-python/src/client/common/constants.ts @@ -38,9 +38,9 @@ export namespace Commands { export const ClearWorkspaceInterpreter = 'python.clearWorkspaceInterpreter'; export const Create_Environment = 'python.createEnvironment'; 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 Enable_Linter = 'python.enableLinting'; export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; export const Exec_In_Terminal = 'python.execInTerminal'; export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon'; @@ -59,9 +59,7 @@ export namespace Commands { export const PickLocalProcess = 'python.pickLocalProcess'; export const RefreshTensorBoard = 'python.refreshTensorBoard'; export const ReportIssue = 'python.reportIssue'; - export const Run_Linter = 'python.runLinting'; export const Set_Interpreter = 'python.setInterpreter'; - export const Set_Linter = 'python.setLinter'; export const Set_ShebangInterpreter = 'python.setShebangInterpreter'; export const Sort_Imports = 'python.sortImports'; export const Start_REPL = 'python.startREPL'; diff --git a/extensions/positron-python/src/client/common/installer/productInstaller.ts b/extensions/positron-python/src/client/common/installer/productInstaller.ts index f6d27b9edabe..b18733c86100 100644 --- a/extensions/positron-python/src/client/common/installer/productInstaller.ts +++ b/extensions/positron-python/src/client/common/installer/productInstaller.ts @@ -6,12 +6,10 @@ import { CancellationToken, l10n, MessageOptions, 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 } from '../constants'; +import { IApplicationShell, IWorkspaceService } from '../application/types'; import { IProcessServiceFactory, IPythonExecutionFactory } from '../process/types'; import { IConfigurationService, @@ -22,7 +20,7 @@ import { Product, ProductType, } from '../types'; -import { Common, Linters } from '../utils/localize'; +import { Common } from '../utils/localize'; import { isResource, noop } from '../utils/misc'; import { translateProductToModule } from './moduleInstaller'; import { ProductNames } from './productNames'; @@ -235,166 +233,6 @@ abstract class BaseInstaller implements IBaseInstaller { } } -const doNotDisplayFormatterPromptStateKey = 'FORMATTER_NOT_INSTALLED_KEY'; - -export class FormatterInstaller extends BaseInstaller { - protected async promptToInstallImplementation( - product: Product, - resource?: Uri, - cancel?: CancellationToken, - _flags?: ModuleInstallFlags, - // --- Start Positron --- - _options?: InstallOptions, - _messageOptions?: MessageOptions, - // --- End Positron --- - ): Promise { - const neverShowAgain = this.persistentStateFactory.createGlobalPersistentState( - doNotDisplayFormatterPromptStateKey, - false, - ); - - if (neverShowAgain.value) { - return InstallerResponse.Ignore; - } - - // Hard-coded on purpose because the UI won't necessarily work having - // another formatter. - const formatters = [Product.autopep8, Product.black, Product.yapf]; - const formatterNames = formatters.map((formatter) => ProductNames.get(formatter)!); - const productName = ProductNames.get(product)!; - formatterNames.splice(formatterNames.indexOf(productName), 1); - const useOptions = formatterNames.map((name) => l10n.t('Use {0}', name)); - const yesChoice = Common.bannerLabelYes; - - const options = [...useOptions, Common.doNotShowAgain]; - let message = l10n.t('Formatter {0} is not installed. Install?', productName); - if (this.isExecutableAModule(product, resource)) { - options.splice(0, 0, yesChoice); - } else { - const executable = this.getExecutableNameFromSettings(product, resource); - message = l10n.t('Path to the {0} formatter is invalid ({1})', productName, executable); - } - - const item = await this.appShell.showErrorMessage(message, ...options); - if (item === yesChoice) { - return this.install(product, resource, cancel); - } - - if (item === Common.doNotShowAgain) { - neverShowAgain.updateValue(true); - return InstallerResponse.Ignore; - } - - if (typeof item === 'string') { - for (const formatter of formatters) { - const formatterName = ProductNames.get(formatter)!; - - if (item.endsWith(formatterName)) { - await this.configService.updateSetting('formatting.provider', formatterName, resource); - return this.install(formatter, resource, cancel); - } - } - } - - return InstallerResponse.Ignore; - } -} - -export class LinterInstaller extends BaseInstaller { - constructor(protected serviceContainer: IServiceContainer) { - super(serviceContainer); - } - - protected async promptToInstallImplementation( - product: Product, - resource?: Uri, - cancel?: CancellationToken, - _flags?: ModuleInstallFlags, - // --- Start Positron --- - _options?: InstallOptions, - _messageOptions?: MessageOptions, - // --- End Positron --- - ): Promise { - return this.oldPromptForInstallation(product, resource, cancel); - } - - /** - * 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; - } - - private async oldPromptForInstallation(product: Product, resource?: Uri, cancel?: CancellationToken) { - const productName = ProductNames.get(product)!; - const { install } = Common; - const { doNotShowAgain } = Common; - const disableLinterInstallPromptKey = `${productName}_DisableLinterInstallPrompt`; - const { selectLinter } = Linters; - - if (this.getStoredResponse(disableLinterInstallPromptKey) === true) { - return InstallerResponse.Ignore; - } - - const options = [selectLinter, doNotShowAgain]; - - let message = l10n.t('Linter {0} is not installed.', productName); - if (this.isExecutableAModule(product, resource)) { - options.splice(0, 0, install); - } else { - const executable = this.getExecutableNameFromSettings(product, resource); - message = l10n.t('Path to the {0} linter is invalid ({1})', productName, executable); - } - const response = await this.appShell.showErrorMessage(message, ...options); - if (response === install) { - sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { - tool: productName as LinterId, - action: 'install', - }); - return this.install(product, resource, cancel); - } - if (response === doNotShowAgain) { - await this.setStoredResponse(disableLinterInstallPromptKey, true); - sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { - tool: productName as LinterId, - action: 'disablePrompt', - }); - return InstallerResponse.Ignore; - } - - if (response === selectLinter) { - sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { action: 'select' }); - const commandManager = this.serviceContainer.get(ICommandManager); - await commandManager.executeCommand(Commands.Set_Linter); - } - return InstallerResponse.Ignore; - } - - /** - * For installers that want to avoid prompting the user over and over, they can make use of a - * persisted true/false value representing user responses to 'stop showing this prompt'. This - * method will set that persisted value given the installer-defined key. - * - * @param key Key to use to get a persisted response value, each installer must define this for themselves. - * @param value Boolean value to store for the user - if they choose to not be prompted again for instance. - * @returns Boolean: The current state of the stored response key given. - */ - private async setStoredResponse(key: string, value: boolean): Promise { - const factory = this.serviceContainer.get(IPersistentStateFactory); - const state = factory.createGlobalPersistentState(key, undefined); - if (state && state.value !== value) { - await state.updateValue(value); - } - } -} - export class TestFrameworkInstaller extends BaseInstaller { protected async promptToInstallImplementation( product: Product, @@ -733,10 +571,6 @@ export class ProductInstaller implements IInstaller { private createInstaller(product: Product): IBaseInstaller { const productType = this.productService.getProductType(product); switch (productType) { - case ProductType.Formatter: - return new FormatterInstaller(this.serviceContainer); - case ProductType.Linter: - return new LinterInstaller(this.serviceContainer); case ProductType.TestFramework: return new TestFrameworkInstaller(this.serviceContainer); case ProductType.DataScience: diff --git a/extensions/positron-python/src/client/common/persistentState.ts b/extensions/positron-python/src/client/common/persistentState.ts index 76f6d2112fe0..2959a2dc8216 100644 --- a/extensions/positron-python/src/client/common/persistentState.ts +++ b/extensions/positron-python/src/client/common/persistentState.ts @@ -20,6 +20,46 @@ import { import { cache } from './utils/decorators'; import { noop } from './utils/misc'; +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, @@ -93,7 +133,10 @@ export class PersistentStateFactory implements IPersistentStateFactory, IExtensi ) {} public async activate(): Promise { - this.cmdManager?.registerCommand(Commands.ClearStorage, this.cleanAllPersistentStates.bind(this)); + this.cmdManager?.registerCommand(Commands.ClearStorage, async () => { + await clearWorkspaceState(); + await this.cleanAllPersistentStates(); + }); const globalKeysStorageDeprecated = this.createGlobalPersistentState(GLOBAL_PERSISTENT_KEYS_DEPRECATED, []); const workspaceKeysStorageDeprecated = this.createWorkspacePersistentState( WORKSPACE_PERSISTENT_KEYS_DEPRECATED, diff --git a/extensions/positron-python/src/client/common/process/rawProcessApis.ts b/extensions/positron-python/src/client/common/process/rawProcessApis.ts index 6f3e40d68736..23c2f3253bb1 100644 --- a/extensions/positron-python/src/client/common/process/rawProcessApis.ts +++ b/extensions/positron-python/src/client/common/process/rawProcessApis.ts @@ -257,6 +257,10 @@ export function execObservable( subscriber.error(ex); internalDisposables.forEach((d) => d.dispose()); }); + if (options.stdinStr !== undefined) { + proc.stdin?.write(options.stdinStr); + proc.stdin?.end(); + } }); return { diff --git a/extensions/positron-python/src/client/common/utils/localize.ts b/extensions/positron-python/src/client/common/utils/localize.ts index 4cda15e15ec0..bc32c1078cad 100644 --- a/extensions/positron-python/src/client/common/utils/localize.ts +++ b/extensions/positron-python/src/client/common/utils/localize.ts @@ -415,7 +415,7 @@ export namespace Testing { 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 errorPytestExecution = l10n.t('pytest test execution error'); } export namespace OutdatedDebugger { @@ -464,8 +464,10 @@ export namespace CreateEnv { 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('Recreate'); - export const recreateDescription = l10n.t('Delete existing ".venv" environment and create a new one'); + 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( @@ -473,6 +475,7 @@ export namespace CreateEnv { ); export const deletingEnvironmentProgress = l10n.t('Deleting existing ".venv" environment...'); export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".venv" environment.'); + export const openRequirementsFile = l10n.t('Open requirements file'); } export namespace Conda { @@ -485,6 +488,25 @@ export namespace CreateEnv { ); export const creating = l10n.t('Creating conda environment...'); export const providerDescription = l10n.t('Creates a `.conda` Conda environment in the current workspace'); + + export const recreate = l10n.t('Delete and Recreate'); + export const recreateDescription = l10n.t('Delete existing ".conda" environment and create a new one'); + export const useExisting = l10n.t('Use Existing'); + export const useExistingDescription = l10n.t('Use existing ".conda" environment with no changes to it'); + export const existingCondaQuickPickPlaceholder = l10n.t( + 'Choose an option to handle the existing ".conda" environment', + ); + export const deletingEnvironmentProgress = l10n.t('Deleting existing ".conda" environment...'); + export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".conda" environment.'); + } + + export namespace Trigger { + export const workspaceTriggerMessage = l10n.t( + 'A virtual environment is not currently selected for your Python interpreter. Would you like to create a virtual environment?', + ); + export const createEnvironment = l10n.t('Create'); + export const disableCheck = l10n.t('Disable'); + export const disableCheckWorkspace = l10n.t('Disable (Workspace)'); } } diff --git a/extensions/positron-python/src/client/common/utils/multiStepInput.ts b/extensions/positron-python/src/client/common/utils/multiStepInput.ts index e44879e8bbbb..e2b2567b5b4e 100644 --- a/extensions/positron-python/src/client/common/utils/multiStepInput.ts +++ b/extensions/positron-python/src/client/common/utils/multiStepInput.ts @@ -47,7 +47,7 @@ export interface IQuickPickParameters { totalSteps?: number; canGoBack?: boolean; items: T[]; - activeItem?: T | Promise; + activeItem?: T | ((quickPick: QuickPick) => Promise); placeholder: string | undefined; customButtonSetups?: QuickInputButtonSetup[]; matchOnDescription?: boolean; @@ -156,7 +156,13 @@ export class MultiStepInput implements IMultiStepInput { initialize(input); } if (activeItem) { - input.activeItems = [await activeItem]; + if (typeof activeItem === 'function') { + activeItem(input).then((item) => { + if (input.activeItems.length === 0) { + input.activeItems = [item]; + } + }); + } } else { input.activeItems = []; } diff --git a/extensions/positron-python/src/client/common/vscodeApis/windowApis.ts b/extensions/positron-python/src/client/common/vscodeApis/windowApis.ts index 1c242314cb87..c761ff60fa65 100644 --- a/extensions/positron-python/src/client/common/vscodeApis/windowApis.ts +++ b/extensions/positron-python/src/client/common/vscodeApis/windowApis.ts @@ -16,9 +16,15 @@ import { TextEditor, window, Disposable, + QuickPickItemButtonEvent, + Uri, } from 'vscode'; import { createDeferred, Deferred } from '../utils/async'; +export function showTextDocument(uri: Uri): Thenable { + return window.showTextDocument(uri); +} + export function showQuickPick( items: readonly T[] | Thenable, options?: QuickPickOptions, @@ -91,6 +97,7 @@ 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]; @@ -130,6 +137,11 @@ export async function showQuickPickWithBack( deferred.resolve(undefined); } }), + quickPick.onDidTriggerItemButton((e) => { + if (itemButtonHandler) { + itemButtonHandler(e); + } + }), ); if (token) { disposables.push( diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts b/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts index e4c14de407c9..21c8d0f1147b 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts @@ -41,7 +41,7 @@ export class InterpreterPathCommand implements IExtensionSingleActivationService let workspaceFolderUri; try { - workspaceFolderUri = workspaceFolder ? Uri.parse(workspaceFolder) : undefined; + workspaceFolderUri = workspaceFolder ? Uri.file(workspaceFolder) : undefined; } catch (ex) { workspaceFolderUri = undefined; } diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/launch.ts b/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/launch.ts index c4ae6a204d71..274758797eb9 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -16,6 +16,12 @@ import { DebuggerTypeName } from '../../../constants'; import { DebugOptions, DebugPurpose, LaunchRequestArguments } 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 { @@ -84,6 +90,8 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver debugConfiguration.debugOptions!.indexOf(item) === pos, ); } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.Workspace, workspaceFolder); return debugConfiguration; } diff --git a/extensions/positron-python/src/client/debugger/extension/debugCommands.ts b/extensions/positron-python/src/client/debugger/extension/debugCommands.ts index 14a108d27793..b3322e8e7dd1 100644 --- a/extensions/positron-python/src/client/debugger/extension/debugCommands.ts +++ b/extensions/positron-python/src/client/debugger/extension/debugCommands.ts @@ -14,6 +14,10 @@ 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 { @@ -35,6 +39,8 @@ export class DebugCommands implements IExtensionSingleActivationService { 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); }), diff --git a/extensions/positron-python/src/client/extensionActivation.ts b/extensions/positron-python/src/client/extensionActivation.ts index 8dcea063676a..807698f3ec29 100644 --- a/extensions/positron-python/src/client/extensionActivation.ts +++ b/extensions/positron-python/src/client/extensionActivation.ts @@ -28,7 +28,6 @@ import { IDebugConfigurationService, IDynamicDebugConfigurationService } from '. import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; import { IInterpreterService } from './interpreter/contracts'; import { getLanguageConfiguration } from './language/languageConfiguration'; -import { LinterCommands } from './linters/linterCommands'; import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; import { PythonFormattingEditProvider } from './providers/formatProvider'; import { ReplProvider } from './providers/replProvider'; @@ -54,6 +53,8 @@ import { DynamicPythonDebugConfigurationService } from './debugger/extension/con import { IInterpreterQuickPick } from './interpreter/configuration/types'; import { registerInstallFormatterPrompt } from './providers/prompts/installFormatterPrompt'; import { registerAllCreateEnvironmentFeatures } from './pythonEnvironments/creation/registrations'; +import { registerCreateEnvironmentTriggers } from './pythonEnvironments/creation/createEnvironmentTrigger'; +import { initializePersistentStateForTriggers } from './common/persistentState'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -168,8 +169,6 @@ async function activateLegacy(ext: ExtensionState): Promise { serviceManager.get(ICodeExecutionManager).registerCommands(); - disposables.push(new LinterCommands(serviceManager)); - if ( pythonSettings && pythonSettings.formatting && @@ -202,6 +201,8 @@ async function activateLegacy(ext: ExtensionState): Promise { ); registerInstallFormatterPrompt(serviceContainer); + registerCreateEnvironmentTriggers(disposables); + initializePersistentStateForTriggers(ext.context); } } diff --git a/extensions/positron-python/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/extensions/positron-python/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 9015dd7b9388..c11ec221d4d7 100644 --- a/extensions/positron-python/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/extensions/positron-python/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -36,6 +36,8 @@ import { getSearchPathEnvVarNames } from '../../common/utils/exec'; import { EnvironmentVariables } from '../../common/variables/types'; import { TerminalShellType } from '../../common/terminal/types'; import { OSType } from '../../common/utils/platform'; +import { normCase } from '../../common/platform/fs-paths'; +import { PythonEnvType } from '../../pythonEnvironments/base/info'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { @@ -62,6 +64,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ */ private processEnvVars: EnvironmentVariables | undefined; + private separator: string; + constructor( @inject(IPlatformService) private readonly platform: IPlatformService, @inject(IInterpreterService) private interpreterService: IInterpreterService, @@ -74,7 +78,9 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(IConfigurationService) private readonly configurationService: IConfigurationService, @inject(IPathUtils) private readonly pathUtils: IPathUtils, - ) {} + ) { + this.separator = platform.osType === OSType.Windows ? ';' : ':'; + } public async activate(resource: Resource): Promise { try { @@ -96,21 +102,17 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (!this.registeredOnce) { this.interpreterService.onDidChangeInterpreter( async (r) => { - this.showProgress(); await this._applyCollection(r).ignoreErrors(); - this.hideProgress(); }, this, this.disposables, ); this.applicationEnvironment.onDidChangeShell( async (shell: string) => { - this.showProgress(); 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.hideProgress(); }, this, this.disposables, @@ -123,22 +125,28 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } } - public async _applyCollection(resource: Resource, shell = this.applicationEnvironment.shell): Promise { + public async _applyCollection(resource: Resource, shell?: string): Promise { + this.showProgress(); + await this._applyCollectionImpl(resource, shell); + this.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 }); - // Clear any previously set env vars from collection - envVarCollection.clear(); if (!settings.terminal.activateEnvironment) { + envVarCollection.clear(); traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); return; } - const env = await this.environmentActivationService.getActivatedEnvironmentVariables( + const activatedEnv = await this.environmentActivationService.getActivatedEnvironmentVariables( resource, undefined, undefined, shell, ); + const env = activatedEnv ? normCaseKeys(activatedEnv) : undefined; if (!env) { const shellType = identifyShellFromShellPath(shell); const defaultShell = defaultShells[this.platform.osType]; @@ -149,6 +157,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ return; } await this.trackTerminalPrompt(shell, resource, env); + envVarCollection.clear(); this.processEnvVars = undefined; return; } @@ -158,11 +167,13 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ shell, ); } - const processEnv = this.processEnvVars; + 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); + // Clear any previously set env vars from collection + envVarCollection.clear(); Object.keys(env).forEach((key) => { if (shouldSkip(key)) { return; @@ -192,6 +203,9 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ applyAtProcessCreation: true, }); } else { + if (!value.endsWith(this.separator)) { + value = value.concat(this.separator); + } traceVerbose(`Prepending environment variable ${key} in collection to ${value}`); envVarCollection.prepend(key, value, { applyAtShellIntegration: true, @@ -253,8 +267,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ 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 shouldPS1BeSet = interpreter?.type !== undefined; - if (shouldPS1BeSet && !env.PS1) { + const shouldSetPS1 = shouldPS1BeSet(interpreter?.type, env); + if (shouldSetPS1 && !env.PS1) { // PS1 should be set but no PS1 was set. return; } @@ -270,22 +284,25 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } private async getPS1(shell: string, resource: Resource, env: EnvironmentVariables) { - if (env.PS1) { - return env.PS1; - } const customShellType = identifyShellFromShellPath(shell); if (this.noPromptVariableShells.includes(customShellType)) { - return undefined; + 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 shouldPS1BeSet = interpreter?.type !== undefined; - if (shouldPS1BeSet && !env.PS1) { - // PS1 should be set but no PS1 was set. - return getPromptForEnv(interpreter); + const shouldSetPS1 = shouldPS1BeSet(interpreter?.type, env); + if (shouldSetPS1) { + const prompt = getPromptForEnv(interpreter); + 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; } @@ -356,6 +373,26 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } } +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'].includes(env); } @@ -376,3 +413,11 @@ function getPromptForEnv(interpreter: PythonEnvironment | undefined) { } 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/extensions/positron-python/src/client/interpreter/autoSelection/index.ts b/extensions/positron-python/src/client/interpreter/autoSelection/index.ts index a57577c8c918..7714c487ed30 100644 --- a/extensions/positron-python/src/client/interpreter/autoSelection/index.ts +++ b/extensions/positron-python/src/client/interpreter/autoSelection/index.ts @@ -181,6 +181,11 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio return this.stateFactory.createWorkspacePersistentState(key, undefined); } + private getAutoSelectionQueriedOnceState(): IPersistentState { + const key = `autoSelectionInterpretersQueriedOnce`; + return this.stateFactory.createWorkspacePersistentState(key, undefined); + } + /** * Auto-selection logic: * 1. If there are cached interpreters (not the first session in this workspace) @@ -200,7 +205,12 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio }); } - await this.interpreterService.refreshPromise; + const globalQueriedState = this.getAutoSelectionQueriedOnceState(); + 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); const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); @@ -215,6 +225,7 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio } queriedState.updateValue(true); + globalQueriedState.updateValue(true); this.didAutoSelectedInterpreterEmitter.fire(); } diff --git a/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index c0876ff518dd..9b8ecec74f9f 100644 --- a/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -50,7 +50,7 @@ import { BaseInterpreterSelectorCommand } from './base'; const untildify = require('untildify'); export type InterpreterStateArgs = { path?: string; workspace: Resource }; -type QuickPickType = IInterpreterQuickPickItem | ISpecialQuickPickItem | QuickPickItem; +export type QuickPickType = IInterpreterQuickPickItem | ISpecialQuickPickItem | QuickPickItem; function isInterpreterQuickPickItem(item: QuickPickType): item is IInterpreterQuickPickItem { return 'interpreter' in item; @@ -177,7 +177,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem items: suggestions, sortByLabel: !preserveOrderWhenFiltering, keepScrollPosition: true, - activeItem: this.getActiveItem(state.workspace, suggestions), // Use a promise here to ensure quickpick is initialized synchronously. + activeItem: (quickPick) => this.getActiveItem(state.workspace, quickPick), // Use a promise here to ensure quickpick is initialized synchronously. matchOnDetail: true, matchOnDescription: true, title, @@ -277,8 +277,9 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem return getGroupedQuickPickItems(items, recommended, workspaceFolder?.uri.fsPath); } - private async getActiveItem(resource: Resource, suggestions: QuickPickType[]) { + 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, ); @@ -339,7 +340,9 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem return false; }) : undefined; - quickPick.activeItems = activeItem ? [activeItem] : []; + if (activeItem) { + quickPick.activeItems = [activeItem]; + } } /** diff --git a/extensions/positron-python/src/client/interpreter/interpreterService.ts b/extensions/positron-python/src/client/interpreter/interpreterService.ts index b595fc2365a8..c97a35c4a973 100644 --- a/extensions/positron-python/src/client/interpreter/interpreterService.ts +++ b/extensions/positron-python/src/client/interpreter/interpreterService.ts @@ -85,6 +85,11 @@ export class InterpreterService implements Disposable, IInterpreterService { 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, @@ -100,10 +105,12 @@ export class InterpreterService implements Disposable, IInterpreterService { const workspaceFolder = this.serviceContainer .get(IWorkspaceService) .getWorkspaceFolder(resource); - this.ensureEnvironmentContainsPython( - this.configService.getSettings(resource).pythonPath, - workspaceFolder, - ).ignoreErrors(); + const path = this.configService.getSettings(resource).pythonPath; + const workspaceKey = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolderIdentifier(resource); + this.activeInterpreterPaths.set(workspaceKey, { path, workspaceFolder }); + this.ensureEnvironmentContainsPython(path, workspaceFolder).ignoreErrors(); } public initialize(): void { @@ -155,6 +162,16 @@ export class InterpreterService implements Disposable, IInterpreterService { 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, + }); + } + } } }), ); @@ -246,6 +263,10 @@ export class InterpreterService implements Disposable, IInterpreterService { path: pySettings.pythonPath, resource: workspaceFolder, }); + const workspaceKey = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolderIdentifier(resource); + this.activeInterpreterPaths.set(workspaceKey, { path: pySettings.pythonPath, workspaceFolder }); const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); interpreterDisplay.refresh().catch((ex) => traceError('Python Extension: display.refresh', ex)); await this.ensureEnvironmentContainsPython(this._pythonPathSetting, workspaceFolder); diff --git a/extensions/positron-python/src/client/linters/errorHandlers/errorHandler.ts b/extensions/positron-python/src/client/linters/errorHandlers/errorHandler.ts index dc884e97739c..af28dd61c3a4 100644 --- a/extensions/positron-python/src/client/linters/errorHandlers/errorHandler.ts +++ b/extensions/positron-python/src/client/linters/errorHandlers/errorHandler.ts @@ -3,17 +3,13 @@ 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, serviceContainer: IServiceContainer) { - // Create chain of handlers. - const standardErrorHandler = new StandardErrorHandler(product, serviceContainer); - this.handler = new NotInstalledErrorHandler(product, serviceContainer); - this.handler.setNextHandler(standardErrorHandler); + this.handler = new StandardErrorHandler(product, serviceContainer); } public handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { diff --git a/extensions/positron-python/src/client/linters/errorHandlers/notInstalled.ts b/extensions/positron-python/src/client/linters/errorHandlers/notInstalled.ts deleted file mode 100644 index 8c598ae5ece2..000000000000 --- a/extensions/positron-python/src/client/linters/errorHandlers/notInstalled.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Uri } from 'vscode'; -import { IPythonExecutionFactory } from '../../common/process/types'; -import { ExecutionInfo } from '../../common/types'; -import { traceError, traceLog, traceWarn } from '../../logging'; -import { ILinterManager } from '../types'; -import { BaseErrorHandler } from './baseErrorHandler'; - -export class NotInstalledErrorHandler extends BaseErrorHandler { - public async handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { - const pythonExecutionService = await this.serviceContainer - .get(IPythonExecutionFactory) - .create({ resource }); - const isModuleInstalled = await pythonExecutionService.isModuleInstalled(execInfo.moduleName!); - if (isModuleInstalled) { - return this.nextHandler ? this.nextHandler.handleError(error, resource, execInfo) : false; - } - - this.installer - .promptToInstall(this.product, resource) - .catch((ex) => traceError('NotInstalledErrorHandler.promptToInstall', ex)); - - const linterManager = this.serviceContainer.get(ILinterManager); - const info = linterManager.getLinterInfo(execInfo.product!); - const customError = `Linter '${info.id}' is not installed. Please install it or select another linter".`; - traceLog(`\n${customError}\n${error}`); - traceWarn(customError, error); - return true; - } -} diff --git a/extensions/positron-python/src/client/linters/errorHandlers/standard.ts b/extensions/positron-python/src/client/linters/errorHandlers/standard.ts index f6e04b50ff19..6367da7abe4a 100644 --- a/extensions/positron-python/src/client/linters/errorHandlers/standard.ts +++ b/extensions/positron-python/src/client/linters/errorHandlers/standard.ts @@ -18,6 +18,24 @@ export class StandardErrorHandler extends BaseErrorHandler { const info = linterManager.getLinterInfo(execInfo.product!); traceError(`There was an error in running the linter ${info.id}`, error); + if (info.id === LinterId.PyLint) { + traceError('Support for "pylint" is moved to ms-python.pylint extension.'); + traceError( + 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.pylint', + ); + } else if (info.id === LinterId.Flake8) { + traceError('Support for "flake8" is moved to ms-python.flake8 extension.'); + traceError( + 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.flake8', + ); + } else if (info.id === LinterId.MyPy) { + traceError('Support for "mypy" is moved to ms-python.mypy-type-checker extension.'); + traceError( + 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker', + ); + } + traceError(`If the error is due to missing ${info.id}, please install ${info.id} using pip manually.`); + traceError('Learn more here: https://aka.ms/AAlgvkb'); traceLog(`Linting with ${info.id} failed.`); traceLog(error.toString()); diff --git a/extensions/positron-python/src/client/linters/linterCommands.ts b/extensions/positron-python/src/client/linters/linterCommands.ts deleted file mode 100644 index cc35e80f26b1..000000000000 --- a/extensions/positron-python/src/client/linters/linterCommands.ts +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DiagnosticCollection, Disposable, l10n, QuickPickOptions, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager, IDocumentManager } from '../common/application/types'; -import { Commands } from '../common/constants'; -import { IDisposable } from '../common/types'; -import { Common } 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(): void { - 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(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( - l10n.t("Multiple linters are enabled in settings. Replace with '{0}'?", selection), - Common.bannerLabelYes, - Common.bannerLabelNo, - ); - if (response !== Common.bannerLabelYes) { - 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 = ['Enable', 'Disable']; - const current = (await this.linterManager.isLintingEnabled(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: boolean = selection === options[0]; - await this.linterManager.enableLintingAsync(enable, this.settingsUri); - } - } - - public runLinting(): Promise { - const engine = this.serviceContainer.get(ILintingEngine); - return engine.lintOpenPythonFiles('manual'); - } - - private get settingsUri(): Uri | undefined { - return this.documentManager.activeTextEditor ? this.documentManager.activeTextEditor.document.uri : undefined; - } -} diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/info/envKind.ts b/extensions/positron-python/src/client/pythonEnvironments/base/info/envKind.ts index 8828003c5ce7..ff53a57d2f45 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/info/envKind.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/info/envKind.ts @@ -12,15 +12,15 @@ 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.MicrosoftStore, 'Microsoft Store'], [PythonEnvKind.Pyenv, 'pyenv'], - [PythonEnvKind.Poetry, 'poetry'], + [PythonEnvKind.Poetry, 'Poetry'], [PythonEnvKind.Custom, 'custom'], // For now we treat OtherGlobal like Unknown. [PythonEnvKind.Venv, 'venv'], [PythonEnvKind.VirtualEnv, 'virtualenv'], [PythonEnvKind.VirtualEnvWrapper, 'virtualenv'], - [PythonEnvKind.Pipenv, 'pipenv'], + [PythonEnvKind.Pipenv, 'Pipenv'], [PythonEnvKind.Conda, 'conda'], [PythonEnvKind.ActiveState, 'ActiveState'], // For now we treat OtherVirtual like Unknown. diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts b/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts index 2567168c6325..fb1a791d07ed 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts @@ -115,9 +115,9 @@ export class EnvsCollectionService extends PythonEnvsWatcher this.sendTelemetry(query, stopWatch)); } - return refreshPromise.then(() => this.sendTelemetry(query, stopWatch)); + return refreshPromise; } private startRefresh(query: PythonLocatorQuery | undefined): Promise { diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts index d86b2182d50c..71f3d69e9067 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { uniq } from 'lodash'; +import { toLower, uniq, uniqBy } from 'lodash'; import * as path from 'path'; import { chain, iterable } from '../../../../common/utils/async'; import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../../common/utils/platform'; @@ -39,10 +39,14 @@ async function getGlobalVirtualEnvDirs(): Promise { const homeDir = getUserHomeDir(); if (homeDir && (await pathExists(homeDir))) { - const subDirs = ['Envs', '.direnv', '.venvs', '.virtualenvs', path.join('.local', 'share', 'virtualenvs')]; - if (getOSType() !== OSType.Windows) { - subDirs.push('envs'); - } + const subDirs = [ + 'envs', + 'Envs', + '.direnv', + '.venvs', + '.virtualenvs', + path.join('.local', 'share', 'virtualenvs'), + ]; const filtered = await asyncFilter( subDirs.map((d) => path.join(homeDir, d)), pathExists, @@ -50,7 +54,7 @@ async function getGlobalVirtualEnvDirs(): Promise { filtered.forEach((d) => venvDirs.push(d)); } - return uniq(venvDirs); + return [OSType.Windows, OSType.OSX].includes(getOSType()) ? uniqBy(venvDirs, toLower) : uniq(venvDirs); } /** diff --git a/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts b/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts index 88178d02d58a..8f048ddd0676 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -23,6 +23,7 @@ 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'; export const AnacondaCompanyName = 'Anaconda, Inc.'; export const CONDAPATH_SETTING_KEY = 'condaPath'; @@ -238,7 +239,7 @@ export function getCondaInterpreterPath(condaEnvironmentPath: string): string { // 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 = 50000; +const CONDA_GENERAL_TIMEOUT = 45000; /** Wraps the "conda" utility, and exposes its functionality. */ @@ -439,9 +440,19 @@ export class Conda { if (shellPath) { options.shell = shellPath; } - const result = await exec(command, ['info', '--json'], options); - traceVerbose(`${command} info --json: ${result.stdout}`); - return JSON.parse(result.stdout); + const resultPromise = exec(command, ['info', '--json'], options); + // 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`); } /** diff --git a/extensions/positron-python/src/client/pythonEnvironments/common/posixUtils.ts b/extensions/positron-python/src/client/pythonEnvironments/common/posixUtils.ts index eb60fc029949..0e79ec9d590e 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/common/posixUtils.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/common/posixUtils.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import { uniq } from 'lodash'; import { getSearchPathEntries } from '../../common/utils/exec'; import { resolveSymbolicLink } from './externalDependencies'; -import { traceError, traceInfo, traceVerbose } from '../../logging'; +import { traceError, traceInfo, traceVerbose, traceWarn } from '../../logging'; /** * Determine if the given filename looks like the simplest Python executable. @@ -117,7 +117,10 @@ function pickShortestPath(pythonPaths: string[]) { export async function getPythonBinFromPosixPaths(searchDirs: string[]): Promise { const binToLinkMap = new Map(); for (const searchDir of searchDirs) { - const paths = await findPythonBinariesInDir(searchDir); + 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 diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/common/commonUtils.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/common/commonUtils.ts index b4d4a37eae9b..16d8015e3f26 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/common/commonUtils.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/common/commonUtils.ts @@ -32,3 +32,11 @@ export function getVenvExecutable(workspaceFolder: WorkspaceFolder): string { } 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/extensions/positron-python/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts new file mode 100644 index 000000000000..0c1c2b38eab2 --- /dev/null +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as fsapi from 'fs-extra'; +import { ConfigurationTarget, Uri, WorkspaceFolder } from 'vscode'; +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, updateWorkspaceStateValue } 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. + * @export + * @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); + } +} + +/** + * Sets trigger to 'off' in workspace persistent state. This disables trigger check + * for the current workspace only. In multi root case, it is disabled for all folders + * in the multi root workspace. + */ +export async function disableWorkspaceCreateEnvironmentTrigger(): Promise { + await updateWorkspaceStateValue(CREATE_ENV_TRIGGER_SETTING, 'off'); +} + +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/extensions/positron-python/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts new file mode 100644 index 000000000000..1737d351ca7b --- /dev/null +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts @@ -0,0 +1,160 @@ +// 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, + disableWorkspaceCreateEnvironmentTrigger, +} 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 { 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, + CreateEnv.Trigger.disableCheckWorkspace, + CreateEnv.Trigger.disableCheck, + ); + + if (selection === CreateEnv.Trigger.createEnvironment) { + try { + await executeCommand(Commands.Create_Environment); + } catch (error) { + traceError('CreateEnv Trigger - Error while creating environment: ', error); + } + } else if (selection === CreateEnv.Trigger.disableCheck) { + disableCreateEnvironmentTrigger(); + } else if (selection === CreateEnv.Trigger.disableCheckWorkspace) { + disableWorkspaceCreateEnvironmentTrigger(); + } +} + +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/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index 7ca44c1b7eff..9dff50c5586d 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -9,11 +9,18 @@ import { CreateEnvironmentProgress } from '../types'; import { pickWorkspaceFolder } from '../common/workspaceSelection'; import { execObservable } from '../../../common/process/rawProcessApis'; import { createDeferred } from '../../../common/utils/async'; -import { getEnvironmentVariable, getOSType, OSType } from '../../../common/utils/platform'; +import { getOSType, OSType } from '../../../common/utils/platform'; import { createCondaScript } from '../../../common/process/internal/scripts'; import { Common, CreateEnv } from '../../../common/utils/localize'; -import { getCondaBaseEnv, pickPythonVersion } from './condaUtils'; -import { showErrorMessageWithLogs } from '../common/commonUtils'; +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'; @@ -83,22 +90,7 @@ async function createCondaEnv( }); const deferred = createDeferred(); - let 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(command); - 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); - pathEnv = `${libPath}${path.delimiter}${pathEnv}`; - } + const pathEnv = getPathEnvVariableForConda(command); traceLog('Running Conda Env creation script: ', [command, ...args]); const { proc, out, dispose } = execObservable(command, args, { mergeStdOutErr: true, @@ -182,35 +174,93 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise { + 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 () => { - try { - version = await pickPythonVersion(); - } catch (ex) { - if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { - return ex; + 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; } - 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.`); return MultiStepAction.Continue; }, undefined, ); - workspaceStep.next = versionStep; + 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 }; + } + } + return withProgress( { location: ProgressLocation.Notification, diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaDeleteUtils.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaDeleteUtils.ts new file mode 100644 index 000000000000..e4f4784f15c8 --- /dev/null +++ b/extensions/positron-python/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/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaUtils.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaUtils.ts index e00a1c8dca09..51c55e414245 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaUtils.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -1,14 +1,22 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, QuickPickItem, Uri } from 'vscode'; -import { Common } from '../../../browser/localize'; -import { Octicons } from '../../../common/constants'; -import { CreateEnv } from '../../../common/utils/localize'; +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 { showErrorMessage, showQuickPickWithBack } from '../../../common/vscodeApis/windowApis'; +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.10'; @@ -39,7 +47,7 @@ export async function getCondaBaseEnv(): Promise { } export async function pickPythonVersion(token?: CancellationToken): Promise { - const items: QuickPickItem[] = ['3.10', '3.11', '3.9', '3.8', '3.7'].map((v) => ({ + const items: QuickPickItem[] = ['3.10', '3.11', '3.9', '3.8'].map((v) => ({ label: v === RECOMMENDED_CONDA_PYTHON ? `${Octicons.Star} Python` : 'Python', description: v, })); @@ -59,3 +67,78 @@ export async function pickPythonVersion(token?: CancellationToken): 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/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index edbdcd7d84a6..61850e404c3d 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -32,8 +32,14 @@ import { CreateEnvironmentResult, } from '../proposed.createEnvApis'; -function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgnore?: boolean): string[] { +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'); @@ -52,14 +58,21 @@ function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgn }); const requirements = installInfo.filter((i) => i.installType === 'requirements').map((i) => i.installItem); - requirements.forEach((r) => { - if (r) { - command.push('--requirements', r); - } - }); + + 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 command; + return { argv: command, stdin }; } function getVenvFromOutput(output: string): string | undefined { @@ -81,7 +94,7 @@ function getVenvFromOutput(output: string): string | undefined { async function createVenv( workspace: WorkspaceFolder, command: string, - args: string[], + args: IVenvCommandArgs, progress: CreateEnvironmentProgress, token?: CancellationToken, ): Promise { @@ -94,11 +107,15 @@ async function createVenv( }); const deferred = createDeferred(); - traceLog('Running Env creation script: ', [command, ...args]); - const { proc, out, dispose } = execObservable(command, args, { + 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); diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvUtils.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvUtils.ts index d7a0be170f99..cc506a11a88f 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvUtils.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvUtils.ts @@ -5,12 +5,22 @@ import * as tomljs from '@iarna/toml'; import * as fs from 'fs-extra'; import { flatten, isArray } from 'lodash'; import * as path from 'path'; -import { CancellationToken, ProgressLocation, QuickPickItem, RelativePattern, WorkspaceFolder } from 'vscode'; +import { + CancellationToken, + ProgressLocation, + QuickPickItem, + QuickPickItemButtonEvent, + RelativePattern, + ThemeIcon, + Uri, + WorkspaceFolder, +} from 'vscode'; import { Common, CreateEnv } from '../../../common/utils/localize'; import { MultiStepAction, MultiStepNode, showQuickPickWithBack, + showTextDocument, withProgress, } from '../../../common/vscodeApis/windowApis'; import { findFiles } from '../../../common/vscodeApis/workspaceApis'; @@ -20,8 +30,12 @@ import { isWindows } from '../../../common/platform/platformService'; 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__}/**'; -async function getPipRequirementsFiles( +export async function getPipRequirementsFiles( workspaceFolder: WorkspaceFolder, token?: CancellationToken, ): Promise { @@ -78,8 +92,13 @@ async function pickTomlExtras(extras: string[], token?: CancellationToken): Prom return undefined; } -async function pickRequirementsFiles(files: string[], token?: CancellationToken): Promise { +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; @@ -91,7 +110,10 @@ async function pickRequirementsFiles(files: string[], token?: CancellationToken) } return al - bl; }) - .map((e) => ({ label: e })); + .map((e) => ({ + label: e, + buttons: [OPEN_REQUIREMENTS_BUTTON], + })); const selection = await showQuickPickWithBack( items, @@ -101,6 +123,11 @@ async function pickRequirementsFiles(files: string[], token?: CancellationToken) canPickMany: true, }, token, + async (e: QuickPickItemButtonEvent) => { + if (e.item.label) { + await showTextDocument(Uri.file(path.join(root, e.item.label))); + } + }, ); if (selection && isArray(selection)) { @@ -195,14 +222,11 @@ export async function pickPackagesToInstall( tomlStep, async (context?: MultiStepAction) => { traceVerbose('Looking for pip requirements.'); - const requirementFiles = (await getPipRequirementsFiles(workspaceFolder, token))?.map((p) => - path.relative(workspaceFolder.uri.fsPath, p), - ); - + const requirementFiles = await getPipRequirementsFiles(workspaceFolder, token); if (requirementFiles && requirementFiles.length > 0) { traceVerbose('Found pip requirements.'); try { - const result = await pickRequirementsFiles(requirementFiles, token); + 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) => { @@ -268,11 +292,14 @@ export async function pickExistingVenvAction( if (workspaceFolder) { if (await hasVenv(workspaceFolder)) { const items: QuickPickItem[] = [ - { label: CreateEnv.Venv.recreate, description: CreateEnv.Venv.recreateDescription }, { label: CreateEnv.Venv.useExisting, description: CreateEnv.Venv.useExistingDescription, }, + { + label: CreateEnv.Venv.recreate, + description: CreateEnv.Venv.recreateDescription, + }, ]; const selection = (await showQuickPickWithBack( diff --git a/extensions/positron-python/src/client/pythonEnvironments/info/index.ts b/extensions/positron-python/src/client/pythonEnvironments/info/index.ts index ee2ff9d7cc22..17e8958f6310 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/info/index.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/info/index.ts @@ -98,7 +98,7 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType): string return 'conda'; } case EnvironmentType.Pipenv: { - return 'pipenv'; + return 'Pipenv'; } case EnvironmentType.Pyenv: { return 'pyenv'; @@ -110,16 +110,16 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType): string return 'virtualenv'; } case EnvironmentType.MicrosoftStore: { - return 'microsoft store'; + return 'Microsoft Store'; } case EnvironmentType.Poetry: { - return 'poetry'; + return 'Poetry'; } case EnvironmentType.VirtualEnvWrapper: { return 'virtualenvwrapper'; } case EnvironmentType.ActiveState: { - return 'activestate'; + return 'ActiveState'; } default: { return ''; diff --git a/extensions/positron-python/src/client/telemetry/constants.ts b/extensions/positron-python/src/client/telemetry/constants.ts index a729b3d491e8..c680b91094cb 100644 --- a/extensions/positron-python/src/client/telemetry/constants.ts +++ b/extensions/positron-python/src/client/telemetry/constants.ts @@ -115,6 +115,9 @@ export enum EventName { ENVIRONMENT_DELETE = 'ENVIRONMENT.DELETE', ENVIRONMENT_REUSE = 'ENVIRONMENT.REUSE', + ENVIRONMENT_CHECK_TRIGGER = 'ENVIRONMENT.CHECK.TRIGGER', + ENVIRONMENT_CHECK_RESULT = 'ENVIRONMENT.CHECK.RESULT', + TOOLS_EXTENSIONS_ALREADY_INSTALLED = 'TOOLS_EXTENSIONS.ALREADY_INSTALLED', TOOLS_EXTENSIONS_PROMPT_SHOWN = 'TOOLS_EXTENSIONS.PROMPT_SHOWN', TOOLS_EXTENSIONS_INSTALL_SELECTED = 'TOOLS_EXTENSIONS.INSTALL_SELECTED', diff --git a/extensions/positron-python/src/client/telemetry/importTracker.ts b/extensions/positron-python/src/client/telemetry/importTracker.ts index 06991a815140..e00714d24cbb 100644 --- a/extensions/positron-python/src/client/telemetry/importTracker.ts +++ b/extensions/positron-python/src/client/telemetry/importTracker.ts @@ -49,7 +49,9 @@ const testExecution = isTestExecution(); export class ImportTracker implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; - private pendingChecks = new Map(); + // --- Start Positron --- + private pendingChecks = new Map(); + // --- End Positron --- private static sentMatches: Set = new Set(); diff --git a/extensions/positron-python/src/client/telemetry/index.ts b/extensions/positron-python/src/client/telemetry/index.ts index f4947cd73f05..600f9a2d48ff 100644 --- a/extensions/positron-python/src/client/telemetry/index.ts +++ b/extensions/positron-python/src/client/telemetry/index.ts @@ -2144,6 +2144,34 @@ export interface IEventNamePropertyMapping { [EventName.ENVIRONMENT_REUSE]: { environmentType: 'venv' | 'conda'; }; + /** + * Telemetry event sent when a check for environment creation conditions is triggered. + */ + /* __GDPR__ + "environemt.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__ + "environemt.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 a linter or formatter extension is already installed. */ diff --git a/extensions/positron-python/src/client/terminals/codeExecution/codeExecutionManager.ts b/extensions/positron-python/src/client/terminals/codeExecution/codeExecutionManager.ts index 05265a0e918f..37ddea169891 100644 --- a/extensions/positron-python/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/extensions/positron-python/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -22,6 +22,10 @@ import { traceError } 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'; @injectable() export class CodeExecutionManager implements ICodeExecutionManager { @@ -52,6 +56,10 @@ export class CodeExecutionManager implements ICodeExecutionManager { .then(noop, noop); return; } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { + trigger: 'run-in-terminal', + }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; await this.executeFileInTerminal(file, trigger, { newTerminalPerFile: cmd === Commands.Exec_In_Separate_Terminal, @@ -124,6 +132,8 @@ export class CodeExecutionManager implements ICodeExecutionManager { 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'); @@ -140,6 +150,8 @@ export class CodeExecutionManager implements ICodeExecutionManager { 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'); diff --git a/extensions/positron-python/src/client/testing/testController/common/resultResolver.ts b/extensions/positron-python/src/client/testing/testController/common/resultResolver.ts index 6e875473c836..79cee6452a8c 100644 --- a/extensions/positron-python/src/client/testing/testController/common/resultResolver.ts +++ b/extensions/positron-python/src/client/testing/testController/common/resultResolver.ts @@ -3,7 +3,7 @@ import { CancellationToken, TestController, TestItem, Uri, TestMessage, Location, TestRun } from 'vscode'; import * as util from 'util'; -import { DiscoveredTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; +import { DiscoveredTestPayload, EOTTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; import { TestProvider } from '../../types'; import { traceError, traceLog } from '../../../logging'; import { Testing } from '../../../common/utils/localize'; @@ -11,7 +11,8 @@ import { clearAllChildren, createErrorTestItem, getTestCaseNodes } from './testI import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { splitLines } from '../../../common/stringUtils'; -import { buildErrorNodeOptions, fixLogLines, populateTestTree } from './utils'; +import { buildErrorNodeOptions, fixLogLines, populateTestTree, splitTestNameWithRegex } from './utils'; +import { Deferred } from '../../../common/utils/async'; export class PythonResultResolver implements ITestResultResolver { testController: TestController; @@ -35,16 +36,30 @@ export class PythonResultResolver implements ITestResultResolver { this.vsIdToRunId = new Map(); } - public resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise { - const workspacePath = this.workspaceUri.fsPath; - traceLog('Using result resolver for discovery'); - - const rawTestData = payload; - if (!rawTestData) { + public resolveDiscovery( + payload: DiscoveredTestPayload | EOTTestPayload, + deferredTillEOT: Deferred, + token?: CancellationToken, + ): Promise { + if (!payload) { // No test data is available return Promise.resolve(); } + if ('eot' in payload) { + // the payload is an EOT payload, so resolve the deferred promise. + traceLog('ResultResolver EOT received for discovery.'); + const eotPayload = payload as EOTTestPayload; + if (eotPayload.eot === true) { + deferredTillEOT.resolve(); + return Promise.resolve(); + } + } + return this._resolveDiscovery(payload as DiscoveredTestPayload, token); + } + public _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise { + const workspacePath = this.workspaceUri.fsPath; + const rawTestData = payload as DiscoveredTestPayload; // Check if there were any errors in the discovery process. if (rawTestData.status === 'error') { const testingErrorConst = @@ -77,7 +92,12 @@ export class PythonResultResolver implements ITestResultResolver { populateTestTree(this.testController, rawTestData.tests, undefined, this, token); } else { // Delete everything from the test controller. + const errorNode = this.testController.items.get(`DiscoveryError:${workspacePath}`); this.testController.items.replace([]); + // Add back the error node if it exists. + if (errorNode !== undefined) { + this.testController.items.add(errorNode); + } } sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { @@ -87,8 +107,25 @@ export class PythonResultResolver implements ITestResultResolver { return Promise.resolve(); } - public resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise { - const rawTestExecData = payload; + public resolveExecution( + payload: ExecutionTestPayload | EOTTestPayload, + runInstance: TestRun, + deferredTillEOT: Deferred, + ): Promise { + if (payload !== undefined && 'eot' in payload) { + // the payload is an EOT payload, so resolve the deferred promise. + traceLog('ResultResolver EOT received for execution.'); + const eotPayload = payload as EOTTestPayload; + if (eotPayload.eot === true) { + deferredTillEOT.resolve(); + return Promise.resolve(); + } + } + return this._resolveExecution(payload as ExecutionTestPayload, runInstance); + } + + public _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise { + const rawTestExecData = payload as ExecutionTestPayload; if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { // Map which holds the subtest information for each test item. @@ -179,9 +216,8 @@ export class PythonResultResolver implements ITestResultResolver { }); } } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-failure') { - // split on " " since the subtest ID has the parent test ID in the first part of the ID. - const parentTestCaseId = keyTemp.split(' ')[0]; - const subtestId = keyTemp.split(' ')[1]; + // split on [] or () based on how the subtest is setup. + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); const data = rawTestExecData.result[keyTemp]; // find the subtest's parent test item @@ -190,7 +226,10 @@ export class PythonResultResolver implements ITestResultResolver { if (subtestStats) { subtestStats.failed += 1; } else { - this.subTestStats.set(parentTestCaseId, { failed: 1, passed: 0 }); + this.subTestStats.set(parentTestCaseId, { + failed: 1, + passed: 0, + }); runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); @@ -216,9 +255,8 @@ export class PythonResultResolver implements ITestResultResolver { throw new Error('Parent test item not found'); } } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { - // split on " " since the subtest ID has the parent test ID in the first part of the ID. - const parentTestCaseId = keyTemp.split(' ')[0]; - const subtestId = keyTemp.split(' ')[1]; + // split on [] or () based on how the subtest is setup. + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); // find the subtest's parent test item diff --git a/extensions/positron-python/src/client/testing/testController/common/server.ts b/extensions/positron-python/src/client/testing/testController/common/server.ts index 8797a861fb4a..46217eab0459 100644 --- a/extensions/positron-python/src/client/testing/testController/common/server.ts +++ b/extensions/positron-python/src/client/testing/testController/common/server.ts @@ -5,17 +5,23 @@ import * as net from 'net'; import * as crypto from 'crypto'; import { Disposable, Event, EventEmitter, TestRun } from 'vscode'; import * as path from 'path'; +import { ChildProcess } from 'child_process'; import { ExecutionFactoryCreateWithEnvironmentOptions, ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; -import { traceError, traceInfo, traceLog } from '../../../logging'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; -import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER } from './utils'; +import { + createDiscoveryErrorPayload, + createEOTPayload, + createExecutionErrorPayload, + extractJsonPayload, +} from './utils'; import { createDeferred } from '../../../common/utils/async'; export class PythonTestServer implements ITestServer, Disposable { @@ -35,56 +41,22 @@ export class PythonTestServer implements ITestServer, Disposable { this.server = net.createServer((socket: net.Socket) => { let buffer: Buffer = Buffer.alloc(0); // Buffer to accumulate received data socket.on('data', (data: Buffer) => { - try { - let rawData: string = data.toString(); - buffer = Buffer.concat([buffer, data]); - while (buffer.length > 0) { - const rpcHeaders = jsonRPCHeaders(buffer.toString()); - const uuid = rpcHeaders.headers.get(JSONRPC_UUID_HEADER); - const totalContentLength = rpcHeaders.headers.get('Content-Length'); - if (!uuid) { - traceError('On data received: Error occurred because payload UUID is undefined'); - this._onDataReceived.fire({ uuid: '', data: '' }); - return; - } - if (!this.uuids.includes(uuid)) { - traceError('On data received: Error occurred because the payload UUID is not recognized'); - this._onDataReceived.fire({ uuid: '', data: '' }); - return; - } - rawData = rpcHeaders.remainingRawData; - const rpcContent = jsonRPCContent(rpcHeaders.headers, rawData); - const extractedData = rpcContent.extractedJSON; - // do not send until we have the full content - if (extractedData.length === Number(totalContentLength)) { - // if the rawData includes tests then this is a discovery request - if (rawData.includes(`"tests":`)) { - this._onDiscoveryDataReceived.fire({ - uuid, - data: rpcContent.extractedJSON, - }); - // if the rawData includes result then this is a run request - } else if (rawData.includes(`"result":`)) { - this._onRunDataReceived.fire({ - uuid, - data: rpcContent.extractedJSON, - }); - } else { - traceLog( - `Error processing test server request: request is not recognized as discovery or run.`, - ); - this._onDataReceived.fire({ uuid: '', data: '' }); - return; - } - // this.uuids = this.uuids.filter((u) => u !== uuid); WHERE DOES THIS GO?? - buffer = Buffer.alloc(0); - } else { + buffer = Buffer.concat([buffer, data]); // get the new data and add it to the buffer + while (buffer.length > 0) { + try { + // try to resolve data, returned unresolved data + const remainingBuffer = this._resolveData(buffer); + if (remainingBuffer.length === buffer.length) { + // if the remaining buffer is exactly the same as the buffer before processing, + // then there is no more data to process so loop should be exited. break; } + buffer = remainingBuffer; + } catch (ex) { + traceError(`Error reading data from buffer: ${ex} observed.`); + buffer = Buffer.alloc(0); + this._onDataReceived.fire({ uuid: '', data: '' }); } - } catch (ex) { - traceError(`Error processing test server request: ${ex} observe`); - this._onDataReceived.fire({ uuid: '', data: '' }); } }); }); @@ -107,6 +79,47 @@ export class PythonTestServer implements ITestServer, Disposable { }); } + savedBuffer = ''; + + public _resolveData(buffer: Buffer): Buffer { + try { + const extractedJsonPayload = extractJsonPayload(buffer.toString(), this.uuids); + // what payload is so small it doesn't include the whole UUID think got this + if (extractedJsonPayload.uuid !== undefined && extractedJsonPayload.cleanedJsonData !== undefined) { + // if a full json was found in the buffer, fire the data received event then keep cycling with the remaining raw data. + traceVerbose(`Firing data received event, ${extractedJsonPayload.cleanedJsonData}`); + this._fireDataReceived(extractedJsonPayload.uuid, extractedJsonPayload.cleanedJsonData); + } + buffer = Buffer.from(extractedJsonPayload.remainingRawData); + if (buffer.length === 0) { + // if the buffer is empty, then there is no more data to process so buffer should be cleared. + buffer = Buffer.alloc(0); + } + } catch (ex) { + traceError(`Error attempting to resolve data: ${ex}`); + this._onDataReceived.fire({ uuid: '', data: '' }); + } + return buffer; + } + + private _fireDataReceived(uuid: string, extractedJSON: string): void { + if (extractedJSON.includes(`"tests":`) || extractedJSON.includes(`"command_type": "discovery"`)) { + this._onDiscoveryDataReceived.fire({ + uuid, + data: extractedJSON, + }); + // if the rawData includes result then this is a run request + } else if (extractedJSON.includes(`"result":`) || extractedJSON.includes(`"command_type": "execution"`)) { + this._onRunDataReceived.fire({ + uuid, + data: extractedJSON, + }); + } else { + traceError(`Error processing test server request: request is not recognized as discovery or run.`); + this._onDataReceived.fire({ uuid: '', data: '' }); + } + } + public serverReady(): Promise { return this.ready; } @@ -133,6 +146,14 @@ export class PythonTestServer implements ITestServer, Disposable { return this._onDiscoveryDataReceived.event; } + public triggerRunDataReceivedEvent(payload: DataReceivedEvent): void { + this._onRunDataReceived.fire(payload); + } + + public triggerDiscoveryDataReceivedEvent(payload: DataReceivedEvent): void { + this._onDiscoveryDataReceived.fire(payload); + } + public dispose(): void { this.server.close(); this._onDataReceived.dispose(); @@ -146,6 +167,7 @@ export class PythonTestServer implements ITestServer, Disposable { options: TestCommandOptions, runTestIdPort?: string, runInstance?: TestRun, + testIds?: string[], callback?: () => void, ): Promise { const { uuid } = options; @@ -157,7 +179,11 @@ export class PythonTestServer implements ITestServer, Disposable { cwd: options.cwd, throwOnStdErr: true, outputChannel: options.outChannel, - extraVariables: { PYTHONPATH: pythonPathCommand }, + extraVariables: { + PYTHONPATH: pythonPathCommand, + TEST_UUID: uuid.toString(), + TEST_PORT: this.getPort().toString(), + }, }; if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; @@ -169,12 +195,7 @@ export class PythonTestServer implements ITestServer, Disposable { }; const execService = await this.executionFactory.createActivatedEnvironment(creationOptions); - // Add the generated UUID to the data to be sent (expecting to receive it back). - // first check if we have testIds passed in (in case of execution) and - // insert appropriate flag and test id array - const args = [options.command.script, '--port', this.getPort().toString(), '--uuid', uuid].concat( - options.command.args, - ); + const args = [options.command.script].concat(options.command.args); if (options.outChannel) { options.outChannel.appendLine(`python ${args.join(' ')}`); @@ -202,14 +223,23 @@ export class PythonTestServer implements ITestServer, Disposable { // This means it is running discovery traceLog(`Discovering unittest tests with arguments: ${args}\r\n`); } - const deferred = createDeferred>(); + const deferredTillExecClose = createDeferred>(); - const result = execService.execObservable(args, spawnOptions); + let resultProc: ChildProcess | undefined; runInstance?.token.onCancellationRequested(() => { - result?.proc?.kill(); + traceInfo('Test run cancelled, killing unittest subprocess.'); + // 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(); + } }); + const result = execService?.execObservable(args, spawnOptions); + resultProc = result?.proc; + // 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) => { @@ -218,14 +248,49 @@ export class PythonTestServer implements ITestServer, Disposable { result?.proc?.stderr?.on('data', (data) => { spawnOptions?.outputChannel?.append(data.toString()); }); - result?.proc?.on('exit', () => { - traceLog('Exec server closed.', uuid); - deferred.resolve({ stdout: '', stderr: '' }); - callback?.(); + result?.proc?.on('exit', (code, signal) => { + if (code !== 0) { + traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}`); + } + }); + + result?.proc?.on('exit', (code, signal) => { + // if the child has testIds then this is a run request + if (code !== 0 && testIds && testIds?.length !== 0) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, + ); + // if the child process exited with a non-zero exit code, then we need to send the error payload. + this._onRunDataReceived.fire({ + uuid, + data: JSON.stringify(createExecutionErrorPayload(code, signal, testIds, options.cwd)), + }); + // then send a EOT payload + this._onRunDataReceived.fire({ + uuid, + data: JSON.stringify(createEOTPayload(true)), + }); + } else if (code !== 0) { + // This occurs when we are running discovery + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, + ); + this._onDiscoveryDataReceived.fire({ + uuid, + data: JSON.stringify(createDiscoveryErrorPayload(code, signal, options.cwd)), + }); + // then send a EOT payload + this._onDiscoveryDataReceived.fire({ + uuid, + data: JSON.stringify(createEOTPayload(true)), + }); + } + deferredTillExecClose.resolve({ stdout: '', stderr: '' }); }); - await deferred.promise; + await deferredTillExecClose.promise; } } catch (ex) { + traceError(`Error while server attempting to run unittest command: ${ex}`); this.uuids = this.uuids.filter((u) => u !== uuid); this._onDataReceived.fire({ uuid, diff --git a/extensions/positron-python/src/client/testing/testController/common/types.ts b/extensions/positron-python/src/client/testing/testController/common/types.ts index 16c0bd0e3cee..32e0c4ba8cc6 100644 --- a/extensions/positron-python/src/client/testing/testController/common/types.ts +++ b/extensions/positron-python/src/client/testing/testController/common/types.ts @@ -14,6 +14,7 @@ import { } from 'vscode'; import { ITestDebugLauncher, TestDiscoveryOptions } from '../../common/types'; import { IPythonExecutionFactory } from '../../../common/process/types'; +import { Deferred } from '../../../common/utils/async'; export type TestRunInstanceOptions = TestRunOptions & { exclude?: readonly TestItem[]; @@ -178,19 +179,32 @@ export interface ITestServer { options: TestCommandOptions, runTestIdsPort?: string, runInstance?: TestRun, + testIds?: string[], callback?: () => void, ): Promise; serverReady(): Promise; getPort(): number; createUUID(cwd: string): string; deleteUUID(uuid: string): void; + triggerRunDataReceivedEvent(data: DataReceivedEvent): void; + triggerDiscoveryDataReceivedEvent(data: DataReceivedEvent): void; } export interface ITestResultResolver { runIdToVSid: Map; runIdToTestItem: Map; vsIdToRunId: Map; - resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise; - resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise; + resolveDiscovery( + payload: DiscoveredTestPayload | EOTTestPayload, + deferredTillEOT: Deferred, + token?: CancellationToken, + ): Promise; + resolveExecution( + payload: ExecutionTestPayload | EOTTestPayload, + runInstance: TestRun, + deferredTillEOT: Deferred, + ): Promise; + _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise; + _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise; } export interface ITestDiscoveryAdapter { // ** first line old method signature, second line new method signature @@ -239,6 +253,11 @@ export type DiscoveredTestPayload = { error?: string[]; }; +export type EOTTestPayload = { + commandType: 'discovery' | 'execution'; + eot: boolean; +}; + export type ExecutionTestPayload = { cwd: string; status: 'success' | 'error'; diff --git a/extensions/positron-python/src/client/testing/testController/common/utils.ts b/extensions/positron-python/src/client/testing/testController/common/utils.ts index f98550d3e72b..f5f416529c42 100644 --- a/extensions/positron-python/src/client/testing/testController/common/utils.ts +++ b/extensions/positron-python/src/client/testing/testController/common/utils.ts @@ -9,27 +9,101 @@ import { EnableTestAdapterRewrite } from '../../../common/experiments/groups'; import { IExperimentService } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { DebugTestTag, ErrorTestItemOptions, RunTestTag } from './testItemUtilities'; -import { DiscoveredTestItem, DiscoveredTestNode, ITestResultResolver } from './types'; +import { + DiscoveredTestItem, + DiscoveredTestNode, + DiscoveredTestPayload, + EOTTestPayload, + ExecutionTestPayload, + ITestResultResolver, +} from './types'; +import { Deferred, createDeferred } from '../../../common/utils/async'; export function fixLogLines(content: string): string { const lines = content.split(/\r?\n/g); return `${lines.join('\r\n')}\r\n`; } -export interface IJSONRPCContent { +export interface IJSONRPCData { extractedJSON: string; remainingRawData: string; } -export interface IJSONRPCHeaders { +export interface ParsedRPCHeadersAndData { headers: Map; remainingRawData: string; } +export interface ExtractOutput { + uuid: string | undefined; + cleanedJsonData: string | undefined; + remainingRawData: string; +} + export const JSONRPC_UUID_HEADER = 'Request-uuid'; export const JSONRPC_CONTENT_LENGTH_HEADER = 'Content-Length'; export const JSONRPC_CONTENT_TYPE_HEADER = 'Content-Type'; -export function jsonRPCHeaders(rawData: string): IJSONRPCHeaders { +export function createTestingDeferred(): Deferred { + return createDeferred(); +} + +export function extractJsonPayload(rawData: string, uuids: Array): ExtractOutput { + /** + * Extracts JSON-RPC payload from the provided raw data. + * @param {string} rawData - The raw string data from which the JSON payload will be extracted. + * @param {Array} uuids - The list of UUIDs that are active. + * @returns {string} The remaining raw data after the JSON payload is extracted. + */ + + const rpcHeaders: ParsedRPCHeadersAndData = parseJsonRPCHeadersAndData(rawData); + + // verify the RPC has a UUID and that it is recognized + let uuid = rpcHeaders.headers.get(JSONRPC_UUID_HEADER); + uuid = checkUuid(uuid, uuids); + + const payloadLength = rpcHeaders.headers.get('Content-Length'); + + // separate out the data within context length of the given payload from the remaining data in the buffer + const rpcContent: IJSONRPCData = ExtractJsonRPCData(payloadLength, rpcHeaders.remainingRawData); + const cleanedJsonData = rpcContent.extractedJSON; + const { remainingRawData } = rpcContent; + + // if the given payload has the complete json, process it otherwise wait for the rest in the buffer + if (cleanedJsonData.length === Number(payloadLength)) { + // call to process this data + // remove this data from the buffer + return { uuid, cleanedJsonData, remainingRawData }; + } + // wait for the remaining + return { uuid: undefined, cleanedJsonData: undefined, remainingRawData: rawData }; +} + +export function checkUuid(uuid: string | undefined, uuids: Array): string | undefined { + if (!uuid) { + // no UUID found, this could occurred if the payload is full yet so send back without erroring + return undefined; + } + if (!uuids.includes(uuid)) { + // no UUID found, this could occurred if the payload is full yet so send back without erroring + throw new Error('On data received: Error occurred because the payload UUID is not recognized'); + } + return uuid; +} + +export function parseJsonRPCHeadersAndData(rawData: string): ParsedRPCHeadersAndData { + /** + * Parses the provided raw data to extract JSON-RPC specific headers and remaining data. + * + * This function aims to extract specific JSON-RPC headers (like UUID, content length, + * and content type) from the provided raw string data. Headers are expected to be + * delimited by newlines and the format should be "key:value". The function stops parsing + * once it encounters an empty line, and the rest of the data after this line is treated + * as the remaining raw data. + * + * @param {string} rawData - The raw string containing headers and possibly other data. + * @returns {ParsedRPCHeadersAndData} An object containing the parsed headers as a map and the + * remaining raw data after the headers. + */ const lines = rawData.split('\n'); let remainingRawData = ''; const headerMap = new Map(); @@ -40,8 +114,10 @@ export function jsonRPCHeaders(rawData: string): IJSONRPCHeaders { break; } const [key, value] = line.split(':'); - if ([JSONRPC_UUID_HEADER, JSONRPC_CONTENT_LENGTH_HEADER, JSONRPC_CONTENT_TYPE_HEADER].includes(key)) { - headerMap.set(key.trim(), value.trim()); + if (value && value.trim()) { + if ([JSONRPC_UUID_HEADER, JSONRPC_CONTENT_LENGTH_HEADER, JSONRPC_CONTENT_TYPE_HEADER].includes(key)) { + headerMap.set(key.trim(), value.trim()); + } } } @@ -51,8 +127,21 @@ export function jsonRPCHeaders(rawData: string): IJSONRPCHeaders { }; } -export function jsonRPCContent(headers: Map, rawData: string): IJSONRPCContent { - const length = parseInt(headers.get('Content-Length') ?? '0', 10); +export function ExtractJsonRPCData(payloadLength: string | undefined, rawData: string): IJSONRPCData { + /** + * Extracts JSON-RPC content based on provided headers and raw data. + * + * This function uses the `Content-Length` header from the provided headers map + * to determine how much of the rawData string represents the actual JSON content. + * After extracting the expected content, it also returns any remaining data + * that comes after the extracted content as remaining raw data. + * + * @param {string | undefined} payloadLength - The value of the `Content-Length` header. + * @param {string} rawData - The raw string data from which the JSON content will be extracted. + * + * @returns {IJSONRPCContent} An object containing the extracted JSON content and any remaining raw data. + */ + const length = parseInt(payloadLength ?? '0', 10); const data = rawData.slice(0, length); const remainingRawData = rawData.slice(length); return { @@ -124,7 +213,7 @@ export async function startTestIdServer(testIds: string[]): Promise { } export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { - const labelText = testType === 'pytest' ? 'Pytest Discovery Error' : 'Unittest Discovery Error'; + const labelText = testType === 'pytest' ? 'pytest Discovery Error' : 'Unittest Discovery Error'; return { id: `DiscoveryError:${uri.fsPath}`, label: `${labelText} [${path.basename(uri.fsPath)}]`, @@ -188,3 +277,65 @@ export function populateTestTree( 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.', + 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}`, + ], + }; +} + +export function createEOTPayload(executionBool: boolean): EOTTestPayload { + return { + commandType: executionBool ? 'execution' : 'discovery', + eot: true, + } as EOTTestPayload; +} + +/** + * 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]; +} diff --git a/extensions/positron-python/src/client/testing/testController/controller.ts b/extensions/positron-python/src/client/testing/testController/controller.ts index 1550323ff8f8..af77ab2b2525 100644 --- a/extensions/positron-python/src/client/testing/testController/controller.ts +++ b/extensions/positron-python/src/client/testing/testController/controller.ts @@ -371,6 +371,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc ); const dispose = token.onCancellationRequested(() => { + runInstance.appendOutput(`Run instance cancelled.\r\n`); runInstance.end(); }); @@ -461,7 +462,6 @@ export class PythonTestController implements ITestController, IExtensionSingleAc ); } } - if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { unconfiguredWorkspaces.push(workspace); } diff --git a/extensions/positron-python/src/client/testing/testController/pytest/pytestController.ts b/extensions/positron-python/src/client/testing/testController/pytest/pytestController.ts index 997e3e29b7ec..d23cac842cda 100644 --- a/extensions/positron-python/src/client/testing/testController/pytest/pytestController.ts +++ b/extensions/positron-python/src/client/testing/testController/pytest/pytestController.ts @@ -235,7 +235,7 @@ export class PytestController implements ITestFrameworkController { testController.items.add( createErrorTestItem(testController, { id: `DiscoveryError:${workspace.uri.fsPath}`, - label: `Pytest Discovery Error [${path.basename(workspace.uri.fsPath)}]`, + label: `pytest Discovery Error [${path.basename(workspace.uri.fsPath)}]`, error: util.format( `${cancel} discovering pytest tests (see Output > Python):\r\n`, message.length > 0 ? message : ex, diff --git a/extensions/positron-python/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/extensions/positron-python/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 450e2ef1edf2..c0e1a310ee4a 100644 --- a/extensions/positron-python/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/extensions/positron-python/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -4,14 +4,13 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { ExecutionFactoryCreateWithEnvironmentOptions, - ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred } from '../../../common/utils/async'; +import { Deferred, createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { traceVerbose } from '../../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; import { DataReceivedEvent, DiscoveredTestPayload, @@ -19,6 +18,7 @@ import { ITestResultResolver, ITestServer, } from '../common/types'; +import { createDiscoveryErrorPayload, createEOTPayload, createTestingDeferred } from '../common/utils'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied @@ -32,20 +32,21 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { ) {} async discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { - const settings = this.configSettings.getSettings(uri); const uuid = this.testServer.createUUID(uri.fsPath); - const { pytestArgs } = settings.testing; - traceVerbose(pytestArgs); - const dataReceivedDisposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { - this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); + const deferredTillEOT: Deferred = createDeferred(); + const dataReceivedDisposable = this.testServer.onDiscoveryDataReceived(async (e: DataReceivedEvent) => { + this.resultResolver?.resolveDiscovery(JSON.parse(e.data), deferredTillEOT); }); const disposeDataReceiver = function (testServer: ITestServer) { + traceInfo(`Disposing data receiver for ${uri.fsPath} and deleting UUID; pytest discovery.`); testServer.deleteUUID(uuid); dataReceivedDisposable.dispose(); }; try { await this.runPytestDiscovery(uri, uuid, executionFactory); } finally { + await deferredTillEOT.promise; + traceVerbose('deferredTill EOT resolved'); disposeDataReceiver(this.testServer); } // this is only a placeholder to handle function overloading until rewrite is finished @@ -54,7 +55,6 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { } async runPytestDiscovery(uri: Uri, uuid: string, executionFactory?: IPythonExecutionFactory): Promise { - const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); const settings = this.configSettings.getSettings(uri); @@ -82,8 +82,10 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; const execService = await executionFactory?.createActivatedEnvironment(creationOptions); // delete UUID following entire discovery finishing. - const deferredExec = createDeferred>(); const execArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); + traceVerbose(`Running pytest discovery with command: ${execArgs.join(' ')}`); + + const deferredTillExecClose: Deferred = createTestingDeferred(); const result = execService?.execObservable(execArgs, spawnOptions); // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. @@ -94,11 +96,31 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { result?.proc?.stderr?.on('data', (data) => { spawnOptions.outputChannel?.append(data.toString()); }); - result?.proc?.on('exit', () => { - deferredExec.resolve({ stdout: '', stderr: '' }); - deferred.resolve(); + result?.proc?.on('exit', (code, signal) => { + if (code !== 0) { + traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}.`); + } }); - - await deferredExec.promise; + result?.proc?.on('close', (code, signal) => { + if (code !== 0) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, + ); + // if the child process exited with a non-zero exit code, then we need to send the error payload. + this.testServer.triggerDiscoveryDataReceivedEvent({ + uuid, + data: JSON.stringify(createDiscoveryErrorPayload(code, signal, cwd)), + }); + // then send a EOT payload + this.testServer.triggerDiscoveryDataReceivedEvent({ + uuid, + data: JSON.stringify(createEOTPayload(true)), + }); + } + // 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(); + }); + await deferredTillExecClose.promise; } } diff --git a/extensions/positron-python/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/extensions/positron-python/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 96d53db22c1c..8020be17cf90 100644 --- a/extensions/positron-python/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/extensions/positron-python/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -3,9 +3,10 @@ import { TestRun, Uri } from 'vscode'; import * as path from 'path'; +import { ChildProcess } from 'child_process'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred } from '../../../common/utils/async'; -import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; +import { Deferred } from '../../../common/utils/async'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, @@ -15,7 +16,6 @@ import { } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, - ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -42,29 +42,43 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { debugLauncher?: ITestDebugLauncher, ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); - traceVerbose(uri, testIds, debugBool); + // deferredTillEOT is resolved when all data sent over payload is received + const deferredTillEOT: Deferred = utils.createTestingDeferred(); + const dataReceivedDisposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { - this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); + const eParsed = JSON.parse(e.data); + this.resultResolver?.resolveExecution(eParsed, runInstance, deferredTillEOT); + } else { + traceError('No run instance found, cannot resolve execution.'); } }); const disposeDataReceiver = function (testServer: ITestServer) { + traceInfo(`Disposing data receiver for ${uri.fsPath} and deleting UUID; pytest execution.`); testServer.deleteUUID(uuid); dataReceivedDisposable.dispose(); }; runInstance?.token.onCancellationRequested(() => { - disposeDataReceiver(this.testServer); + traceInfo("Test run cancelled, resolving 'till EOT' deferred."); + deferredTillEOT.resolve(); }); - await this.runTestsNew( - uri, - testIds, - uuid, - runInstance, - debugBool, - executionFactory, - debugLauncher, - disposeDataReceiver, - ); + + try { + await this.runTestsNew( + uri, + testIds, + uuid, + runInstance, + debugBool, + executionFactory, + debugLauncher, + deferredTillEOT, + ); + } finally { + await deferredTillEOT.promise; + traceVerbose('deferredTill EOT resolved'); + disposeDataReceiver(this.testServer); + } // placeholder until after the rewrite is adopted // TODO: remove after adoption. @@ -84,19 +98,16 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { debugBool?: boolean, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, - disposeDataReceiver?: (testServer: ITestServer) => void, + deferredTillEOT?: Deferred, ): Promise { - const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); - this.configSettings.isTestExecution(); 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; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); - const spawnOptions: SpawnOptions = { cwd, throwOnStdErr: true, @@ -116,7 +127,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { }; // need to check what will happen in the exec service is NOT defined and is null const execService = await executionFactory?.createActivatedEnvironment(creationOptions); - try { // Remove positional test folders and files, we will add as needed per node const testArgs = removePositionalFoldersAndFiles(pytestArgs); @@ -130,7 +140,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { if (debugBool && !testArgs.some((a) => a.startsWith('--capture') || a === '-s')) { testArgs.push('--capture', 'no'); } - traceLog(`Running PYTEST execution for the following test ids: ${testIds}`); const pytestRunTestIdsPort = await utils.startTestIdServer(testIds); if (spawnOptions.extraVariables) @@ -150,22 +159,31 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { }; traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions, () => { - deferred.resolve(); - this.testServer.deleteUUID(uuid); + deferredTillEOT?.resolve(); }); } 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 pytests with arguments: ${runArgs.join(' ')}\r\n`); + traceInfo(`Running pytest with arguments: ${runArgs.join(' ')}\r\n`); - const deferredExec = createDeferred>(); - const result = execService?.execObservable(runArgs, spawnOptions); + let resultProc: ChildProcess | undefined; runInstance?.token.onCancellationRequested(() => { - result?.proc?.kill(); + traceInfo('Test run cancelled, killing pytest subprocess.'); + // 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(); + } }); + const result = execService?.execObservable(runArgs, spawnOptions); + resultProc = result?.proc; + // 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) => { @@ -174,20 +192,46 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { result?.proc?.stderr?.on('data', (data) => { this.outputChannel?.append(data.toString()); }); + result?.proc?.on('exit', (code, signal) => { + if (code !== 0 && testIds) { + traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}.`); + } + }); - result?.proc?.on('exit', () => { - deferredExec.resolve({ stdout: '', stderr: '' }); - deferred.resolve(); - disposeDataReceiver?.(this.testServer); + 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 && testIds) { + traceError( + `Subprocess closed unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, + ); + this.testServer.triggerRunDataReceivedEvent({ + uuid, + data: JSON.stringify(utils.createExecutionErrorPayload(code, signal, testIds, cwd)), + }); + // then send a EOT payload + this.testServer.triggerRunDataReceivedEvent({ + uuid, + data: JSON.stringify(utils.createEOTPayload(true)), + }); + } + // 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(); }); - await deferredExec.promise; + await deferredTillExecClose?.promise; } } catch (ex) { traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); return Promise.reject(ex); } - const executionPayload: ExecutionTestPayload = { cwd, status: 'success', error: '' }; + const executionPayload: ExecutionTestPayload = { + cwd, + status: 'success', + error: '', + }; return executionPayload; } } diff --git a/extensions/positron-python/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/extensions/positron-python/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 1cbad7ef65ef..440df4f94dc6 100644 --- a/extensions/positron-python/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/extensions/positron-python/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -14,6 +14,7 @@ import { TestCommandOptions, TestDiscoveryCommand, } from '../common/types'; +import { Deferred, createDeferred } from '../../../common/utils/async'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. @@ -34,7 +35,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const command = buildDiscoveryCommand(unittestArgs); const uuid = this.testServer.createUUID(uri.fsPath); - + const deferredTillEOT: Deferred = createDeferred(); const options: TestCommandOptions = { workspaceFolder: uri, command, @@ -44,7 +45,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; const dataReceivedDisposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { - this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); + this.resultResolver?.resolveDiscovery(JSON.parse(e.data), deferredTillEOT); }); const disposeDataReceiver = function (testServer: ITestServer) { testServer.deleteUUID(uuid); @@ -52,8 +53,10 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; await this.callSendCommand(options, () => { - disposeDataReceiver(this.testServer); + disposeDataReceiver?.(this.testServer); }); + await deferredTillEOT.promise; + disposeDataReceiver(this.testServer); // placeholder until after the rewrite is adopted // TODO: remove after adoption. const discoveryPayload: DiscoveredTestPayload = { @@ -64,7 +67,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { } private async callSendCommand(options: TestCommandOptions, callback: () => void): Promise { - await this.testServer.sendCommand(options, undefined, undefined, callback); + await this.testServer.sendCommand(options, undefined, undefined, [], callback); const discoveryPayload: DiscoveredTestPayload = { cwd: '', status: 'success' }; return discoveryPayload; } diff --git a/extensions/positron-python/src/client/testing/testController/unittest/testExecutionAdapter.ts b/extensions/positron-python/src/client/testing/testController/unittest/testExecutionAdapter.ts index 9af9e593c246..9da0872ef601 100644 --- a/extensions/positron-python/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/extensions/positron-python/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { TestRun, Uri } from 'vscode'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred } from '../../../common/utils/async'; +import { Deferred, createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { DataReceivedEvent, @@ -15,7 +15,7 @@ import { TestCommandOptions, TestExecutionCommand, } from '../common/types'; -import { traceLog } from '../../../logging'; +import { traceError, traceInfo, traceLog } from '../../../logging'; import { startTestIdServer } from '../common/utils'; /** @@ -37,19 +37,30 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runInstance?: TestRun, ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); + const deferredTillEOT: Deferred = createDeferred(); const disposedDataReceived = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { - this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); + this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance, deferredTillEOT); + } else { + traceError('No run instance found, cannot resolve execution.'); } }); const disposeDataReceiver = function (testServer: ITestServer) { + traceInfo(`Disposing data receiver for ${uri.fsPath} and deleting UUID; unittest execution.`); testServer.deleteUUID(uuid); disposedDataReceived.dispose(); }; runInstance?.token.onCancellationRequested(() => { - disposeDataReceiver(this.testServer); + traceInfo("Test run cancelled, resolving 'till EOT' deferred."); + deferredTillEOT.resolve(); }); - await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, disposeDataReceiver); + try { + await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, deferredTillEOT); + await deferredTillEOT.promise; + disposeDataReceiver(this.testServer); + } catch (error) { + traceError(`Error in running unittest tests: ${error}`); + } const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; return executionPayload; } @@ -60,7 +71,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { uuid: string, runInstance?: TestRun, debugBool?: boolean, - disposeDataReceiver?: (testServer: ITestServer) => void, + deferredTillEOT?: Deferred, ): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; @@ -77,15 +88,12 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { testIds, outChannel: this.outputChannel, }; - - const deferred = createDeferred(); traceLog(`Running UNITTEST execution for the following test ids: ${testIds}`); const runTestIdsPort = await startTestIdServer(testIds); - await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, () => { - deferred.resolve(); - disposeDataReceiver?.(this.testServer); + await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, testIds, () => { + deferredTillEOT?.resolve(); }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. diff --git a/extensions/positron-python/src/test/common/installer.test.ts b/extensions/positron-python/src/test/common/installer.test.ts index 7ff0ee81c27f..5c1842a2c97c 100644 --- a/extensions/positron-python/src/test/common/installer.test.ts +++ b/extensions/positron-python/src/test/common/installer.test.ts @@ -87,7 +87,6 @@ import { ProductType, } from '../../client/common/types'; import { createDeferred } from '../../client/common/utils/async'; -import { getNamesAndValues } from '../../client/common/utils/enum'; import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; import { Random } from '../../client/common/utils/random'; import { ImportTracker } from '../../client/telemetry/importTracker'; @@ -105,6 +104,7 @@ import { } from '../../client/interpreter/configuration/types'; import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; +import { getProductsForInstallerTests } from './productsToTest'; suite('Installer', () => { let ioc: UnitTestIocContainer; @@ -276,7 +276,8 @@ suite('Installer', () => { await installer.isInstalled(product, resource); await checkInstalledDef.promise; } - getNamesAndValues(Product).forEach((prod) => { + + getProductsForInstallerTests().forEach((prod) => { test(`Ensure isInstalled for Product: '${prod.name}' executes the right command`, async function () { if ( new ProductService().getProductType(prod.value) === ProductType.DataScience || @@ -293,7 +294,7 @@ suite('Installer', () => { new MockModuleInstaller('two', true), ); ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.unittest || prod.value === Product.isort) { + if (prod.value === Product.unittest) { return undefined; } await testCheckingIfProductIsInstalled(prod.value); @@ -316,7 +317,8 @@ suite('Installer', () => { await installer.install(product); await checkInstalledDef.promise; } - getNamesAndValues(Product).forEach((prod) => { + + getProductsForInstallerTests().forEach((prod) => { test(`Ensure install for Product: '${prod.name}' executes the right command in IModuleInstaller`, async function () { const productType = new ProductService().getProductType(prod.value); if (productType === ProductType.DataScience || productType === ProductType.Python) { @@ -331,7 +333,7 @@ suite('Installer', () => { new MockModuleInstaller('two', true), ); ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.unittest || prod.value === Product.isort) { + if (prod.value === Product.unittest) { return undefined; } await testInstallingProduct(prod.value); diff --git a/extensions/positron-python/src/test/common/installer/installer.invalidPath.unit.test.ts b/extensions/positron-python/src/test/common/installer/installer.invalidPath.unit.test.ts index 7e8392204600..b6738759f0d7 100644 --- a/extensions/positron-python/src/test/common/installer/installer.invalidPath.unit.test.ts +++ b/extensions/positron-python/src/test/common/installer/installer.invalidPath.unit.test.ts @@ -14,10 +14,10 @@ import { ProductInstaller } from '../../../client/common/installer/productInstal import { ProductService } from '../../../client/common/installer/productService'; import { IProductPathService, IProductService } from '../../../client/common/installer/types'; import { IPersistentState, IPersistentStateFactory, Product, ProductType } 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'; +import { getProductsForInstallerTests } from '../productsToTest'; use(chaiAsPromised); @@ -26,7 +26,7 @@ suite('Module Installer - Invalid Paths', () => { ['moduleName', path.join('users', 'dev', 'tool', 'executable')].forEach((pathToExecutable) => { const isExecutableAModule = path.basename(pathToExecutable) === pathToExecutable; - getNamesAndValues(Product).forEach((product) => { + getProductsForInstallerTests().forEach((product) => { let installer: ProductInstaller; let serviceContainer: TypeMoq.IMock; let app: TypeMoq.IMock; @@ -78,7 +78,6 @@ suite('Module Installer - Invalid Paths', () => { }); switch (product.value) { - case Product.isort: case Product.unittest: { return; } diff --git a/extensions/positron-python/src/test/common/installer/installer.unit.test.ts b/extensions/positron-python/src/test/common/installer/installer.unit.test.ts index 38b9d9174472..69a5f3678f69 100644 --- a/extensions/positron-python/src/test/common/installer/installer.unit.test.ts +++ b/extensions/positron-python/src/test/common/installer/installer.unit.test.ts @@ -6,24 +6,11 @@ import { assert, expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as sinon from 'sinon'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Disposable, Uri, WorkspaceFolder } from 'vscode'; -import { ApplicationShell } from '../../../client/common/application/applicationShell'; -import { CommandManager } from '../../../client/common/application/commandManager'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../client/common/application/types'; -import { WorkspaceService } from '../../../client/common/application/workspace'; -import { ConfigurationService } from '../../../client/common/configuration/service'; -import { Commands } from '../../../client/common/constants'; -import { ExperimentService } from '../../../client/common/experiments/service'; +import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; import '../../../client/common/extensions'; -import { - FormatterInstaller, - LinterInstaller, - ProductInstaller, -} from '../../../client/common/installer/productInstaller'; -import { ProductNames } from '../../../client/common/installer/productNames'; -import { LinterProductPathService } from '../../../client/common/installer/productPath'; +import { ProductInstaller } from '../../../client/common/installer/productInstaller'; import { ProductService } from '../../../client/common/installer/productService'; import { IInstallationChannelManager, @@ -39,9 +26,7 @@ import { IPythonExecutionService, } from '../../../client/common/process/types'; import { - IConfigurationService, IDisposableRegistry, - IExperimentService, InstallerResponse, IPersistentState, IPersistentStateFactory, @@ -49,20 +34,17 @@ import { ProductType, } from '../../../client/common/types'; import { createDeferred, Deferred } from '../../../client/common/utils/async'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { ServiceContainer } from '../../../client/ioc/container'; import { IServiceContainer } from '../../../client/ioc/types'; -import { LinterManager } from '../../../client/linters/linterManager'; -import { ILinterManager } from '../../../client/linters/types'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; import { sleep } from '../../common'; +import { getProductsForInstallerTests } from '../productsToTest'; use(chaiAsPromised); suite('Module Installer only', () => { [undefined, Uri.file('resource')].forEach((resource) => { - getNamesAndValues(Product) + getProductsForInstallerTests() .concat([{ name: 'Unknown product', value: 404 }]) .forEach((product) => { @@ -183,9 +165,6 @@ suite('Module Installer only', () => { }); return; } - case Product.isort: { - return; - } case Product.unittest: { test(`Ensure resource info is passed into the module installer ${product.name} (${ resource ? 'With a resource' : 'without a resource' @@ -638,273 +617,5 @@ suite('Module Installer only', () => { workspaceService.verifyAll(); }); }); - - suite('Test FormatterInstaller.promptToInstallImplementation', () => { - class FormatterInstallerTest extends FormatterInstaller { - public async promptToInstallImplementation(product: Product, uri?: Uri): Promise { - return super.promptToInstallImplementation(product, uri); - } - - // eslint-disable-next-line class-methods-use-this - protected getStoredResponse(_key: string) { - return false; - } - - // eslint-disable-next-line class-methods-use-this - protected isExecutableAModule(_product: Product, _resource?: Uri) { - return true; - } - } - let installer: FormatterInstallerTest; - let appShell: IApplicationShell; - let configService: IConfigurationService; - let workspaceService: IWorkspaceService; - let productService: IProductService; - let cmdManager: ICommandManager; - setup(() => { - const serviceContainer = mock(ServiceContainer); - appShell = mock(ApplicationShell); - configService = mock(ConfigurationService); - workspaceService = mock(WorkspaceService); - productService = mock(ProductService); - cmdManager = mock(CommandManager); - - when(serviceContainer.get(IApplicationShell)).thenReturn(instance(appShell)); - when(serviceContainer.get(IConfigurationService)).thenReturn( - instance(configService), - ); - when(serviceContainer.get(IWorkspaceService)).thenReturn(instance(workspaceService)); - when(serviceContainer.get(IProductService)).thenReturn(instance(productService)); - when(serviceContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - - installer = new FormatterInstallerTest(instance(serviceContainer)); - }); - - teardown(() => { - sinon.restore(); - }); - - test('If nothing is selected, return Ignore as response', async () => { - const product = Product.autopep8; - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn((undefined as unknown) as Thenable); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Ignore); - }); - - test('If `Yes` is selected, install product', async () => { - const product = Product.autopep8; - const install = sinon.stub(FormatterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn(('Yes' as unknown) as Thenable); - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Installed); - assert.ok(install.calledOnceWith(product, resource, undefined)); - }); - - test('If `Use black` is selected, install black formatter', async () => { - const product = Product.autopep8; - const install = sinon.stub(FormatterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn(('Use black' as unknown) as Thenable); - when(configService.updateSetting('formatting.provider', 'black', resource)).thenResolve(); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Installed); - verify(configService.updateSetting('formatting.provider', 'black', resource)).once(); - assert.ok(install.calledOnceWith(Product.black, resource, undefined)); - }); - - test('If `Use yapf` is selected, install black formatter', async () => { - const product = Product.autopep8; - const install = sinon.stub(FormatterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn(('Use yapf' as unknown) as Thenable); - when(configService.updateSetting('formatting.provider', 'yapf', resource)).thenResolve(); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Installed); - verify(configService.updateSetting('formatting.provider', 'yapf', resource)).once(); - assert.ok(install.calledOnceWith(Product.yapf, resource, undefined)); - }); - }); - }); -}); - -[undefined, Uri.file('resource')].forEach((resource) => { - suite(`Test LinterInstaller with resource: ${resource}`, () => { - class LinterInstallerTest extends LinterInstaller { - public isModuleExecutable = true; - - public async promptToInstallImplementation(product: Product, uri?: Uri): Promise { - return super.promptToInstallImplementation(product, uri); - } - - // eslint-disable-next-line class-methods-use-this - protected getStoredResponse(_key: string) { - return false; - } - - protected isExecutableAModule(_product: Product, _resource?: Uri) { - return this.isModuleExecutable; - } - } - - let installer: LinterInstallerTest; - let appShell: IApplicationShell; - let configService: IConfigurationService; - let workspaceService: IWorkspaceService; - let productService: IProductService; - let cmdManager: ICommandManager; - let experimentsService: IExperimentService; - let linterManager: ILinterManager; - let serviceContainer: IServiceContainer; - let productPathService: IProductPathService; - setup(() => { - serviceContainer = mock(ServiceContainer); - appShell = mock(ApplicationShell); - configService = mock(ConfigurationService); - workspaceService = mock(WorkspaceService); - productService = mock(ProductService); - cmdManager = mock(CommandManager); - experimentsService = mock(ExperimentService); - linterManager = mock(LinterManager); - productPathService = mock(LinterProductPathService); - - when(serviceContainer.get(IApplicationShell)).thenReturn(instance(appShell)); - when(serviceContainer.get(IConfigurationService)).thenReturn( - instance(configService), - ); - when(serviceContainer.get(IWorkspaceService)).thenReturn(instance(workspaceService)); - when(serviceContainer.get(IProductService)).thenReturn(instance(productService)); - when(serviceContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - - const exp = instance(experimentsService); - when(serviceContainer.get(IExperimentService)).thenReturn(exp); - when(experimentsService.inExperiment(anything())).thenResolve(false); - - when(serviceContainer.get(ILinterManager)).thenReturn(instance(linterManager)); - when(serviceContainer.get(IProductPathService, ProductType.Linter)).thenReturn( - instance(productPathService), - ); - - installer = new LinterInstallerTest(instance(serviceContainer)); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Ensure 3 options for pylint', async () => { - const product = Product.pylint; - const options = ['Select Linter', "Don't 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', "Don't show again"]; - const productName = ProductNames.get(product)!; - when( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).thenResolve(('Select Linter' as unknown) as void); - when(cmdManager.executeCommand(Commands.Set_Linter)).thenResolve(undefined); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).once(); - verify(cmdManager.executeCommand(Commands.Set_Linter)).once(); - expect(response).to.be.equal(InstallerResponse.Ignore); - }); - test('If install button is selected, install linter and return response', async () => { - const product = Product.pylint; - const options = ['Select Linter', "Don't show again"]; - const productName = ProductNames.get(product)!; - when( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).thenResolve(('Install' as unknown) as void); - when(cmdManager.executeCommand(Commands.Set_Linter)).thenResolve(undefined); - const install = sinon.stub(LinterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - const response = await installer.promptToInstallImplementation(product, resource); - - expect(response).to.be.equal(InstallerResponse.Installed); - assert.ok(install.calledOnceWith(product, resource, undefined)); - }); }); }); diff --git a/extensions/positron-python/src/test/common/installer/productInstaller.unit.test.ts b/extensions/positron-python/src/test/common/installer/productInstaller.unit.test.ts index a63ce23aa438..7e21a241ff31 100644 --- a/extensions/positron-python/src/test/common/installer/productInstaller.unit.test.ts +++ b/extensions/positron-python/src/test/common/installer/productInstaller.unit.test.ts @@ -3,26 +3,15 @@ 'use strict'; -import * as assert from 'assert'; import { expect } from 'chai'; -import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; import { IApplicationShell } from '../../../client/common/application/types'; -import { DataScienceInstaller, FormatterInstaller } from '../../../client/common/installer/productInstaller'; -import { ProductNames } from '../../../client/common/installer/productNames'; -import { - IInstallationChannelManager, - IModuleInstaller, - InterpreterUri, - IProductPathService, - IProductService, -} from '../../../client/common/installer/types'; -import { InstallerResponse, IPersistentStateFactory, Product, ProductType } from '../../../client/common/types'; -import { Common } from '../../../client/common/utils/localize'; +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'; -import { MockMemento } from '../../mocks/mementos'; class AlwaysInstalledDataScienceInstaller extends DataScienceInstaller { // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this @@ -213,250 +202,3 @@ suite('DataScienceInstaller install', async () => { }); }); - -suite('Formatter installer', async () => { - let serviceContainer: TypeMoq.IMock; - // let outputChannel: TypeMoq.IMock; - let appShell: TypeMoq.IMock; - let persistentStateFactory: TypeMoq.IMock; - let productPathService: TypeMoq.IMock; - // let isExecutableAsModuleStub: sinon.SinonStub; - - // constructor(protected serviceContainer: IServiceContainer, protected outputChannel: OutputChannel) { - // this.appShell = serviceContainer.get(IApplicationShell); - // this.configService = serviceContainer.get(IConfigurationService); - // this.workspaceService = serviceContainer.get(IWorkspaceService); - // this.productService = serviceContainer.get(IProductService); - // this.persistentStateFactory = serviceContainer.get(IPersistentStateFactory); - // } - - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - // outputChannel = TypeMoq.Mock.ofType(); - appShell = TypeMoq.Mock.ofType(); - persistentStateFactory = TypeMoq.Mock.ofType(); - productPathService = TypeMoq.Mock.ofType(); - - const installStub = sinon.stub(FormatterInstaller.prototype, 'install'); - installStub.returns(Promise.resolve(InstallerResponse.Installed)); - - const productService = TypeMoq.Mock.ofType(); - productService.setup((p) => p.getProductType(TypeMoq.It.isAny())).returns(() => ProductType.Formatter); - - 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(IProductService))).returns(() => productService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductPathService), ProductType.Formatter)) - .returns(() => productPathService.object); - }); - - teardown(() => { - sinon.restore(); - }); - - // - if black not installed, offer autopep8 and yapf options - // - if autopep8 not installed, offer black and yapf options - // - if yapf not installed, offer black and autopep8 options - // - if not executable as a module, display error message - // - if never show again was set to true earlier, ignore - // if never show again is selected, ignore - - test('If black is not installed, offer autopep8 and yapf as options', async () => { - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Installed); - }); - - test('If autopep8 is not installed, offer black and yapf as options', async () => { - const messageOptions = [ - Common.bannerLabelYes, - - 'Use {0}'.format(ProductNames.get(Product.black)!), - 'Use {0}'.format(ProductNames.get(Product.yapf)!), - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.autopep8); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Installed); - }); - - test('If yapf is not installed, offer autopep8 and black as options', async () => { - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.black)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.yapf); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Installed); - }); - - test('If the formatter is not executable as a module, display an error message', async () => { - const messageOptions = [ - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => false) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.getExecutableNameFromSettings(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => 'foo'); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - productPathService.verifyAll(); - }); - - test('If "Do not show again" has been selected earlier, do not display the prompt', async () => { - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.never()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: true, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - assert.strictEqual(result, InstallerResponse.Ignore); - }); - - test('If "Do not show again" is selected, do not install the formatter and do not show the prompt again', async () => { - let value = false; - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.doNotShowAgain)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value, - updateValue: (newValue) => { - value = newValue; - return Promise.resolve(); - }, - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.black); - const resultTwo = await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Ignore); - assert.strictEqual(resultTwo, InstallerResponse.Ignore); - }); -}); diff --git a/extensions/positron-python/src/test/common/installer/productPath.unit.test.ts b/extensions/positron-python/src/test/common/installer/productPath.unit.test.ts index 1e64ca63e117..0f627289da70 100644 --- a/extensions/positron-python/src/test/common/installer/productPath.unit.test.ts +++ b/extensions/positron-python/src/test/common/installer/productPath.unit.test.ts @@ -26,18 +26,18 @@ import { 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'; import { ITestingSettings } from '../../../client/testing/configuration/types'; +import { getProductsForInstallerTests } from '../productsToTest'; use(chaiAsPromised); suite('Product Path', () => { [undefined, Uri.file('resource')].forEach((resource) => { - getNamesAndValues(Product).forEach((product) => { + getProductsForInstallerTests().forEach((product) => { class TestBaseProductPathsService extends BaseProductPathsService { public getExecutableNameFromSettings(_: Product, _resource?: Uri): string { return ''; @@ -75,9 +75,6 @@ suite('Product Path', () => { .returns(() => new ProductService()); }); - if (product.value === Product.isort) { - return; - } suite('Method isExecutableAModule()', () => { test('Returns true if User has customized the executable name', () => { productInstaller.translateProductToModuleName = () => 'moduleName'; diff --git a/extensions/positron-python/src/test/common/productsToTest.ts b/extensions/positron-python/src/test/common/productsToTest.ts new file mode 100644 index 000000000000..7fc06863f67c --- /dev/null +++ b/extensions/positron-python/src/test/common/productsToTest.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Product } from '../../client/common/types'; +import { getNamesAndValues } from '../../client/common/utils/enum'; + +export function getProductsForInstallerTests(): { name: string; value: Product }[] { + return getNamesAndValues(Product).filter( + (p) => + ![ + 'pylint', + 'flake8', + 'pycodestyle', + 'pylama', + 'prospector', + 'pydocstyle', + 'yapf', + 'autopep8', + 'mypy', + 'isort', + 'black', + 'bandit', + ].includes(p.name), + ); +} diff --git a/extensions/positron-python/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts b/extensions/positron-python/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts index 7059fb7ab26f..f177db5c2a32 100644 --- a/extensions/positron-python/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts +++ b/extensions/positron-python/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts @@ -31,6 +31,7 @@ import { import { EnvGroups, InterpreterStateArgs, + QuickPickType, SetInterpreterCommand, } from '../../../../client/interpreter/configuration/interpreterSelector/commands/setInterpreter'; import { @@ -265,8 +266,14 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; - const activeItem = await actualParameters!.activeItem; - assert.deepStrictEqual(activeItem, recommended); + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, recommended); + } else { + assert(false, 'Not a function'); + } delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); }); @@ -308,8 +315,14 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; - const activeItem = await actualParameters!.activeItem; - assert.deepStrictEqual(activeItem, noPythonInstalled); + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, noPythonInstalled); + } else { + assert(false, 'Not a function'); + } delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); }); @@ -666,8 +679,14 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; - const activeItem = await actualParameters!.activeItem; - assert.deepStrictEqual(activeItem, recommended); + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, recommended); + } else { + assert(false, 'Not a function'); + } delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts index c773e1cbd5bc..77077ad945fb 100644 --- a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts @@ -45,7 +45,7 @@ suite('Interpreter Path Command', () => { test('If `workspaceFolder` property exists in `args`, it is used to retrieve setting from config', async () => { const args = { workspaceFolder: 'folderPath' }; when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { - assert.deepEqual(arg, Uri.parse('folderPath')); + assert.deepEqual(arg, Uri.file('folderPath')); return Promise.resolve({ path: 'settingValue' }) as unknown; }); @@ -56,7 +56,7 @@ suite('Interpreter Path Command', () => { 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.parse('folderPath')); + assert.deepEqual(arg, Uri.file('folderPath')); return Promise.resolve({ path: 'settingValue' }) as unknown; }); @@ -73,14 +73,4 @@ suite('Interpreter Path Command', () => { const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); expect(setting).to.equal('settingValue'); }); - - test('If `args[1]` is not a valid uri', async () => { - const args = ['command', '${input:some_input}']; - when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { - assert.deepEqual(arg, undefined); - return Promise.resolve({ path: 'settingValue' }) as unknown; - }); - const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); - expect(setting).to.equal('settingValue'); - }); }); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts index 2aec3dcfd041..59f61f81cd85 100644 --- a/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts @@ -22,6 +22,7 @@ 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) { @@ -42,12 +43,18 @@ getInfoPerOS().forEach(([osName, osType, path]) => { 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(() => { diff --git a/extensions/positron-python/src/test/debugger/extension/debugCommands.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/debugCommands.unit.test.ts index 3c023f3f1450..7d2463072f06 100644 --- a/extensions/positron-python/src/test/debugger/extension/debugCommands.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/debugCommands.unit.test.ts @@ -14,6 +14,7 @@ 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; @@ -21,6 +22,7 @@ suite('Debugging - commands', () => { let disposables: typemoq.IMock; let interpreterService: typemoq.IMock; let debugCommands: IExtensionSingleActivationService; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; setup(() => { commandManager = typemoq.Mock.ofType(); @@ -36,6 +38,11 @@ suite('Debugging - commands', () => { sinon.stub(telemetry, 'sendTelemetryEvent').callsFake(() => { /** noop */ }); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); }); teardown(() => { sinon.restore(); diff --git a/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 1513be676ee4..e41d6ce4d53c 100644 --- a/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -191,21 +191,28 @@ suite('Terminal Environment Variable Collection Service', () => { verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); }); - test('If activated variables contain PS1, prefix it using shell integration', async () => { - const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env, PS1: '(prompt)' }; + // 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, - customShell, - ), + 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', '(prompt)', anything())).thenCall((_, _v, o) => { + when(collection.prepend('PS1', '(envName) ', anything())).thenCall((_, _v, o) => { opts = o; }); @@ -216,6 +223,85 @@ suite('Terminal Environment Variable Collection Service', () => { 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); @@ -248,12 +334,13 @@ suite('Terminal Environment Variable Collection Service', () => { assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); }); - test('Prepend full PATH otherwise', async () => { + 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( @@ -275,7 +362,7 @@ suite('Terminal Environment Variable Collection Service', () => { await terminalEnvVarCollectionService._applyCollection(undefined, customShell); verify(collection.clear()).once(); - verify(collection.prepend('PATH', finalPath, anything())).once(); + verify(collection.prepend('PATH', `${finalPath}${separator}`, anything())).once(); verify(collection.replace('PATH', anything(), anything())).never(); assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); }); @@ -415,7 +502,11 @@ suite('Terminal Environment Variable Collection Service', () => { 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 }; + 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 = { @@ -570,7 +661,7 @@ suite('Terminal Environment Variable Collection Service', () => { await terminalEnvVarCollectionService._applyCollection(undefined, customShell); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(collection.clear()).twice(); + verify(collection.clear()).once(); }); test('If no activated variables are returned for default shell, clear collection', async () => { diff --git a/extensions/positron-python/src/test/linters/lint.multilinter.test.ts b/extensions/positron-python/src/test/linters/lint.multilinter.test.ts deleted file mode 100644 index dba263e78479..000000000000 --- a/extensions/positron-python/src/test/linters/lint.multilinter.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as path from 'path'; -import { ConfigurationTarget, DiagnosticCollection, Uri, window, workspace } from 'vscode'; -import { LanguageServerType } from '../../client/activation/types'; -import { ICommandManager } from '../../client/common/application/types'; -import { Product } from '../../client/common/installer/productInstaller'; -import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; -import { ExecutionResult, IPythonToolExecutionService, SpawnOptions } from '../../client/common/process/types'; -import { ExecutionInfo, IConfigurationService } from '../../client/common/types'; -import { ILinterManager } from '../../client/linters/types'; -import { deleteFile, IExtensionTestApi, PythonSettingKeys, rootWorkspaceUri } from '../common'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; - -const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); -const pythonFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'linting'); - -// Mocked out python tool execution (all we need is mocked linter return values). -class MockPythonToolExecService extends PythonToolExecutionService { - // Mocked samples of linter messages from flake8 and pylint: - public flake8Msg = - '1,1,W,W391:blank line at end of file\ns:142:13), :1\n1,7,E,E999:SyntaxError: invalid syntax\n'; - - public pylintMsg = `[ - { - "type": "error", - "module": "print", - "obj": "", - "line": 1, - "column": 0, - "path": "print.py", - "symbol": "syntax-error", - "message": "Missing parentheses in call to 'print'. Did you mean print(x)? (, line 1)", - "message-id": "E0001" - } -]`; - - // Depending on moduleName being exec'd, return the appropriate sample. - public async execForLinter( - executionInfo: ExecutionInfo, - _options: SpawnOptions, - _resource: Uri, - ): Promise> { - let msg = this.flake8Msg; - if (executionInfo.moduleName === 'pylint') { - msg = this.pylintMsg; - } - return { stdout: msg }; - } -} - -suite('Linting - Multiple Linters Enabled Test', () => { - let api: IExtensionTestApi; - let configService: IConfigurationService; - let linterManager: ILinterManager; - - suiteSetup(async () => { - api = await initialize(); - configService = api.serviceContainer.get(IConfigurationService); - linterManager = api.serviceContainer.get(ILinterManager); - }); - setup(async () => { - await initializeTest(); - await resetSettings(); - - // We only want to return some valid strings from linters, we don't care if they - // are being returned by actual linters (we aren't testing linters here, only how - // our code responds to those linters). - api.serviceManager.rebind(IPythonToolExecutionService, MockPythonToolExecService); - }); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await closeActiveWindows(); - await resetSettings(); - await deleteFile(path.join(workspaceUri.fsPath, '.pylintrc')); - await deleteFile(path.join(workspaceUri.fsPath, '.pydocstyle')); - - // Restore the execution service as it was... - api.serviceManager.rebind(IPythonToolExecutionService, PythonToolExecutionService); - }); - - async function resetSettings() { - // Don't run these updates in parallel, as they are updating the same file. - const target = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; - - await configService.updateSetting('linting.enabled', true, rootWorkspaceUri, target); - await configService.updateSetting('linting.lintOnSave', false, rootWorkspaceUri, target); - - 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(pythonFilesPath, 'print.py')); - await window.showTextDocument(document); - await configService.updateSetting( - 'languageServer', - LanguageServerType.Jedi, - undefined, - ConfigurationTarget.Workspace, - ); - await configService.updateSetting('linting.enabled', true, workspaceUri); - await configService.updateSetting('linting.pylintEnabled', true, workspaceUri); - await configService.updateSetting('linting.flake8Enabled', true, workspaceUri); - - const commands = api.serviceContainer.get(ICommandManager); - - const collection = (await commands.executeCommand('python.runLinting')) as DiagnosticCollection; - assert.notStrictEqual(collection, undefined, 'python.runLinting did not return valid diagnostics collection.'); - - const messages = collection!.get(document.uri); - assert.notStrictEqual(messages!.length, 0, 'No diagnostic messages.'); - assert.notStrictEqual(messages!.filter((x) => x.source === 'pylint').length, 0, 'No pylint messages.'); - assert.notStrictEqual(messages!.filter((x) => x.source === 'flake8').length, 0, 'No flake8 messages.'); - }); -}); diff --git a/extensions/positron-python/src/test/linters/linterCommands.unit.test.ts b/extensions/positron-python/src/test/linters/linterCommands.unit.test.ts deleted file mode 100644 index b3d5c4693832..000000000000 --- a/extensions/positron-python/src/test/linters/linterCommands.unit.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { anything, capture, deepEqual, instance, mock, verify, when } from 'ts-mockito'; -import { DiagnosticCollection } 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 } from '../../client/common/application/types'; -import { Commands } from '../../client/common/constants'; -import { Product } from '../../client/common/types'; -import { ServiceContainer } from '../../client/ioc/container'; -import { LinterCommands } from '../../client/linters/linterCommands'; -import { LinterManager } from '../../client/linters/linterManager'; -import { LintingEngine } from '../../client/linters/lintingEngine'; -import { ILinterInfo, ILinterManager, ILintingEngine } from '../../client/linters/types'; - -suite('Linting - Linter Commands', () => { - let linterCommands: LinterCommands; - let manager: ILinterManager; - let shell: IApplicationShell; - let docManager: IDocumentManager; - let cmdManager: ICommandManager; - let lintingEngine: ILintingEngine; - setup(() => { - const svcContainer = mock(ServiceContainer); - manager = mock(LinterManager); - shell = mock(ApplicationShell); - docManager = mock(DocumentManager); - cmdManager = mock(CommandManager); - lintingEngine = mock(LintingEngine); - when(svcContainer.get(ILinterManager)).thenReturn(instance(manager)); - when(svcContainer.get(IApplicationShell)).thenReturn(instance(shell)); - when(svcContainer.get(IDocumentManager)).thenReturn(instance(docManager)); - when(svcContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - when(svcContainer.get(ILintingEngine)).thenReturn(instance(lintingEngine)); - linterCommands = new LinterCommands(instance(svcContainer)); - }); - - test('Commands are registered', () => { - verify(cmdManager.registerCommand(Commands.Set_Linter, anything())).once(); - verify(cmdManager.registerCommand(Commands.Enable_Linter, anything())).once(); - verify(cmdManager.registerCommand(Commands.Run_Linter, anything())).once(); - }); - - test('Run Linting method will lint all open files', async () => { - when(lintingEngine.lintOpenPythonFiles('manual')).thenResolve(('Hello' as unknown) as DiagnosticCollection); - - const result = await linterCommands.runLinting(); - - expect(result).to.be.equal('Hello'); - }); - - async function testEnableLintingWithCurrentState( - currentState: boolean, - selectedState: 'Enable' | 'Disable' | undefined, - ) { - when(manager.isLintingEnabled(anything())).thenResolve(currentState); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${currentState ? 'Enable' : 'Disable'}`, - }; - when(shell.showQuickPick(anything(), anything())).thenReturn(Promise.resolve(selectedState)); - - 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(['Enable', 'Disable']); - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - - if (selectedState) { - verify(manager.enableLintingAsync(selectedState === 'Enable', anything())).once(); - } else { - verify(manager.enableLintingAsync(anything(), anything())).never(); - } - } - test("Enable linting should check if linting is enabled, and display current state of 'Enable' and select nothing", async () => { - await testEnableLintingWithCurrentState(true, undefined); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Enable' and select 'Enable'", async () => { - await testEnableLintingWithCurrentState(true, 'Enable'); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Enable' and select 'Disable'", async () => { - await testEnableLintingWithCurrentState(true, 'Disable'); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Disable' and select 'Enable'", async () => { - await testEnableLintingWithCurrentState(true, 'Enable'); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Disable' and select 'Disable'", async () => { - await testEnableLintingWithCurrentState(true, 'Disable'); - }); - - test('Set Linter should display a quickpick', async () => { - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(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 unknown) as ILinterInfo]; - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(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' }, { id: 'linterId2' }] as unknown) as ILinterInfo[]; - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(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 unknown) as ILinterInfo[]; - const activeLinters: ILinterInfo[] = ([{ id: '1' }, { id: '3' }] as unknown) as ILinterInfo[]; - when(manager.getAllLinterInfos()).thenReturn(linters); - when(manager.getActiveLinters(anything())).thenResolve(activeLinters); - when(shell.showQuickPick(anything(), anything())).thenReturn(Promise.resolve('3')); - when(shell.showWarningMessage(anything(), 'Yes', 'No')).thenReturn(Promise.resolve('Yes')); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: 'current: multiple selected', - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())).once(); - verify(shell.showWarningMessage(anything(), 'Yes', 'No')).once(); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - verify(manager.setActiveLintersAsync(deepEqual([('Three' as unknown) as Product]), anything())).once(); - }); -}); diff --git a/extensions/positron-python/src/test/mocks/mockChildProcess.ts b/extensions/positron-python/src/test/mocks/mockChildProcess.ts index a46d66d79ca0..c0a24b1c955f 100644 --- a/extensions/positron-python/src/test/mocks/mockChildProcess.ts +++ b/extensions/positron-python/src/test/mocks/mockChildProcess.ts @@ -133,9 +133,9 @@ export class MockChildProcess extends EventEmitter { 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 = Array.isArray(args) ? args : [args]; - listener(argsArray); + this.eventMap.get(event.toString()).forEach((listener: (...arg0: unknown[]) => void) => { + const argsArray: unknown[] = Array.isArray(args) ? args : [args]; + listener(...argsArray); }); } return true; diff --git a/extensions/positron-python/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts index ca0e24d5f3d3..1e9de68ad77a 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts @@ -156,6 +156,7 @@ suite('Conda and its environments are located correctly', () => { const isFile = typeof dir[name] === 'string'; return { name, + path: dir.name?.toString() ?? '', isFile: () => isFile, isDirectory: () => !isFile, isBlockDevice: () => false, diff --git a/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName b/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName index b723d0199f86..b5846df18ca8 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName +++ b/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile b/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile index b723d0199f86..b5846df18ca8 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile +++ b/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile b/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile index b723d0199f86..b5846df18ca8 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile +++ b/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile b/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile index b723d0199f86..b5846df18ca8 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile +++ b/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/extensions/positron-python/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts new file mode 100644 index 000000000000..f751d270219e --- /dev/null +++ b/extensions/positron-python/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts @@ -0,0 +1,285 @@ +// 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 { 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; + let disableWorkspaceCreateEnvironmentTriggerStub: 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'); + disableWorkspaceCreateEnvironmentTriggerStub = sinon.stub( + triggerUtils, + 'disableWorkspaceCreateEnvironmentTrigger', + ); + }); + + 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); + sinon.assert.notCalled(disableWorkspaceCreateEnvironmentTriggerStub); + }); + + 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); + sinon.assert.notCalled(disableWorkspaceCreateEnvironmentTriggerStub); + }); + + test('Should show prompt if all conditions met: User clicks disable global', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + + showInformationMessageStub.resolves(CreateEnv.Trigger.disableCheck); + 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); + sinon.assert.notCalled(disableWorkspaceCreateEnvironmentTriggerStub); + }); + + test('Should show prompt if all conditions met: User clicks disable workspace', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + + showInformationMessageStub.resolves(CreateEnv.Trigger.disableCheckWorkspace); + 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); + sinon.assert.calledOnce(disableWorkspaceCreateEnvironmentTriggerStub); + }); +}); diff --git a/extensions/positron-python/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts index 10fe06bba442..4addb5687085 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts @@ -62,7 +62,7 @@ function getPyProjectTomlFile(): typemoq.IMock { .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.7"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + '[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; } @@ -76,7 +76,7 @@ function getSomeTomlFile(): typemoq.IMock { .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.7"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + '[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; } diff --git a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts index e1ac1bafe6ac..e1344dc5f3ad 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts @@ -35,6 +35,8 @@ suite('Conda Creation provider tests', () => { 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'); @@ -46,6 +48,11 @@ suite('Conda Creation provider tests', () => { showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); showErrorMessageWithLogsStub.resolves(); + pickExistingCondaActionStub = sinon.stub(condaUtils, 'pickExistingCondaAction'); + pickExistingCondaActionStub.resolves(condaUtils.ExistingCondaAction.Create); + + getPrefixCondaEnvPathStub = sinon.stub(commonUtils, 'getPrefixCondaEnvPath'); + progressMock = typemoq.Mock.ofType(); condaProvider = condaCreationProvider(); }); @@ -77,6 +84,7 @@ suite('Conda Creation provider tests', () => { pickPythonVersionStub.resolves(undefined); await assert.isRejected(condaProvider.createEnvironment()); + assert.isTrue(pickExistingCondaActionStub.calledOnce); }); test('Create conda environment', async () => { @@ -136,6 +144,7 @@ suite('Conda Creation provider tests', () => { workspaceFolder: workspace1, }); assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(pickExistingCondaActionStub.calledOnce); }); test('Create conda environment failed', async () => { @@ -188,6 +197,7 @@ suite('Conda Creation provider tests', () => { 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 () => { @@ -245,5 +255,26 @@ suite('Conda Creation provider tests', () => { 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/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts new file mode 100644 index 000000000000..b1acd0678714 --- /dev/null +++ b/extensions/positron-python/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/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts index 3f115f9f58ed..a3f4a1abe905 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts @@ -3,9 +3,17 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; -import { CancellationTokenSource } from 'vscode'; +import * as path from 'path'; +import { CancellationTokenSource, Uri } from 'vscode'; import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; -import { pickPythonVersion } from '../../../../client/pythonEnvironments/creation/provider/condaUtils'; +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; @@ -43,3 +51,60 @@ suite('Conda Utils test', () => { 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/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts index de65887b7edc..72914b9118e2 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -15,7 +15,7 @@ import * as rawProcessApis from '../../../../client/common/process/rawProcessApi 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 } from '../../../../client/common/process/types'; +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'; @@ -394,4 +394,157 @@ suite('venv Creation provider tests', () => { assert.isTrue(showErrorMessageWithLogsStub.notCalled); assert.isTrue(deleteEnvironmentStub.notCalled); }); + + test('Create venv with 1000 requirement files', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + const requirements = Array.from({ length: 1000 }, (_, i) => ({ + installType: 'requirements', + installItem: `requirements${i}.txt`, + })); + pickPackagesToInstallStub.resolves(requirements); + const expected = JSON.stringify({ requirements: requirements.map((r) => r.installItem) }); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + let stdin: undefined | string; + let hasStdinArg = false; + execObservableStub.callsFake((_c, argv: string[], options) => { + stdin = options?.stdinStr; + hasStdinArg = argv.includes('--stdin'); + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + assert.strictEqual(stdin, expected); + assert.isTrue(hasStdinArg); + }); + + test('Create venv with 5 requirement files', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + const requirements = Array.from({ length: 5 }, (_, i) => ({ + installType: 'requirements', + installItem: `requirements${i}.txt`, + })); + pickPackagesToInstallStub.resolves(requirements); + const expectedRequirements = requirements.map((r) => r.installItem).sort(); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + let stdin: undefined | string; + let hasStdinArg = false; + let actualRequirements: string[] = []; + execObservableStub.callsFake((_c, argv: string[], options: SpawnOptions) => { + stdin = options?.stdinStr; + actualRequirements = argv.filter((arg) => arg.startsWith('requirements')).sort(); + hasStdinArg = argv.includes('--stdin'); + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + assert.isUndefined(stdin); + assert.deepStrictEqual(actualRequirements, expectedRequirements); + assert.isFalse(hasStdinArg); + }); }); diff --git a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts index ae4f43a0296c..1671026d5dd4 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts @@ -10,11 +10,13 @@ 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); @@ -23,6 +25,7 @@ suite('Venv Utils test', () => { 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')), @@ -35,6 +38,7 @@ suite('Venv Utils test', () => { showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); pathExistsStub = sinon.stub(fs, 'pathExists'); readFileStub = sinon.stub(fs, 'readFile'); + showTextDocumentStub = sinon.stub(windowApis, 'showTextDocument'); }); teardown(() => { @@ -224,13 +228,18 @@ suite('Venv Utils test', () => { await assert.isRejected(pickPackagesToInstall(workspace1)); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { 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); @@ -257,13 +266,18 @@ suite('Venv Utils test', () => { const actual = await pickPackagesToInstall(workspace1); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { 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, []); @@ -290,13 +304,18 @@ suite('Venv Utils test', () => { const actual = await pickPackagesToInstall(workspace1); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { 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, [ @@ -328,13 +347,18 @@ suite('Venv Utils test', () => { const actual = await pickPackagesToInstall(workspace1); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { 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, [ @@ -349,6 +373,45 @@ suite('Venv Utils test', () => { ]); 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', () => { diff --git a/extensions/positron-python/src/test/standardTest.ts b/extensions/positron-python/src/test/standardTest.ts index 0562d1adf431..0fe53437cf3d 100644 --- a/extensions/positron-python/src/test/standardTest.ts +++ b/extensions/positron-python/src/test/standardTest.ts @@ -6,6 +6,7 @@ import { downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath, runTest 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') { @@ -85,18 +86,20 @@ async function start() { : ['--disable-extensions']; await installJupyterExtension(vscodeExecutablePath); await installPylanceExtension(vscodeExecutablePath); + console.log('VS Code executable', vscodeExecutablePath); const launchArgs = baseLaunchArgs .concat([workspacePath]) .concat(channel === 'insiders' ? ['--enable-proposed-api'] : []) .concat(['--timeout', '5000']); console.log(`Starting vscode ${channel} with args ${launchArgs.join(' ')}`); - await runTests({ + 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().catch((ex) => { console.error('End Standard tests (with errors)', ex); diff --git a/extensions/positron-python/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts b/extensions/positron-python/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts index 30f95c94d217..29c310f6c724 100644 --- a/extensions/positron-python/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts +++ b/extensions/positron-python/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts @@ -2,6 +2,7 @@ // 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'; @@ -13,6 +14,7 @@ import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } fr 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'; suite('Terminal - Code Execution Manager', () => { let executionManager: ICodeExecutionManager; @@ -24,6 +26,7 @@ suite('Terminal - Code Execution Manager', () => { let configService: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; let interpreterService: TypeMoq.IMock; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; setup(() => { fileSystem = TypeMoq.Mock.ofType(); fileSystem.setup((f) => f.readFile(TypeMoq.It.isAny())).returns(() => Promise.resolve('')); @@ -52,8 +55,14 @@ suite('Terminal - Code Execution Manager', () => { configService.object, serviceContainer.object, ); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); }); teardown(() => { + sinon.restore(); disposables.forEach((disposable) => { if (disposable) { disposable.dispose(); diff --git a/extensions/positron-python/src/test/testing/common/testingAdapter.test.ts b/extensions/positron-python/src/test/testing/common/testingAdapter.test.ts index 1334085e4cea..4f46f1cf738c 100644 --- a/extensions/positron-python/src/test/testing/common/testingAdapter.test.ts +++ b/extensions/positron-python/src/test/testing/common/testingAdapter.test.ts @@ -1,32 +1,38 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { TestRun, Uri } from 'vscode'; +import { TestController, TestRun, Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as path from 'path'; import * as assert from 'assert'; import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; -import { ITestResultResolver, ITestServer } from '../../../client/testing/testController/common/types'; +import { ITestController, ITestResultResolver } from '../../../client/testing/testController/common/types'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; import { IPythonExecutionFactory } from '../../../client/common/process/types'; import { ITestDebugLauncher } from '../../../client/testing/common/types'; import { IConfigurationService, ITestOutputChannel } 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 { 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'; suite('End to End Tests: test adapters', () => { - let resultResolver: typeMoq.IMock; - let pythonTestServer: ITestServer; + let resultResolver: ITestResultResolver; + let pythonTestServer: PythonTestServer; let pythonExecFactory: IPythonExecutionFactory; let debugLauncher: ITestDebugLauncher; let configService: IConfigurationService; - let testOutputChannel: ITestOutputChannel; let serviceContainer: IServiceContainer; let workspaceUri: Uri; + let testOutputChannel: typeMoq.IMock; + let testController: TestController; + const unittestProvider: TestProvider = UNITTEST_PROVIDER; + const pytestProvider: TestProvider = PYTEST_PROVIDER; const rootPathSmallWorkspace = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, 'src', @@ -39,6 +45,18 @@ suite('End to End Tests: test adapters', () => { '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', + ); suiteSetup(async () => { serviceContainer = (await initialize()).serviceContainer; }); @@ -48,72 +66,97 @@ suite('End to End Tests: test adapters', () => { configService = serviceContainer.get(IConfigurationService); pythonExecFactory = serviceContainer.get(IPythonExecutionFactory); debugLauncher = serviceContainer.get(ITestDebugLauncher); - testOutputChannel = serviceContainer.get(ITestOutputChannel); - - // create mock resultResolver object - resultResolver = typeMoq.Mock.ofType(); + testController = serviceContainer.get(ITestController); // create objects that were not injected pythonTestServer = new PythonTestServer(pythonExecFactory, debugLauncher); await pythonTestServer.serverReady(); + + testOutputChannel = typeMoq.Mock.ofType(); + testOutputChannel + .setup((x) => x.append(typeMoq.It.isAny())) + .callback((appendVal: any) => { + traceLog('output channel - ', appendVal.toString()); + }) + .returns(() => { + // Whatever you need to return + }); + testOutputChannel + .setup((x) => x.appendLine(typeMoq.It.isAny())) + .callback((appendVal: any) => { + traceLog('output channel ', appendVal.toString()); + }) + .returns(() => { + // Whatever you need to return + }); + }); + teardown(async () => { + pythonTestServer.dispose(); }); test('unittest discovery adapter small workspace', async () => { // result resolver and saved data for assertions let actualData: { - status: unknown; - error: string | any[]; - tests: unknown; + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + workspaceUri = Uri.parse(rootPathSmallWorkspace); + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + resultResolver._resolveDiscovery = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveDiscovery ${data}`); - actualData = data; - return Promise.resolve(); - }); // set workspace to test workspace folder and set up settings - workspaceUri = Uri.parse(rootPathSmallWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; // run unittest discovery const discoveryAdapter = new UnittestTestDiscoveryAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); await discoveryAdapter.discoverTests(workspaceUri).finally(() => { // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be '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: { - status: unknown; - error: string | any[]; - tests: unknown; + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + resultResolver._resolveDiscovery = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveDiscovery ${data}`); - actualData = data; - return Promise.resolve(); - }); // set settings to work for the given workspace workspaceUri = Uri.parse(rootPathLargeWorkspace); @@ -122,85 +165,89 @@ suite('End to End Tests: test adapters', () => { const discoveryAdapter = new UnittestTestDiscoveryAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); await discoveryAdapter.discoverTests(workspaceUri).finally(() => { - // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be '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: { - status: unknown; - error: string | any[]; - tests: unknown; + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver._resolveDiscovery = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveDiscovery ${data}`); - actualData = data; - return Promise.resolve(); - }); // run pytest discovery const discoveryAdapter = new PytestTestDiscoveryAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathSmallWorkspace); - await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(actualData.error.length, 0, "Expected no errors in 'error' field"); + 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 large workspace', async () => { // result resolver and saved data for assertions let actualData: { - status: unknown; - error: string | any[]; - tests: unknown; + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver._resolveDiscovery = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveDiscovery ${data}`); - actualData = data; - return Promise.resolve(); - }); // run pytest discovery const discoveryAdapter = new PytestTestDiscoveryAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); // set workspace to test workspace folder @@ -208,33 +255,42 @@ suite('End to End Tests: test adapters', () => { await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(actualData.error.length, 0, "Expected no errors in 'error' field"); + 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', async () => { // result resolver and saved data for assertions - let actualData: { - status: unknown; - error: string | any[]; - result: unknown; + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + 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; + } + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveExecution ${data}`); - actualData = data; - return Promise.resolve(); - }); // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathSmallWorkspace); @@ -243,8 +299,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new UnittestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -258,35 +314,34 @@ suite('End to End Tests: test adapters', () => { await executionAdapter .runTests(workspaceUri, ['test_simple.SimpleClass.test_simple_unit'], false, testRun.object) .finally(() => { - // verification after execution is complete - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); - - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm tests are found - assert.ok(actualData.result, 'Expected results to be present'); + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); test('unittest execution adapter large workspace', async () => { // result resolver and saved data for assertions - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceError(`resolveExecution ${data}`); - traceLog(`resolveExecution ${data}`); - // do the following asserts for each time resolveExecution is called, should be called once per test. - // 1. Check the status, can be subtest success or failure - assert( - data.status === 'subtest-success' || data.status === 'subtest-failure', - "Expected status to be 'subtest-success' or 'subtest-failure'", + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + 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}`, ); - // 2. Confirm tests are found - assert.ok(data.result, 'Expected results to be present'); - return Promise.resolve(); - }); + assert.ok(payload.result, 'Expected results to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathLargeWorkspace); @@ -296,8 +351,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new UnittestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -310,29 +365,35 @@ suite('End to End Tests: test adapters', () => { ); await executionAdapter .runTests(workspaceUri, ['test_parameterized_subtest.NumbersTest.test_even'], false, testRun.object) - .finally(() => { - // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.atLeastOnce(), - ); + .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); }); }); test('pytest execution adapter small workspace', async () => { // result resolver and saved data for assertions - let actualData: { - status: unknown; - error: string | any[]; - result: unknown; + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + 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; + } + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveExecution ${data}`); - actualData = data; - return Promise.resolve(); - }); - // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathSmallWorkspace); @@ -340,8 +401,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new PytestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -360,42 +421,42 @@ suite('End to End Tests: test adapters', () => { testRun.object, pythonExecFactory, ) - .finally(() => { - // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); - - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(actualData.error, null, "Expected no errors in 'error' field"); - // 3. Confirm tests are found - assert.ok(actualData.result, 'Expected results to be present'); + .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); }); }); test('pytest execution adapter large workspace', async () => { - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveExecution ${data}`); - // do the following asserts for each time resolveExecution is called, should be called once per test. - // 1. Check the status is "success" - assert.strictEqual(data.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(data.error, null, "Expected no errors in 'error' field"); - // 3. Confirm tests are found - assert.ok(data.result, 'Expected results to be present'); - return Promise.resolve(); - }); + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + 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; + } + return Promise.resolve(); + }; // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathLargeWorkspace); // generate list of test_ids const testIds: string[] = []; - for (let i = 0; i < 200; i = i + 1) { + for (let i = 0; i < 2000; i = i + 1) { const testId = `${rootPathLargeWorkspace}/test_parameterized_subtest.py::test_odd_even[${i}]`; testIds.push(testId); } @@ -404,8 +465,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new PytestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -416,12 +477,243 @@ suite('End to End Tests: test adapters', () => { onCancellationRequested: () => undefined, } as any), ); - await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object, pythonExecFactory).finally(() => { - // resolve execution should be called 200 times since there are 200 tests run. - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.exactly(200), + await executionAdapter.runTests(workspaceUri, testIds, false, 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); + }); + }); + 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 = async (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; + } + return Promise.resolve(); + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathDiscoveryErrorWorkspace); + + const discoveryAdapter = new UnittestTestDiscoveryAdapter( + pythonTestServer, + configService, + testOutputChannel.object, + resultResolver, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await discoveryAdapter.discoverTests(workspaceUri).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 = async (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'); + 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; + } + return Promise.resolve(); + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter( + pythonTestServer, + configService, + testOutputChannel.object, + resultResolver, + ); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathDiscoveryErrorWorkspace); + 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('unittest execution adapter seg fault error handling', async () => { + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + traceLog(`unittest execution 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.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.TestSegmentationFault.test_segfault'); + assert.notDeepEqual(indexOfTest, -1, 'Expected testId to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; + + const testId = `test_seg_fault.TestSegmentationFault.test_segfault`; + const testIds: string[] = [testId]; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathErrorWorkspace); + + // run pytest execution + const executionAdapter = new UnittestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel.object, + resultResolver, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object).finally(() => { + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + 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 = async (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 (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; + } + return Promise.resolve(); + }; + + const testId = `${rootPathErrorWorkspace}/test_seg_fault.py::TestSegmentationFault::test_segfault`; + const testIds: string[] = [testId]; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathErrorWorkspace); + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel.object, + resultResolver, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object, pythonExecFactory).finally(() => { + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); }); diff --git a/extensions/positron-python/src/test/testing/common/testingPayloadsEot.test.ts b/extensions/positron-python/src/test/testing/common/testingPayloadsEot.test.ts new file mode 100644 index 000000000000..a30b1efe288c --- /dev/null +++ b/extensions/positron-python/src/test/testing/common/testingPayloadsEot.test.ts @@ -0,0 +1,214 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { TestController, TestRun, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as path from 'path'; +import * as assert from 'assert'; +import * as net from 'net'; +import { Observable } from 'rxjs'; +import * as crypto from 'crypto'; +// import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; +import * as sinon from 'sinon'; +import { ITestController, ITestResultResolver } from '../../../client/testing/testController/common/types'; +import { PythonTestServer } from '../../../client/testing/testController/common/server'; +import { IPythonExecutionFactory, IPythonExecutionService, Output } from '../../../client/common/process/types'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { initialize } from '../../initialize'; +import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { PythonResultResolver } from '../../../client/testing/testController/common/resultResolver'; +import { PYTEST_PROVIDER } from '../../../client/testing/common/constants'; +import { MockChildProcess } from '../../mocks/mockChildProcess'; +import { + PAYLOAD_SINGLE_CHUNK, + PAYLOAD_MULTI_CHUNK, + PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY, + DataWithPayloadChunks, + PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY, + PAYLOAD_ONLY_HEADER_MULTI_CHUNK, +} from '../testController/payloadTestCases'; +import { traceLog } from '../../../client/logging'; + +const FAKE_UUID = 'fake-u-u-i-d'; +export interface TestCase { + name: string; + value: DataWithPayloadChunks; +} + +const testCases: Array = [ + { + name: 'header in single chunk edge case', + value: PAYLOAD_ONLY_HEADER_MULTI_CHUNK(FAKE_UUID), + }, + { + name: 'single payload single chunk', + value: PAYLOAD_SINGLE_CHUNK(FAKE_UUID), + }, + { + name: 'multiple payloads per buffer chunk', + value: PAYLOAD_MULTI_CHUNK(FAKE_UUID), + }, + { + name: 'single payload across multiple buffer chunks', + value: PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY(FAKE_UUID), + }, + { + name: 'two chunks, payload split and two payloads in a chunk', + value: PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY(FAKE_UUID), + }, +]; + +suite('EOT tests', () => { + let resultResolver: ITestResultResolver; + let pythonTestServer: PythonTestServer; + let debugLauncher: ITestDebugLauncher; + let configService: IConfigurationService; + let serviceContainer: IServiceContainer; + let workspaceUri: Uri; + let testOutputChannel: typeMoq.IMock; + let testController: TestController; + let stubExecutionFactory: typeMoq.IMock; + let client: net.Socket; + let mockProc: MockChildProcess; + const sandbox = sinon.createSandbox(); + // const unittestProvider: TestProvider = UNITTEST_PROVIDER; + // const pytestProvider: TestProvider = PYTEST_PROVIDER; + const rootPathSmallWorkspace = path.join('src'); + suiteSetup(async () => { + serviceContainer = (await initialize()).serviceContainer; + }); + + setup(async () => { + // create objects that were injected + configService = serviceContainer.get(IConfigurationService); + debugLauncher = serviceContainer.get(ITestDebugLauncher); + testController = serviceContainer.get(ITestController); + + // create client to act as python server which sends testing result response + client = new net.Socket(); + client.on('error', (error) => { + traceLog('Socket connection error:', error); + }); + + mockProc = new MockChildProcess('', ['']); + const output2 = new Observable>(() => { + /* no op */ + }); + + // stub out execution service and factory so mock data is returned from client. + const stubExecutionService = ({ + execObservable: () => { + client.connect(pythonTestServer.getPort()); + return { + proc: mockProc, + out: output2, + dispose: () => { + /* no-body */ + }, + }; + }, + } as unknown) as IPythonExecutionService; + + stubExecutionFactory = typeMoq.Mock.ofType(); + stubExecutionFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(stubExecutionService)); + + // stub create UUID + + const v4Stub = sandbox.stub(crypto, 'randomUUID'); + v4Stub.returns(FAKE_UUID); + + // create python test server + pythonTestServer = new PythonTestServer(stubExecutionFactory.object, debugLauncher); + await pythonTestServer.serverReady(); + // handles output from client + testOutputChannel = typeMoq.Mock.ofType(); + testOutputChannel + .setup((x) => x.append(typeMoq.It.isAny())) + .callback((appendVal: any) => { + traceLog('out - ', appendVal.toString()); + }) + .returns(() => { + // Whatever you need to return + }); + testOutputChannel + .setup((x) => x.appendLine(typeMoq.It.isAny())) + .callback((appendVal: any) => { + traceLog('outL - ', appendVal.toString()); + }) + .returns(() => { + // Whatever you need to return + }); + }); + teardown(async () => { + pythonTestServer.dispose(); + sandbox.restore(); + }); + testCases.forEach((testCase) => { + test(`Testing Payloads: ${testCase.name}`, async () => { + let actualCollectedResult = ''; + client.on('connect', async () => { + traceLog('socket connected, sending stubbed data'); + // payload is a string array, each string represents one line written to the buffer + const { payloadArray } = testCase.value; + for (let i = 0; i < payloadArray.length; i = i + 1) { + await (async (clientSub, payloadSub) => { + if (!clientSub.write(payloadSub)) { + // If write returns false, wait for the 'drain' event before proceeding + await new Promise((resolve) => clientSub.once('drain', resolve)); + } + })(client, payloadArray[i]); + } + mockProc.emit('close', 0, null); + client.end(); + }); + + resultResolver = new PythonResultResolver(testController, PYTEST_PROVIDER, workspaceUri); + resultResolver._resolveExecution = async (payload, _token?) => { + // the payloads that get to the _resolveExecution are all data and should be successful. + actualCollectedResult = actualCollectedResult + JSON.stringify(payload.result); + assert.strictEqual(payload.status, 'success', "Expected status to be 'success'"); + assert.ok(payload.result, 'Expected results to be present'); + + return Promise.resolve(); + }; + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel.object, + resultResolver, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter + .runTests( + workspaceUri, + [`${rootPathSmallWorkspace}/test_simple.py::test_a`], + false, + testRun.object, + stubExecutionFactory.object, + ) + .then(() => { + assert.strictEqual( + testCase.value.data, + actualCollectedResult, + "Expected collected result to match 'data'", + ); + }); + }); + }); +}); diff --git a/extensions/positron-python/src/test/testing/testController/payloadTestCases.ts b/extensions/positron-python/src/test/testing/testController/payloadTestCases.ts new file mode 100644 index 000000000000..f7f94a926f5f --- /dev/null +++ b/extensions/positron-python/src/test/testing/testController/payloadTestCases.ts @@ -0,0 +1,173 @@ +export interface DataWithPayloadChunks { + payloadArray: string[]; + data: string; +} + +const EOT_PAYLOAD = `Content-Length: 42 +Content-Type: application/json +Request-uuid: fake-u-u-i-d + +{"command_type": "execution", "eot": true}`; + +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)', + }, + }, +}; + +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 PAYLOAD_SINGLE_CHUNK(uuid: string): DataWithPayloadChunks { + const payload = createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + + return { + payloadArray: [payload, EOT_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, EOT_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); + payloadArray.push(EOT_PAYLOAD); + 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); + splitPayload.push(EOT_PAYLOAD); + 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), + ); + + splitPayload.push(EOT_PAYLOAD); + return { + payloadArray: splitPayload, + data: finalResult, + }; +} + +export function PAYLOAD_SPLIT_MULTI_CHUNK_RAN_ORDER_ARRAY(uuid: string): Array { + return [ + `Content-Length: 411 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-success", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=0)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-success", "message": "None", "traceback": null, "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=0)"}}} + +Content-Length: 411 +Content-Type: application/json +Request-uuid: 9${uuid} + +{"cwd": "/home/runner/work/vscode-`, + `python/vscode-python/path with`, + ` spaces/src" + +Content-Length: 959 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-failure", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=1)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-failure", "message": "(, AssertionError('1 != 0'), )", "traceback": " File \"/opt/hostedtoolcache/Python/3.11.4/x64/lib/python3.11/unittest/case.py\", line 57, in testPartExecutor\n yield\n File \"/opt/hostedtoolcache/Python/3.11.4/x64/lib/python3.11/unittest/case.py\", line 538, in subTest\n yield\n File \"/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py\", line 16, in test_even\n self.assertEqual(i % 2, 0)\nAssertionError: 1 != 0\n", "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=1)"}}} +Content-Length: 411 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-success", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=2)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-success", "message": "None", "traceback": null, "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=2)"}}}`, + ]; +} diff --git a/extensions/positron-python/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/extensions/positron-python/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 43b763f56e6c..a2e5c810dc86 100644 --- a/extensions/positron-python/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/extensions/positron-python/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -21,6 +21,7 @@ import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/co 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'; suite('pytest test execution adapter', () => { let testServer: typeMoq.IMock; @@ -33,7 +34,7 @@ suite('pytest test execution adapter', () => { (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; let myTestPath: string; let mockProc: MockChildProcess; - let utilsStub: sinon.SinonStub; + let utilsStartServerStub: sinon.SinonStub; setup(() => { testServer = typeMoq.Mock.ofType(); testServer.setup((t) => t.getPort()).returns(() => 12345); @@ -51,6 +52,8 @@ suite('pytest test execution adapter', () => { isTestExecution: () => false, } as unknown) as IConfigurationService; + // mock out the result resolver + // set up exec service with child process mockProc = new MockChildProcess('', ['']); const output = new Observable>(() => { @@ -67,7 +70,7 @@ suite('pytest test execution adapter', () => { }, })); execFactory = typeMoq.Mock.ofType(); - utilsStub = sinon.stub(util, 'startTestIdServer'); + utilsStartServerStub = sinon.stub(util, 'startTestIdServer'); debugLauncher = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) @@ -79,13 +82,6 @@ suite('pytest test execution adapter', () => { deferred.resolve(); return Promise.resolve({ stdout: '{}' }); }); - debugLauncher - .setup((d) => d.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => { - deferred.resolve(); - return Promise.resolve(); - }); - 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); @@ -104,7 +100,7 @@ suite('pytest test execution adapter', () => { deferred2.resolve(); return Promise.resolve(execService.object); }); - utilsStub.callsFake(() => { + utilsStartServerStub.callsFake(() => { deferred3.resolve(); return Promise.resolve(54321); }); @@ -131,7 +127,7 @@ suite('pytest test execution adapter', () => { mockProc.trigger('close'); // assert - sinon.assert.calledWithExactly(utilsStub, testIds); + sinon.assert.calledWithExactly(utilsStartServerStub, testIds); }); test('pytest execution called with correct args', async () => { const deferred2 = createDeferred(); @@ -143,7 +139,7 @@ suite('pytest test execution adapter', () => { deferred2.resolve(); return Promise.resolve(execService.object); }); - utilsStub.callsFake(() => { + utilsStartServerStub.callsFake(() => { deferred3.resolve(); return Promise.resolve(54321); }); @@ -175,7 +171,6 @@ suite('pytest test execution adapter', () => { TEST_UUID: 'uuid123', TEST_PORT: '12345', }; - // execService.verify((x) => x.exec(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); execService.verify( (x) => x.execObservable( @@ -203,7 +198,7 @@ suite('pytest test execution adapter', () => { deferred2.resolve(); return Promise.resolve(execService.object); }); - utilsStub.callsFake(() => { + utilsStartServerStub.callsFake(() => { deferred3.resolve(); return Promise.resolve(54321); }); @@ -262,12 +257,28 @@ suite('pytest test execution adapter', () => { }); test('Debug launched correctly for pytest', async () => { const deferred3 = createDeferred(); - utilsStub.callsFake(() => { + const deferredEOT = createDeferred(); + utilsStartServerStub.callsFake(() => { deferred3.resolve(); return Promise.resolve(54321); }); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async () => { + traceInfo('stubs launch debugger'); + deferredEOT.resolve(); + }); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => deferredEOT); const testRun = typeMoq.Mock.ofType(); - testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer @@ -298,5 +309,6 @@ suite('pytest test execution adapter', () => { ), typeMoq.Times.once(), ); + testServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); }); }); diff --git a/extensions/positron-python/src/test/testing/testController/resultResolver.unit.test.ts b/extensions/positron-python/src/test/testing/testController/resultResolver.unit.test.ts index 09a68128167d..2078c72e8cf6 100644 --- a/extensions/positron-python/src/test/testing/testController/resultResolver.unit.test.ts +++ b/extensions/positron-python/src/test/testing/testController/resultResolver.unit.test.ts @@ -14,6 +14,8 @@ import { 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 { Deferred, createDeferred } from '../../../client/common/utils/async'; +import { traceLog } from '../../../client/logging'; suite('Result Resolver tests', () => { suite('Test discovery', () => { @@ -87,7 +89,8 @@ suite('Result Resolver tests', () => { const populateTestTreeStub = sinon.stub(util, 'populateTestTree').returns(); // call resolve discovery - resultResolver.resolveDiscovery(payload, cancelationToken); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveDiscovery(payload, deferredTillEOT, cancelationToken); // assert the stub functions were called with the correct parameters @@ -126,7 +129,8 @@ suite('Result Resolver tests', () => { const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); // call resolve discovery - resultResolver.resolveDiscovery(payload); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveDiscovery(payload, deferredTillEOT, cancelationToken); // assert the stub functions were called with the correct parameters @@ -171,7 +175,8 @@ suite('Result Resolver 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); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveDiscovery(payload, deferredTillEOT, cancelationToken); // assert the stub functions were called with the correct parameters @@ -270,22 +275,23 @@ suite('Result Resolver tests', () => { testProvider, workspaceUri, ); - const mockSubtestItem = createMockTestItem('parentTest subTest'); + 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('parentTest subTest', 'parentTest subTest'); + resultResolver.runIdToVSid.set(subtestName, subtestName); // add this mock test to the map of known test items resultResolver.runIdToTestItem.set('parentTest', mockTestItem2); - resultResolver.runIdToTestItem.set('parentTest subTest', mockSubtestItem); + resultResolver.runIdToTestItem.set(subtestName, mockSubtestItem); let generatedId: string | undefined; testControllerMock .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny())) .callback((id: string) => { generatedId = id; - console.log('createTestItem function called with id:', id); + traceLog('createTestItem function called with id:', id); }) .returns(() => ({ id: 'id_this', label: 'label_this', uri: workspaceUri } as TestItem)); @@ -294,23 +300,24 @@ suite('Result Resolver tests', () => { cwd: workspaceUri.fsPath, status: 'success', result: { - 'parentTest subTest': { - test: 'test', + '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: 'subtest', + subtest: subtestName, }, }, error: '', }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item assert.ok(generatedId); - assert.strictEqual(generatedId, 'subTest'); + assert.strictEqual(generatedId, '[subTest with spaces and [brackets]]'); }); test('resolveExecution handles failed tests correctly', async () => { // test specific constants used expected values @@ -346,7 +353,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // 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()); @@ -385,7 +393,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item runInstance.verify((r) => r.skipped(typemoq.It.isAny()), typemoq.Times.once()); @@ -424,7 +433,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // 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()); @@ -463,7 +473,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item runInstance.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.once()); @@ -484,7 +495,8 @@ suite('Result Resolver tests', () => { error: 'error', }; - resultResolver.resolveExecution(errorPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(errorPayload, runInstance.object, deferredTillEOT); // verify that none of these functions are called diff --git a/extensions/positron-python/src/test/testing/testController/server.unit.test.ts b/extensions/positron-python/src/test/testing/testController/server.unit.test.ts index 53c2b72e40f7..02c35e806156 100644 --- a/extensions/positron-python/src/test/testing/testController/server.unit.test.ts +++ b/extensions/positron-python/src/test/testing/testController/server.unit.test.ts @@ -6,58 +6,66 @@ import * as assert from 'assert'; import * as net from 'net'; import * as sinon from 'sinon'; import * as crypto from 'crypto'; -import { OutputChannel, Uri } from 'vscode'; import { Observable } from 'rxjs'; import * as typeMoq from 'typemoq'; +import { OutputChannel, Uri } from 'vscode'; import { IPythonExecutionFactory, IPythonExecutionService, + ObservableExecutionResult, Output, - SpawnOptions, } from '../../../client/common/process/types'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; import { ITestDebugLauncher } from '../../../client/testing/common/types'; import { Deferred, createDeferred } from '../../../client/common/utils/async'; import { MockChildProcess } from '../../mocks/mockChildProcess'; - -suite('Python Test Server', () => { - const fakeUuid = 'fake-uuid'; - - let stubExecutionFactory: IPythonExecutionFactory; - let stubExecutionService: IPythonExecutionService; +import { + PAYLOAD_MULTI_CHUNK, + PAYLOAD_SINGLE_CHUNK, + PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY, + DataWithPayloadChunks, +} from './payloadTestCases'; +import { traceLog } from '../../../client/logging'; + +const testCases = [ + { + val: () => PAYLOAD_SINGLE_CHUNK('fake-uuid'), + }, + { + val: () => PAYLOAD_MULTI_CHUNK('fake-uuid'), + }, + { + val: () => PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY('fake-uuid'), + }, +]; + +suite('Python Test Server, DataWithPayloadChunks', () => { + const FAKE_UUID = 'fake-uuid'; let server: PythonTestServer; - let sandbox: sinon.SinonSandbox; let v4Stub: sinon.SinonStub; let debugLauncher: ITestDebugLauncher; let mockProc: MockChildProcess; let execService: typeMoq.IMock; let deferred: Deferred; - let execFactory = typeMoq.Mock.ofType(); - - setup(() => { - sandbox = sinon.createSandbox(); - v4Stub = sandbox.stub(crypto, 'randomUUID'); + const sandbox = sinon.createSandbox(); - v4Stub.returns(fakeUuid); - stubExecutionService = ({ - execObservable: () => Promise.resolve({ stdout: '', stderr: '' }), - } as unknown) as IPythonExecutionService; + setup(async () => { + // set up test command options - stubExecutionFactory = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService), - } as unknown) as IPythonExecutionFactory; + v4Stub = sandbox.stub(crypto, 'randomUUID'); + v4Stub.returns(FAKE_UUID); // set up exec service with child process mockProc = new MockChildProcess('', ['']); execService = typeMoq.Mock.ofType(); - const output = new Observable>(() => { + const outputObservable = new Observable>(() => { /* no op */ }); execService .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ proc: mockProc, - out: output, + out: outputObservable, dispose: () => { /* no-body */ }, @@ -70,53 +78,164 @@ suite('Python Test Server', () => { server.dispose(); }); + testCases.forEach((testCase) => { + test(`run correctly`, async () => { + const testCaseDataObj: DataWithPayloadChunks = testCase.val(); + let eventData = ''; + const client = new net.Socket(); + + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output2 = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { + client.connect(server.getPort()); + return { + proc: mockProc, + out: output2, + dispose: () => { + /* no-body */ + }, + }; + }, + } as unknown) as IPythonExecutionService; + + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); + const uuid = server.createUUID(); + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid, + }; + + const dataWithPayloadChunks = testCaseDataObj; + + await server.serverReady(); + + server.onRunDataReceived(({ data }) => { + try { + const resultData = JSON.parse(data).result; + eventData = eventData + JSON.stringify(resultData); + } catch (e) { + assert(false, 'Error parsing data'); + } + deferred.resolve(); + }); + client.on('connect', () => { + traceLog('Socket connected, local port:', client.localPort); + // since this test is a single payload as a single chunk there should be a single line in the payload. + for (const line of dataWithPayloadChunks.payloadArray) { + client.write(line); + } + client.end(); + }); + client.on('error', (error) => { + traceLog('Socket connection error:', error); + }); + + server.sendCommand(options); + await deferred.promise; + const expectedResult = dataWithPayloadChunks.data; + assert.deepStrictEqual(eventData, expectedResult); + }); + }); +}); + +suite('Python Test Server, Send command etc', () => { + const FAKE_UUID = 'fake-uuid'; + let server: PythonTestServer; + let v4Stub: sinon.SinonStub; + let debugLauncher: ITestDebugLauncher; + let mockProc: MockChildProcess; + let execService: typeMoq.IMock; + let deferred: Deferred; + const sandbox = sinon.createSandbox(); + + setup(async () => { + // set up test command options + + v4Stub = sandbox.stub(crypto, 'randomUUID'); + v4Stub.returns(FAKE_UUID); + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + execService = typeMoq.Mock.ofType(); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + }); + + teardown(() => { + sandbox.restore(); + server.dispose(); + }); test('sendCommand should add the port to the command being sent and add the correct extra spawn variables', async () => { - const options = { - command: { - script: 'myscript', - args: ['-foo', 'foo'], - }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - const expectedSpawnOptions = { - cwd: '/foo/bar', - outputChannel: undefined, - token: undefined, - throwOnStdErr: true, - extraVariables: { - PYTHONPATH: '/foo/bar', - RUN_TEST_IDS_PORT: '56789', - }, - } as SpawnOptions; const deferred2 = createDeferred(); - execFactory = typeMoq.Mock.ofType(); + const RUN_TEST_IDS_PORT_CONST = '5678'; + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((_args, options2) => { + try { + assert.strictEqual( + options2.extraVariables.PYTHONPATH, + '/foo/bar', + 'Expect python path to exist as extra variable and be set correctly', + ); + assert.strictEqual( + options2.extraVariables.RUN_TEST_IDS_PORT, + RUN_TEST_IDS_PORT_CONST, + 'Expect test id port to be in extra variables and set correctly', + ); + assert.strictEqual( + options2.extraVariables.TEST_UUID, + FAKE_UUID, + 'Expect test uuid to be in extra variables and set correctly', + ); + assert.strictEqual( + options2.extraVariables.TEST_PORT, + 12345, + 'Expect server port to be set correctly as a env var', + ); + } catch (e) { + assert(false, 'Error parsing data, extra variables do not match'); + } + return typeMoq.Mock.ofType>().object; + }); + const execFactory = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) .returns(() => { deferred2.resolve(); return Promise.resolve(execService.object); }); - server = new PythonTestServer(execFactory.object, debugLauncher); + sinon.stub(server, 'getPort').returns(12345); + // const portServer = server.getPort(); await server.serverReady(); - - server.sendCommand(options, '56789'); + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: FAKE_UUID, + }; + server.sendCommand(options, RUN_TEST_IDS_PORT_CONST); // add in await and trigger await deferred2.promise; mockProc.trigger('close'); - const port = server.getPort(); - const expectedArgs = ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']; - execService.verify((x) => x.execObservable(expectedArgs, expectedSpawnOptions), typeMoq.Times.once()); + const expectedArgs = ['myscript', '-foo', 'foo']; + execService.verify((x) => x.execObservable(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); }); test('sendCommand should write to an output channel if it is provided as an option', async () => { - const output: string[] = []; + const output2: string[] = []; const outChannel = { appendLine: (str: string) => { - output.push(str); + output2.push(str); }, } as OutputChannel; const options = { @@ -126,11 +245,11 @@ suite('Python Test Server', () => { }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', - uuid: fakeUuid, + uuid: FAKE_UUID, outChannel, }; deferred = createDeferred(); - execFactory = typeMoq.Mock.ofType(); + const execFactory = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) .returns(() => { @@ -146,330 +265,49 @@ suite('Python Test Server', () => { await deferred.promise; mockProc.trigger('close'); - const port = server.getPort(); - const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); + const expected = ['python', 'myscript', '-foo', 'foo'].join(' '); - assert.deepStrictEqual(output, [expected]); + assert.deepStrictEqual(output2, [expected]); }); test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { let eventData: { status: string; errors: string[] } | undefined; - stubExecutionService = ({ - execObservable: () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + const stubExecutionService = typeMoq.Mock.ofType(); + stubExecutionService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + stubExecutionService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred3.resolve(); throw new Error('Failed to execute'); - }, - } as unknown) as IPythonExecutionService; + }); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', - uuid: fakeUuid, + uuid: FAKE_UUID, }; + const stubExecutionFactory = typeMoq.Mock.ofType(); + stubExecutionFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(stubExecutionService.object); + }); - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(stubExecutionFactory.object, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = JSON.parse(data); }); - await server.sendCommand(options); - + server.sendCommand(options); + await deferred2.promise; + await deferred3.promise; assert.notEqual(eventData, undefined); assert.deepStrictEqual(eventData?.status, 'error'); assert.deepStrictEqual(eventData?.errors, ['Failed to execute']); }); - - test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - - deferred = createDeferred(); - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - await server.serverReady(); - server.onDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write('malformed data'); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - // add in await and trigger - await deferred.promise; - mockProc.trigger('close'); - - assert.deepStrictEqual(eventData, ''); - }); - - test('If the server doesnt recognize the UUID it should ignore it', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - await server.serverReady(); - server.onDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write('{"Request-uuid": "unknown-uuid"}'); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - await deferred.promise; - assert.deepStrictEqual(eventData, ''); - }); - - // required to have "tests" or "results" - // the heading length not being equal and yes being equal - // multiple payloads - test('Error if payload does not have a content length header', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - await server.serverReady(); - server.onDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write('{"not content length": "5"}'); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - await deferred.promise; - assert.deepStrictEqual(eventData, ''); - }); - - const testData = [ - { - testName: 'fires discovery correctly on test payload', - payload: `Content-Length: 52 -Content-Type: application/json -Request-uuid: UUID_HERE - -{"cwd": "path", "status": "success", "tests": "xyz"}`, - expectedResult: '{"cwd": "path", "status": "success", "tests": "xyz"}', - }, - // Add more test data as needed - ]; - - testData.forEach(({ testName, payload, expectedResult }) => { - test(`test: ${testName}`, async () => { - // Your test logic here - let eventData: string | undefined; - const client = new net.Socket(); - - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - await server.serverReady(); - const uuid = server.createUUID(); - payload = payload.replace('UUID_HERE', uuid); - server.onDiscoveryDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write(payload); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - await deferred.promise; - assert.deepStrictEqual(eventData, expectedResult); - }); - }); - - test('Calls run resolver if the result header is in the payload', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - - await server.serverReady(); - const uuid = server.createUUID(); - server.onRunDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - const payload = `Content-Length: 87 -Content-Type: application/json -Request-uuid: ${uuid} - -{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}`; - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write(payload); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - await deferred.promise; - const expectedResult = - '{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}'; - assert.deepStrictEqual(eventData, expectedResult); - }); }); diff --git a/extensions/positron-python/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/extensions/positron-python/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts new file mode 100644 index 000000000000..e85cd2b62834 --- /dev/null +++ b/extensions/positron-python/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -0,0 +1,379 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { CancellationTokenSource, TestRun, 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, ITestOutputChannel } 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 { ITestServer } from '../../../client/testing/testController/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'; + +suite('Execution Flow Run Adapters', () => { + let testServer: typeMoq.IMock; + let configService: IConfigurationService; + let execFactory = typeMoq.Mock.ofType(); + let adapter: PytestTestExecutionAdapter; + let execService: typeMoq.IMock; + let deferred: Deferred; + let debugLauncher: typeMoq.IMock; + (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; + let myTestPath: string; + let mockProc: MockChildProcess; + let utilsStartServerStub: sinon.SinonStub; + + setup(() => { + testServer = typeMoq.Mock.ofType(); + testServer.setup((t) => t.getPort()).returns(() => 12345); + testServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'], unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + + // mock out the result resolver + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + execService = typeMoq.Mock.ofType(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); + execFactory = typeMoq.Mock.ofType(); + utilsStartServerStub = sinon.stub(util, 'startTestIdServer'); + debugLauncher = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + deferred = createDeferred(); + execService + .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve({ stdout: '{}' }); + }); + execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + myTestPath = path.join('/', 'my', 'test', 'path', '/'); + }); + teardown(() => { + sinon.restore(); + }); + test('PYTEST cancelation token called mid-run resolves correctly', async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + // mock exec service and exec factory + const execServiceMock = typeMoq.Mock.ofType(); + execServiceMock + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + cancellationToken.cancel(); + return { + proc: mockProc, + out: typeMoq.Mock.ofType>>().object, + dispose: () => { + /* no-body */ + }, + }; + }); + const execFactoryMock = typeMoq.Mock.ofType(); + execFactoryMock + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceMock.object)); + execFactoryMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + const deferredStartServer = createDeferred(); + utilsStartServerStub.callsFake(() => { + deferredStartServer.resolve(); + return Promise.resolve(54321); + }); + // mock EOT token & ExecClose token + const deferredEOT = createDeferred(); + const deferredExecClose = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => { + if (utilsCreateEOTStub.callCount === 1) { + return deferredEOT; + } + return deferredExecClose; + }); + // set up test server + testServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => 'uuid123'); + adapter = new PytestTestExecutionAdapter( + testServer.object, + configService, + typeMoq.Mock.ofType().object, + ); + await adapter.runTests( + Uri.file(myTestPath), + [], + false, + testRunMock.object, + execFactoryMock.object, + debugLauncher.object, + ); + // wait for server to start to keep test from failing + await deferredStartServer.promise; + + testServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); + }); + test('PYTEST cancelation token called mid-debug resolves correctly', async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + // mock exec service and exec factory + const execServiceMock = typeMoq.Mock.ofType(); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .callback((_options, callback) => { + if (callback) { + callback(); + } + }) + .returns(async () => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + const execFactoryMock = typeMoq.Mock.ofType(); + execFactoryMock + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceMock.object)); + execFactoryMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + const deferredStartServer = createDeferred(); + utilsStartServerStub.callsFake(() => { + deferredStartServer.resolve(); + return Promise.resolve(54321); + }); + // mock EOT token & ExecClose token + const deferredEOT = createDeferred(); + const deferredExecClose = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => { + if (utilsCreateEOTStub.callCount === 1) { + return deferredEOT; + } + return deferredExecClose; + }); + // set up test server + testServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => 'uuid123'); + adapter = new PytestTestExecutionAdapter( + testServer.object, + configService, + typeMoq.Mock.ofType().object, + ); + await adapter.runTests( + Uri.file(myTestPath), + [], + true, + testRunMock.object, + execFactoryMock.object, + debugLauncher.object, + ); + // wait for server to start to keep test from failing + await deferredStartServer.promise; + + testServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); + }); + test('UNITTEST cancelation token called mid-run resolves correctly', async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + + // Stub send command to then have token canceled + const stubTestServer = typeMoq.Mock.ofType(); + stubTestServer + .setup((t) => + t.sendCommand( + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + ) + .returns(() => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + + stubTestServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => 'uuid123'); + stubTestServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + // mock exec service and exec factory + const execServiceMock = typeMoq.Mock.ofType(); + execServiceMock + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + cancellationToken.cancel(); + return { + proc: mockProc, + out: typeMoq.Mock.ofType>>().object, + dispose: () => { + /* no-body */ + }, + }; + }); + const execFactoryMock = typeMoq.Mock.ofType(); + execFactoryMock + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceMock.object)); + execFactoryMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + const deferredStartServer = createDeferred(); + utilsStartServerStub.callsFake(() => { + deferredStartServer.resolve(); + return Promise.resolve(54321); + }); + // mock EOT token & ExecClose token + const deferredEOT = createDeferred(); + const deferredExecClose = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => { + if (utilsCreateEOTStub.callCount === 1) { + return deferredEOT; + } + return deferredExecClose; + }); + // set up test server + const unittestAdapter = new UnittestTestExecutionAdapter( + stubTestServer.object, + configService, + typeMoq.Mock.ofType().object, + ); + await unittestAdapter.runTests(Uri.file(myTestPath), [], false, testRunMock.object); + // wait for server to start to keep test from failing + await deferredStartServer.promise; + + stubTestServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); + }); + test('UNITTEST cancelation token called mid-debug resolves correctly', async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + + // Stub send command to then have token canceled + const stubTestServer = typeMoq.Mock.ofType(); + stubTestServer + .setup((t) => + t.sendCommand( + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + ) + .returns(() => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + + stubTestServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => 'uuid123'); + stubTestServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + // mock exec service and exec factory + const execServiceMock = typeMoq.Mock.ofType(); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async () => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + const execFactoryMock = typeMoq.Mock.ofType(); + execFactoryMock + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceMock.object)); + execFactoryMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + const deferredStartServer = createDeferred(); + utilsStartServerStub.callsFake(() => { + deferredStartServer.resolve(); + return Promise.resolve(54321); + }); + // mock EOT token & ExecClose token + const deferredEOT = createDeferred(); + const deferredExecClose = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => { + if (utilsCreateEOTStub.callCount === 1) { + return deferredEOT; + } + return deferredExecClose; + }); + // set up test server + const unittestAdapter = new UnittestTestExecutionAdapter( + stubTestServer.object, + configService, + typeMoq.Mock.ofType().object, + ); + await unittestAdapter.runTests(Uri.file(myTestPath), [], false, testRunMock.object); + // wait for server to start to keep test from failing + await deferredStartServer.promise; + + stubTestServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); + }); +}); diff --git a/extensions/positron-python/src/test/testing/testController/utils.unit.test.ts b/extensions/positron-python/src/test/testing/testController/utils.unit.test.ts index d971c7d37c9f..014261a40232 100644 --- a/extensions/positron-python/src/test/testing/testController/utils.unit.test.ts +++ b/extensions/positron-python/src/test/testing/testController/utils.unit.test.ts @@ -6,15 +6,16 @@ import { JSONRPC_CONTENT_LENGTH_HEADER, JSONRPC_CONTENT_TYPE_HEADER, JSONRPC_UUID_HEADER, - jsonRPCContent, - jsonRPCHeaders, + ExtractJsonRPCData, + parseJsonRPCHeadersAndData, + splitTestNameWithRegex, } from '../../../client/testing/testController/common/utils'; suite('Test Controller Utils: JSON RPC', () => { test('Empty raw data string', async () => { const rawDataString = ''; - const output = jsonRPCHeaders(rawDataString); + const output = parseJsonRPCHeadersAndData(rawDataString); assert.deepStrictEqual(output.headers.size, 0); assert.deepStrictEqual(output.remainingRawData, ''); }); @@ -22,20 +23,20 @@ suite('Test Controller Utils: JSON RPC', () => { test('Valid data empty JSON', async () => { const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: 2\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n{}`; - const rpcHeaders = jsonRPCHeaders(rawDataString); + const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString); assert.deepStrictEqual(rpcHeaders.headers.size, 3); assert.deepStrictEqual(rpcHeaders.remainingRawData, '{}'); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); assert.deepStrictEqual(rpcContent.extractedJSON, '{}'); }); test('Valid data NO JSON', async () => { const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: 0\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n`; - const rpcHeaders = jsonRPCHeaders(rawDataString); + const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString); assert.deepStrictEqual(rpcHeaders.headers.size, 3); assert.deepStrictEqual(rpcHeaders.remainingRawData, ''); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); assert.deepStrictEqual(rpcContent.extractedJSON, ''); }); @@ -45,10 +46,10 @@ suite('Test Controller Utils: JSON RPC', () => { '{"jsonrpc": "2.0", "method": "initialize", "params": {"processId": 1234, "rootPath": "/home/user/project", "rootUri": "file:///home/user/project", "capabilities": {}}, "id": 0}'; const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: ${json.length}\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n${json}`; - const rpcHeaders = jsonRPCHeaders(rawDataString); + const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString); assert.deepStrictEqual(rpcHeaders.headers.size, 3); assert.deepStrictEqual(rpcHeaders.remainingRawData, json); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); assert.deepStrictEqual(rpcContent.extractedJSON, json); }); @@ -58,10 +59,103 @@ suite('Test Controller Utils: JSON RPC', () => { const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: ${json.length}\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n${json}`; const rawDataString2 = rawDataString + rawDataString; - const rpcHeaders = jsonRPCHeaders(rawDataString2); + const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString2); assert.deepStrictEqual(rpcHeaders.headers.size, 3); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); assert.deepStrictEqual(rpcContent.extractedJSON, json); assert.deepStrictEqual(rpcContent.remainingRawData, rawDataString); }); + + test('Valid constant', async () => { + const data = `{"cwd": "/Users/eleanorboyd/testingFiles/inc_dec_example", "status": "success", "result": {"test_dup_class.test_a.TestSomething.test_a": {"test": "test_dup_class.test_a.TestSomething.test_a", "outcome": "success", "message": "None", "traceback": null, "subtest": null}}}`; + const secondPayload = `Content-Length: 270 +Content-Type: application/json +Request-uuid: 496c86b1-608f-4886-9436-ec00538e144c + +${data}`; + const payload = `Content-Length: 270 +Content-Type: application/json +Request-uuid: 496c86b1-608f-4886-9436-ec00538e144c + +${data}${secondPayload}`; + + const rpcHeaders = parseJsonRPCHeadersAndData(payload); + assert.deepStrictEqual(rpcHeaders.headers.size, 3); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); + assert.deepStrictEqual(rpcContent.extractedJSON, data); + assert.deepStrictEqual(rpcContent.remainingRawData, secondPayload); + }); + test('Valid content length as only header with carriage return', async () => { + const payload = `Content-Length: 7 + `; + + const rpcHeaders = parseJsonRPCHeadersAndData(payload); + assert.deepStrictEqual(rpcHeaders.headers.size, 1); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); + assert.deepStrictEqual(rpcContent.extractedJSON, ''); + assert.deepStrictEqual(rpcContent.remainingRawData, ''); + }); + test('Valid content length header with no value', async () => { + const payload = `Content-Length:`; + + const rpcHeaders = parseJsonRPCHeadersAndData(payload); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); + assert.deepStrictEqual(rpcContent.extractedJSON, ''); + assert.deepStrictEqual(rpcContent.remainingRawData, ''); + }); + + suite('Test Controller Utils: Other', () => { + interface TestCase { + name: string; + input: string; + expectedParent: string; + expectedSubtest: string; + } + + const testCases: Array = [ + { + name: 'Single parameter, named', + input: 'test_package.ClassName.test_method (param=value)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param=value)', + }, + { + name: 'Single parameter, unnamed', + input: 'test_package.ClassName.test_method [value]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '[value]', + }, + { + name: 'Multiple parameters, named', + input: 'test_package.ClassName.test_method (param1=value1, param2=value2)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param1=value1, param2=value2)', + }, + { + name: 'Multiple parameters, unnamed', + input: 'test_package.ClassName.test_method [value1, value2]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '[value1, value2]', + }, + { + name: 'Names with special characters', + input: 'test_package.ClassName.test_method (param1=value/1, param2=value+2)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param1=value/1, param2=value+2)', + }, + { + name: 'Names with spaces', + input: 'test_package.ClassName.test_method ["a b c d"]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '["a b c d"]', + }, + ]; + + testCases.forEach((testCase) => { + test(`splitTestNameWithRegex: ${testCase.name}`, () => { + const splitResult = splitTestNameWithRegex(testCase.input); + assert.deepStrictEqual(splitResult, [testCase.expectedParent, testCase.expectedSubtest]); + }); + }); + }); }); diff --git a/extensions/positron-python/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py b/extensions/positron-python/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py new file mode 100644 index 000000000000..5aac911b575a --- /dev/null +++ b/extensions/positron-python/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/extensions/positron-python/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py b/extensions/positron-python/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py new file mode 100644 index 000000000000..bad7ff8fcbbd --- /dev/null +++ b/extensions/positron-python/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +import ctypes + + +class TestSegmentationFault(unittest.TestCase): + def cause_segfault(self): + ctypes.string_at(0) # Dereference a NULL pointer + + def test_segfault(self): + assert True + self.cause_segfault() + + +if __name__ == "__main__": + unittest.main() diff --git a/extensions/positron-python/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py b/extensions/positron-python/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py index 3e84df0a2d9f..a76856ebb929 100644 --- a/extensions/positron-python/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py +++ b/extensions/positron-python/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py @@ -4,13 +4,13 @@ import unittest -@pytest.mark.parametrize("num", range(0, 200)) +@pytest.mark.parametrize("num", range(0, 2000)) def test_odd_even(num): - return num % 2 == 0 + assert num % 2 == 0 class NumbersTest(unittest.TestCase): def test_even(self): - for i in range(0, 200): + for i in range(0, 2000): with self.subTest(i=i): self.assertEqual(i % 2, 0) diff --git a/extensions/positron-python/yarn.lock b/extensions/positron-python/yarn.lock index e414f152c901..c724a723ea2e 100644 --- a/extensions/positron-python/yarn.lock +++ b/extensions/positron-python/yarn.lock @@ -25,7 +25,16 @@ "@azure/abort-controller" "^1.0.0" tslib "^2.2.0" -"@azure/core-rest-pipeline@^1.10.0": +"@azure/core-auth@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.5.0.tgz#a41848c5c31cb3b7c84c409885267d55a2c92e44" + integrity sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw== + dependencies: + "@azure/abort-controller" "^1.0.0" + "@azure/core-util" "^1.1.0" + tslib "^2.2.0" + +"@azure/core-rest-pipeline@1.10.1": version "1.10.1" resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.1.tgz#348290847ca31b9eecf9cf5de7519aaccdd30968" integrity sha512-Kji9k6TOFRDB5ZMTw8qUf2IJ+CeJtsuMdAHox9eqpTf1cefiNMpzrfnF6sINEBZJsaVaWgQ0o48B6kcUH68niA== @@ -41,13 +50,21 @@ tslib "^2.2.0" uuid "^8.3.0" -"@azure/core-tracing@^1.0.1": +"@azure/core-tracing@^1.0.0", "@azure/core-tracing@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@azure/core-tracing/-/core-tracing-1.0.1.tgz#352a38cbea438c4a83c86b314f48017d70ba9503" integrity sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw== dependencies: tslib "^2.2.0" +"@azure/core-util@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.2.0.tgz#3499deba1fc36dda6f1912b791809b6f15d4a392" + integrity sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng== + dependencies: + "@azure/abort-controller" "^1.0.0" + tslib "^2.2.0" + "@azure/core-util@^1.0.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.1.1.tgz#8f87b3dd468795df0f0849d9f096c3e7b29452c1" @@ -56,6 +73,14 @@ "@azure/abort-controller" "^1.0.0" tslib "^2.2.0" +"@azure/core-util@^1.1.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.5.0.tgz#ffe49c3e867044da67daeb8122143fa065e1eb0e" + integrity sha512-GZBpVFDtQ/15hW1OgBcRdT4Bl7AEpcEZqLfbAvOtm1CQUncKWiYapFHVD588hmlV27NbOOtSm3cnLF3lvoHi4g== + dependencies: + "@azure/abort-controller" "^1.0.0" + tslib "^2.2.0" + "@azure/logger@^1.0.0": version "1.0.3" resolved "https://registry.yarnpkg.com/@azure/logger/-/logger-1.0.3.tgz#6e36704aa51be7d4a1bae24731ea580836293c96" @@ -63,6 +88,18 @@ dependencies: tslib "^2.2.0" +"@azure/opentelemetry-instrumentation-azure-sdk@^1.0.0-beta.5": + version "1.0.0-beta.5" + resolved "https://registry.yarnpkg.com/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0-beta.5.tgz#78809e6c005d08450701e5d37f087f6fce2f86eb" + 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" + "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" @@ -382,66 +419,88 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@microsoft/1ds-core-js@3.2.11", "@microsoft/1ds-core-js@^3.2.9": - version "3.2.11" - resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-3.2.11.tgz#0a3857562a9bde7c15a2fa141c647f40aabd52e2" - integrity sha512-UuqZ1iWjaEFsBsnB+J0Q4IbgJdvJAq3LcNArAdMQQq9+wBWpjyGG4yu9gL6fS5AKfpF6yy73mtgWD7WzvphMLQ== +"@microsoft/1ds-core-js@3.2.14", "@microsoft/1ds-core-js@^3.2.13": + version "3.2.14" + resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-3.2.14.tgz#407c83999b0f75d9939bb32f6fc10b1439938cef" + integrity sha512-UW1YrUTPuvXmAarzqVSeYU1vwTzxCCZdd+ANcf/SDkxm53mCCgxSodVP9yrxtGOQdKhE9Y3rZdETRSvBBdKdXg== dependencies: - "@microsoft/applicationinsights-core-js" "2.8.13" + "@microsoft/applicationinsights-core-js" "2.8.16" "@microsoft/applicationinsights-shims" "^2.0.2" "@microsoft/dynamicproto-js" "^1.1.7" -"@microsoft/1ds-post-js@^3.2.9": - version "3.2.11" - resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-3.2.11.tgz#bbfc935bafb43982ccaf94ebd12f1fabf5ef8ebe" - integrity sha512-86prrN8cKjfpqfeyC4k0rFqg7fJDkTwPeKLHnkUgfq9mKVzu+BdtlEzaJtc56MroKWNYPkitZ/PWSwwpTPIgMA== +"@microsoft/1ds-post-js@^3.2.13": + version "3.2.14" + resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-3.2.14.tgz#1ebd873a47eab9d97fa0cff593de303f641cdb51" + integrity sha512-pXTfyW8SI6LS2r92W2Zaxl/YjJCHTEX9ej/GdabtlMAikGGgAlEsMlk5a4SJ3BI3K8sDUSGi+oTmJ1FtVes+bQ== dependencies: - "@microsoft/1ds-core-js" "3.2.11" + "@microsoft/1ds-core-js" "3.2.14" "@microsoft/applicationinsights-shims" "^2.0.2" "@microsoft/dynamicproto-js" "^1.1.7" -"@microsoft/applicationinsights-channel-js@2.8.13": - version "2.8.13" - resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.8.13.tgz#bd117cc7b00ec929e74a6555cb7462ab4fccdf62" - integrity sha512-zc2BSsHk4HAqK5STdNzKGV817jKNbiTZPYpNt3zuE+jO5druJgloqrvclUfLnCoa7zwrQ2UxoAXlpJGmroGZPA== +"@microsoft/applicationinsights-channel-js@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.3.tgz#24adc0fd292321a3046b315831e66b7e87d5380d" + integrity sha512-CWcJQCMKU3CxWLFIE8iPa3G5KB3v2RFkAvPICfY8/fwWZq4tWY7zosgvRPDZ+dIkz8Z/+CMy0+KblYzIKDdG4A== dependencies: - "@microsoft/applicationinsights-common" "2.8.13" - "@microsoft/applicationinsights-core-js" "2.8.13" - "@microsoft/applicationinsights-shims" "2.0.2" - "@microsoft/dynamicproto-js" "^1.1.9" + "@microsoft/applicationinsights-common" "3.0.3" + "@microsoft/applicationinsights-core-js" "3.0.3" + "@microsoft/applicationinsights-shims" "3.0.1" + "@microsoft/dynamicproto-js" "^2.0.2" + "@nevware21/ts-async" ">= 0.3.0 < 2.x" + "@nevware21/ts-utils" ">= 0.10.1 < 2.x" -"@microsoft/applicationinsights-common@2.8.13": - version "2.8.13" - resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-common/-/applicationinsights-common-2.8.13.tgz#e3457bdc54a61cbfa481494da25e55bb9156096e" - integrity sha512-UYLLGVtuzrWUEmGYRroMzLyTi2fHqL6SwJUlmVWPJrmdK43PGpviRix/sBW0Qs+6qjiI1Z6CiG4Xah6w/HylhA== +"@microsoft/applicationinsights-common@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.3.tgz#5b2267c02c027e9ff9e07ca8c49741bf139ddf4e" + integrity sha512-GwnerHHPexry2CMTr6gP2Rjm0e3rgVXZzFCbrxcoOQk8MqrEFDWur6Xs66FwXpGFnY3KV3Zsujkfcl0oePs4Cg== dependencies: - "@microsoft/applicationinsights-core-js" "2.8.13" - "@microsoft/applicationinsights-shims" "2.0.2" - "@microsoft/dynamicproto-js" "^1.1.9" + "@microsoft/applicationinsights-core-js" "3.0.3" + "@microsoft/applicationinsights-shims" "3.0.1" + "@microsoft/dynamicproto-js" "^2.0.2" + "@nevware21/ts-utils" ">= 0.10.1 < 2.x" -"@microsoft/applicationinsights-core-js@2.8.13": - version "2.8.13" - resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.13.tgz#45c2b8fff35e5aa519355dd3a69e3758293b08f4" - integrity sha512-PP7Xjplvy0d5G2Tk7DcSDYRmgDYRv+7n3wEiqgm63DrSoa8rEuoODavjWunhX058zPNIeKbus59NE+DusLLyZg== +"@microsoft/applicationinsights-core-js@2.8.16": + version "2.8.16" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.16.tgz#b209c908a63128b4603d00d30357d646bb7da8d3" + integrity sha512-pO5rR6UuiPymiHFj8XxNXhQgBSTvyHWygf+gdEVDh0xpUXYFO99bZe0Ux0D0HqYqVkJrRfXzL1Ocru6+S0x53Q== dependencies: "@microsoft/applicationinsights-shims" "2.0.2" "@microsoft/dynamicproto-js" "^1.1.9" +"@microsoft/applicationinsights-core-js@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.3.tgz#8904e1a1bb023f71c37c20eac27b106f71d76ae8" + integrity sha512-ymtdPHgUhCwIwQZx2ZN3Xw3cq+Z5KHzGmFV8QvURSdUzfaHbjYcHXIQkEZbgCCGOTMLtx9lZqP7J1gbBy0O8GQ== + dependencies: + "@microsoft/applicationinsights-shims" "3.0.1" + "@microsoft/dynamicproto-js" "^2.0.2" + "@nevware21/ts-async" ">= 0.3.0 < 2.x" + "@nevware21/ts-utils" ">= 0.10.1 < 2.x" + "@microsoft/applicationinsights-shims@2.0.2", "@microsoft/applicationinsights-shims@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-2.0.2.tgz#92b36a09375e2d9cb2b4203383b05772be837085" integrity sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg== -"@microsoft/applicationinsights-web-basic@^2.8.11": - version "2.8.13" - resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-2.8.13.tgz#a3934fa7b7f221d09fbf403fcfeb06a8c18ca246" - integrity sha512-DgPx1ryZucLWc285qkAEBG6LCcTSG6Gdb4u4yAlmAW0G+Qau49GoJnfJGR+cDXvyXSwHcH0dsainqzeYYY1K7A== +"@microsoft/applicationinsights-shims@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz#3865b73ace8405b9c4618cc5c571f2fe3876f06f" + integrity sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg== dependencies: - "@microsoft/applicationinsights-channel-js" "2.8.13" - "@microsoft/applicationinsights-common" "2.8.13" - "@microsoft/applicationinsights-core-js" "2.8.13" - "@microsoft/applicationinsights-shims" "2.0.2" - "@microsoft/dynamicproto-js" "^1.1.9" + "@nevware21/ts-utils" ">= 0.9.4 < 2.x" + +"@microsoft/applicationinsights-web-basic@^3.0.2": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.3.tgz#f2c136dfda31a0f22ca06b417e750de8c7c064cb" + integrity sha512-PCKfbCTJUbcyZnRzasxlYgV3L1ORVxpggRB1Uohx0rNyEyKPwk7lcCcFos11NEA+pn/XqZrZY5FYSdaLT45wNA== + dependencies: + "@microsoft/applicationinsights-channel-js" "3.0.3" + "@microsoft/applicationinsights-common" "3.0.3" + "@microsoft/applicationinsights-core-js" "3.0.3" + "@microsoft/applicationinsights-shims" "3.0.1" + "@microsoft/dynamicproto-js" "^2.0.2" + "@nevware21/ts-async" ">= 0.3.0 < 2.x" + "@nevware21/ts-utils" ">= 0.10.1 < 2.x" "@microsoft/applicationinsights-web-snippet@^1.0.1": version "1.0.1" @@ -458,6 +517,25 @@ resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz#7437db7aa061162ee94e4131b69a62b8dad5dea6" integrity sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ== +"@microsoft/dynamicproto-js@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz#e57fbec2e7067d48b7e8e1e1c1d354028ef718a6" + integrity sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg== + dependencies: + "@nevware21/ts-utils" ">= 0.9.4 < 2.x" + +"@nevware21/ts-async@>= 0.3.0 < 2.x": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@nevware21/ts-async/-/ts-async-0.3.0.tgz#a8b97ba01065fc930de9a3f4dd4a05e862becc6c" + integrity sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA== + dependencies: + "@nevware21/ts-utils" ">= 0.10.0 < 2.x" + +"@nevware21/ts-utils@>= 0.10.0 < 2.x", "@nevware21/ts-utils@>= 0.10.1 < 2.x", "@nevware21/ts-utils@>= 0.9.4 < 2.x": + version "0.10.1" + resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz#aa65abc71eba06749a396598f22263d26f796ac7" + integrity sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -479,39 +557,50 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@opentelemetry/api@^1.0.4": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.4.0.tgz#2c91791a9ba6ca0a0f4aaac5e45d58df13639ac8" - integrity sha512-IgMK9i3sFGNUqPMbjABm0G26g0QCKCUBfglhQ7rQq6WcxbKfEHRcmwsoER4hZcuYqJgkYn2OeuoJIv7Jsftp7g== +"@opentelemetry/api@^1.4.1": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.6.0.tgz#de2c6823203d6f319511898bb5de7e70f5267e19" + integrity sha512-OWlrQAnWn9577PhVgqjUvMr1pg57Bc4jv0iL4w0PRuOSRvq67rvHW9Ie/dZVMvCzhSCB+UxhcY/PmCmFj33Q+g== + +"@opentelemetry/core@1.17.0", "@opentelemetry/core@^1.15.2": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.17.0.tgz#6a72425f5f953dc68b4c7c66d947c018173d30d2" + integrity sha512-tfnl3h+UefCgx1aeN2xtrmr6BmdWGKXypk0pflQR0urFS40aE88trnkOMc2HTJZbMrqEEl4HsaBeFhwLVXsrJg== + dependencies: + "@opentelemetry/semantic-conventions" "1.17.0" -"@opentelemetry/core@1.9.1", "@opentelemetry/core@^1.0.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.9.1.tgz#e343337e1a7bf30e9a6aef3ef659b9b76379762a" - integrity sha512-6/qon6tw2I8ZaJnHAQUUn4BqhTbTNRS0WP8/bA0ynaX+Uzp/DDbd0NS0Cq6TMlh8+mrlsyqDE7mO50nmv2Yvlg== +"@opentelemetry/instrumentation@^0.41.2": + version "0.41.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.41.2.tgz#cae11fa64485dcf03dae331f35b315b64bc6189f" + integrity sha512-rxU72E0pKNH6ae2w5+xgVYZLzc5mlxAbGzF4shxMVK8YC2QQsfN38B2GPbj0jvrKWWNUElfclQ+YTykkNg/grw== dependencies: - "@opentelemetry/semantic-conventions" "1.9.1" + "@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@1.9.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.9.1.tgz#5ad3d80ba968a3a0e56498ce4bc82a6a01f2682f" - integrity sha512-VqBGbnAfubI+l+yrtYxeLyOoL358JK57btPMJDd3TCOV3mV5TNBmzvOfmesM4NeTyXuGJByd3XvOHvFezLn3rQ== +"@opentelemetry/resources@1.17.0": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.17.0.tgz#ee29144cfd7d194c69698c8153dbadec7fe6819f" + integrity sha512-+u0ciVnj8lhuL/qGRBPeVYvk7fL+H/vOddfvmOeJaA1KC+5/3UED1c9KoZQlRsNT5Kw1FaK8LkY2NVLYfOVZQw== dependencies: - "@opentelemetry/core" "1.9.1" - "@opentelemetry/semantic-conventions" "1.9.1" + "@opentelemetry/core" "1.17.0" + "@opentelemetry/semantic-conventions" "1.17.0" -"@opentelemetry/sdk-trace-base@^1.0.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.9.1.tgz#c349491b432a7e0ae7316f0b48b2d454d79d2b84" - integrity sha512-Y9gC5M1efhDLYHeeo2MWcDDMmR40z6QpqcWnPCm4Dmh+RHAMf4dnEBBntIe1dDpor686kyU6JV1D29ih1lZpsQ== +"@opentelemetry/sdk-trace-base@^1.15.2": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.17.0.tgz#05a21763c9efa72903c20b8930293cdde344b681" + integrity sha512-2T5HA1/1iE36Q9eg6D4zYlC4Y4GcycI1J6NsHPKZY9oWfAxWsoYnRlkPfUqyY5XVtocCo/xHpnJvGNHwzT70oQ== dependencies: - "@opentelemetry/core" "1.9.1" - "@opentelemetry/resources" "1.9.1" - "@opentelemetry/semantic-conventions" "1.9.1" + "@opentelemetry/core" "1.17.0" + "@opentelemetry/resources" "1.17.0" + "@opentelemetry/semantic-conventions" "1.17.0" -"@opentelemetry/semantic-conventions@1.9.1", "@opentelemetry/semantic-conventions@^1.0.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.9.1.tgz#ad3367684a57879392513479e0a436cb2ac46dad" - integrity sha512-oPQdbFDmZvjXk5ZDoBGXG8B4tSB/qW5vQunJWQMFUBp7Xe8O1ByPANueJ+Jzg58esEBegyyxZ7LRmfJr7kFcFg== +"@opentelemetry/semantic-conventions@1.17.0", "@opentelemetry/semantic-conventions@^1.15.2": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.17.0.tgz#af10baa9f05ce1e64a14065fc138b5739bfb65f6" + integrity sha512-+fguCd2d8d2qruk0H0DsCEy2CTK3t0Tugg7MhZ/UQMvmewbZLNnJ6heSYyzIZWG5IPfAXzoj4f4F/qpM7l4VBA== "@polka/url@^1.0.0-next.20": version "1.0.0-next.21" @@ -739,16 +828,21 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850" integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg== -"@types/node@^16.17.0": - version "16.18.25" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.25.tgz#8863940fefa1234d3fcac7a4b7a48a6c992d67af" - integrity sha512-rUDO6s9Q/El1R1I21HG4qw/LstTHCPO/oQNAwI/4b2f9EWvMnqt4d3HJwPMawfZ3UvodB8516Yg+VAq54YM+eA== +"@types/node@^18.17.1": + version "18.18.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.4.tgz#519fef47a13cf869be290c20fc6ae9b7fe887aa7" + integrity sha512-t3rNFBgJRugIhackit2mVcLfF6IRc0JE4oeizPQL8Zrm8n2WY/0wOdpOPhdtG0V9Q2TlW/axbF1MJ6z+Yj/kKQ== "@types/semver@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ== +"@types/shimmer@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.0.3.tgz#2259274874392a4413fb6f6717a7ec209a8651a7" + integrity sha512-F/IjUGnV6pIN7R4ZV4npHJVoNtaLZWvb+2/9gctxjb99wkpI7Ozg8VPogwDiTRyjLwZXAYxjvdg1KS8LTHKdDA== + "@types/shortid@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/shortid/-/shortid-0.0.29.tgz#8093ee0416a6e2bf2aa6338109114b3fbffa0e9b" @@ -873,15 +967,15 @@ resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== -"@vscode/extension-telemetry@^0.7.7": - version "0.7.7" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.7.7.tgz#8213bfbdb1afa216befb146d6563ec5d200d7608" - integrity sha512-uW508BPjkWDBOKvvvSym3ZmGb7kHIiWaAfB/1PHzLz2x9TrC33CfjmFEI+CywIL/jBv4bqZxxjN4tfefB61F+g== +"@vscode/extension-telemetry@^0.8.4": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.5.tgz#3db305be907c01656160e25d91f5d2840175d199" + integrity sha512-YFKANBT2F3qdWQstjcr40XX8BLsdKlKM7a7YPi/jNuMjuiPhb1Jn7YsDR3WZaVEzAqeqGy4gzXsFCBbuZ+L1Tg== dependencies: - "@microsoft/1ds-core-js" "^3.2.9" - "@microsoft/1ds-post-js" "^3.2.9" - "@microsoft/applicationinsights-web-basic" "^2.8.11" - applicationinsights "2.5.0" + "@microsoft/1ds-core-js" "^3.2.13" + "@microsoft/1ds-post-js" "^3.2.13" + "@microsoft/applicationinsights-web-basic" "^3.0.2" + applicationinsights "^2.7.1" "@vscode/jupyter-lsp-middleware@^0.2.50": version "0.2.50" @@ -904,15 +998,15 @@ vscode-languageserver-protocol "^3.17.3-next.1" vscode-uri "^3.0.2" -"@vscode/test-electron@^2.1.3": - version "2.2.3" - resolved "https://registry.yarnpkg.com/@vscode/test-electron/-/test-electron-2.2.3.tgz#bf6a77542970b5d34561d0671259900632e5eb94" - integrity sha512-7DmdGYQTqRNaLHKG3j56buc9DkstriY4aV0S3Zj32u0U9/T0L8vwWAC9QGCh1meu1VXDEla1ze27TkqysHGP0Q== +"@vscode/test-electron@^2.3.4": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@vscode/test-electron/-/test-electron-2.3.5.tgz#c472c5bdce1329aeb4762b8aa7a2cbe7aa783aac" + integrity sha512-lAW7nQ0HuPqJnGJrtCzEKZCICtRizeP6qNanyCrjmdCOAAWjX3ixiG8RVPwqsYPQBWLPgYuE12qQlwXsOR/2fQ== dependencies: http-proxy-agent "^4.0.1" https-proxy-agent "^5.0.0" - rimraf "^3.0.2" - unzipper "^0.10.11" + jszip "^3.10.1" + semver "^7.5.2" "@vscode/vsce@^2.18.0": version "2.19.0" @@ -1095,6 +1189,11 @@ acorn-import-assertions@^1.7.6: resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== +acorn-import-assertions@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== + acorn-jsx@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -1115,6 +1214,11 @@ acorn@^8.0.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== +acorn@^8.8.2: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -1243,22 +1347,24 @@ append-transform@^2.0.0: dependencies: default-require-extensions "^3.0.0" -applicationinsights@2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-2.5.0.tgz#f008580b2f68267a5d233cce4e1f50b587bdf3c4" - integrity sha512-6kIFmpANRok+6FhCOmO7ZZ/mh7fdNKn17BaT13cg/RV5roLPJlA6q8srWexayHd3MPcwMb9072e8Zp0P47s/pw== +applicationinsights@^2.7.1: + version "2.8.0" + resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-2.8.0.tgz#7a294c9c173e04bf4813e263055afe5763623f33" + integrity sha512-pxVOdCPwXTal1A904yGmzOOUJrIeQ89xQir0ifr7fLl+e0BlGrZ1P4StcIDuEXk93gV9CGxGm5Mol8ksPk2mcg== dependencies: - "@azure/core-auth" "^1.4.0" - "@azure/core-rest-pipeline" "^1.10.0" + "@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.0.4" - "@opentelemetry/core" "^1.0.1" - "@opentelemetry/sdk-trace-base" "^1.0.1" - "@opentelemetry/semantic-conventions" "^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.0" - diagnostic-channel-publishers "1.0.5" + diagnostic-channel "1.1.1" + diagnostic-channel-publishers "1.0.7" arch@^2.1.0: version "2.2.0" @@ -1610,11 +1716,6 @@ bent@^7.3.12: caseless "~0.12.0" is-stream "^2.0.0" -big-integer@^1.6.17: - version "1.6.51" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" - integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== - big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -1630,14 +1731,6 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -binary@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" - integrity sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg== - dependencies: - buffers "~0.1.1" - chainsaw "~0.1.0" - bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -1662,11 +1755,6 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" -bluebird@~3.4.1: - version "3.4.7" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" - integrity sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA== - bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" @@ -1834,11 +1922,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer-indexof-polyfill@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c" - integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A== - buffer-xor@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" @@ -1869,11 +1952,6 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -buffers@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" - integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ== - builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -1985,13 +2063,6 @@ chai@^4.1.2: pathval "^1.1.1" type-detect "^4.0.5" -chainsaw@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" - integrity sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ== - dependencies: - traverse ">=0.3.0 <0.4" - chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -2101,6 +2172,11 @@ circular-json@^0.3.1: resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A== +cjs-module-lexer@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" + integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -2725,17 +2801,17 @@ detect-libc@^2.0.0: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== -diagnostic-channel-publishers@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.5.tgz#df8c317086c50f5727fdfb5d2fce214d2e4130ae" - integrity sha512-dJwUS0915pkjjimPJVDnS/QQHsH0aOYhnZsLJdnZIMOrB+csj8RnZhWTuwnm8R5v3Z7OZs+ksv5luC14DGB7eg== +diagnostic-channel-publishers@1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.7.tgz#9b7f8d5ee1295481aee19c827d917e96fedf2c4a" + integrity sha512-SEECbY5AiVt6DfLkhkaHNeshg1CogdLLANA8xlG/TKvS+XUgvIKl7VspJGYiEdL5OUyzMVnr7o0AwB7f+/Mjtg== -diagnostic-channel@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-1.1.0.tgz#6985e9dfedfbc072d91dc4388477e4087147756e" - integrity sha512-fwujyMe1gj6rk6dYi9hMZm0c8Mz8NDMVl2LB4iaYh3+LIAThZC8RKFGXWG0IML2OxAit/ZFRgZhMkhQ3d/bobQ== +diagnostic-channel@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-1.1.1.tgz#44b60972de9ee055c16216535b0e9db3f6a0efd0" + integrity sha512-r2HV5qFkUICyoaKlBEpLKHjxMXATUf/l+h8UZPGBHGLy4DDiY2sOLcIctax4eRnTw5wH2jTMExLntGPJ8eOJxw== dependencies: - semver "^5.3.0" + semver "^7.5.3" diff-match-patch@^1.0.0: version "1.0.5" @@ -2844,13 +2920,6 @@ download@^8.0.0: p-event "^2.1.0" pify "^4.0.1" -duplexer2@~0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" - integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA== - dependencies: - readable-stream "^2.0.2" - duplexer3@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" @@ -3793,16 +3862,6 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== -fstream@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" - integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== - dependencies: - graceful-fs "^4.1.2" - inherits "~2.0.0" - mkdirp ">=0.5 0" - rimraf "2" - function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -4073,7 +4132,7 @@ got@^8.3.1: url-parse-lax "^3.0.0" url-to-options "^1.0.1" -graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== @@ -4363,6 +4422,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -4371,6 +4435,16 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" +import-in-the-middle@1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz#2a266676e3495e72c04bbaa5ec14756ba168391b" + 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" + import-local@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" @@ -4397,7 +4471,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -5039,6 +5113,16 @@ jsonfile@^6.0.1: array-includes "^3.1.5" object.assign "^4.1.3" +jszip@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" + integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + setimmediate "^1.0.5" + just-debounce@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.1.0.tgz#2f81a3ad4121a76bc7cb45dbf704c0d76a8e5ddf" @@ -5142,6 +5226,13 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + liftoff@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3" @@ -5163,11 +5254,6 @@ linkify-it@^3.0.1: dependencies: uc.micro "^1.0.1" -listenercount@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" - integrity sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ== - load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -5514,7 +5600,7 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.6: +mkdirp@^0.5.0, mkdirp@^0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -5575,6 +5661,11 @@ mocha@^9.2.2: yargs-parser "20.2.4" yargs-unparser "2.0.0" +module-details-from-path@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" + integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A== + mrmime@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" @@ -6155,7 +6246,7 @@ package-hash@^4.0.0: lodash.flattendeep "^4.4.0" release-zalgo "^1.0.0" -pako@~1.0.5: +pako@~1.0.2, pako@~1.0.5: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== @@ -6768,6 +6859,15 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +require-in-the-middle@^7.1.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz#b539de8f00955444dc8aed95e17c69b0a4f10fcf" + integrity sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw== + dependencies: + debug "^4.1.1" + module-details-from-path "^1.0.3" + resolve "^1.22.1" + require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" @@ -6864,13 +6964,6 @@ rewiremock@^3.13.0: wipe-node-cache "^2.1.2" wipe-webpack-cache "^2.1.0" -rimraf@2: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -6981,7 +7074,7 @@ semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: dependencies: lru-cache "^6.0.0" -semver@^7.3.7, semver@^7.5.2: +semver@^7.3.7, semver@^7.5.1, semver@^7.5.2, semver@^7.5.3: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -7017,7 +7110,7 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" -setimmediate@^1.0.4, setimmediate@~1.0.4: +setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== @@ -7061,7 +7154,7 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shimmer@^1.1.0, shimmer@^1.2.0: +shimmer@^1.1.0, shimmer@^1.2.0, shimmer@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== @@ -7735,11 +7828,6 @@ totalist@^1.0.0: resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== -"traverse@>=0.3.0 <0.4": - version "0.3.9" - resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" - integrity sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ== - trim-repeated@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" @@ -8017,22 +8105,6 @@ untildify@^4.0.0: resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== -unzipper@^0.10.11: - version "0.10.11" - resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.11.tgz#0b4991446472cbdb92ee7403909f26c2419c782e" - integrity sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw== - dependencies: - big-integer "^1.6.17" - binary "~0.3.0" - bluebird "~3.4.1" - buffer-indexof-polyfill "~1.0.0" - duplexer2 "~0.1.4" - fstream "^1.0.12" - graceful-fs "^4.2.2" - listenercount "~1.0.1" - readable-stream "~2.3.6" - setimmediate "~1.0.4" - upath@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"