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