From c67a9c7dde591ec32e4794ec07a02d32b1da5031 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 10 Oct 2023 16:48:15 +0000 Subject: [PATCH] Merged PR posit-dev/positron-python#224: Merge/2023.18 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge pull request #224 from posit-dev/merge/2023.18 Merge/2023.18 -------------------- Commit message for posit-dev/positron-python@2b2e3424b2f795dec25b97380838eca053c03b07: Skip failing flaky test, it needs to be re-written -------------------- Commit message for posit-dev/positron-python@1c66d2d5c5996a7ff91116af423e0ed25b54ece0: Complete merge of upstream microsoft/vscode-python extension 2023.18 Remove old linter and formatter prompts and commands manually to match upstream (there was a conflict in prior merge step). Adjust to more specific nodeJS.Timeout type to allow module to compile with VSCode 1.83 typescript settings. Upstream Notes: https://github.com/microsoft/vscode-python/releases/tag/v2023.18.0 -------------------- Commit message for posit-dev/positron-python@e2bdd8fda645a17fa57c673bf17df76cc3a29618: Merge commit 'ab6ab06e60b26109fe22843ea1aa46e918864e10' -------------------- Commit message for microsoft/vscode-python@ab6ab06e60b26109fe22843ea1aa46e918864e10: Use python 3.12-dev (microsoft/vscode-python#22043) -------------------- Commit message for microsoft/vscode-python@ae427391c9058f49fecfd5b8a20511624d2bb262: Remove unsupported command from readme (microsoft/vscode-python#22153) -------------------- Commit message for microsoft/vscode-python@ff0d4df88c9aa612853b5bd43cce65440a7ce0ec: handle key error pytest (microsoft/vscode-python#22151) fixes https://github.com/microsoft/vscode-python/issues/22149 -------------------- Commit message for microsoft/vscode-python@a3633810b5647008c1b89ea3c6f2d466139909ba: switch to using envvars for port and uuid in unittest (microsoft/vscode-python#22131) closes https://github.com/microsoft/vscode-python/issues/22130 -------------------- Commit message for microsoft/vscode-python@fc62bd8d9a2431eb6199decf58c88653de3f9a37: Migrate extension to node 18 (microsoft/vscode-python#22135) -------------------- Commit message for microsoft/vscode-python@add82a0a773d5f38e294852d15a3a2eb9f90c1cc: Bump Jedi to 0.19.1 for Python 3.12 support (microsoft/vscode-python#22132) Follows from https://github.com/microsoft/vscode-python/issues/22011#issuecomment-1742682966 -------------------- Commit message for microsoft/vscode-python@590c12a1a5150490d32fbe0b468a11a7df1daec7: switch end to end tests to randomized substring (microsoft/vscode-python#22114) add in tests which are randomized to provide more testing for the issue that created `https://github.com/microsoft/vscode-python/issues/22104` -------------------- Commit message for microsoft/vscode-python@4f82418173be3b989e07fe2cbb076b006ad8fc83: Update version for pre-release (microsoft/vscode-python#22129) -------------------- Commit message for microsoft/vscode-python@4a3f855c292384fc14c157961b46be8b35db5b7b: Bump packaging from 23.1 to 23.2 (microsoft/vscode-python#22124) Bumps [packaging](https://github.com/pypa/packaging) from 23.1 to 23.2.
Release notes

Sourced from packaging's releases.

23.2

What's Changed

New Contributors

Full Changelog: https://github.com/pypa/packaging/compare/23.1...23.2

Changelog

Sourced from packaging's changelog.

23.2 - 2023-10-01


* Document calendar-based versioning scheme (:issue:`716`)
* Enforce that the entire marker string is parsed (:issue:`687`)
* Requirement parsing no longer automatically validates the URL
(:issue:`120`)
* Canonicalize names for requirements comparison (:issue:`644`)
* Introduce ``metadata.Metadata`` (along with
``metadata.ExceptionGroup`` and ``metadata.InvalidMetadata``;
:issue:`570`)
* Introduce the ``validate`` keyword parameter to
``utils.validate_name()`` (:issue:`570`)
* Introduce ``utils.is_normalized_name()`` (:issue:`570`)
* Make ``utils.parse_sdist_filename()`` and
``utils.parse_wheel_filename()``
raise ``InvalidSdistFilename`` and ``InvalidWheelFilename``,
respectively,
  when the version component of the name is invalid
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=packaging&package-manager=pip&previous-version=23.1&new-version=23.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -------------------- Commit message for microsoft/vscode-python@aeee067b7aef2c9f52d81031e0e7da408a47aa3b: Update version and packages for release candidate (microsoft/vscode-python#22127) -------------------- Commit message for microsoft/vscode-python@66c7db6e369ce02a410deca0913730a87f74df1b: check existence of value in header before trim process json prc (microsoft/vscode-python#22116) Made extra tests to validate that `parseJsonRPCHeadersAndData` works as expected and uncovered a bug. Added check to see if the value is null before trim. -------------------- Commit message for microsoft/vscode-python@e87a83cc34aa72ed6e29f29255c29ff4eb28978a: Correct display name for env kinds (microsoft/vscode-python#22115) Closes https://github.com/microsoft/vscode-python/issues/22094 -------------------- Commit message for microsoft/vscode-python@f577ce6d15af5a68a4c06494ea1500921bc81af3: Align env type capitalization with tool recommendation (microsoft/vscode-python#22103) Maybe it should also be Poetry? 🤷 Fixes microsoft/vscode-python#22094 -------------------- Commit message for microsoft/vscode-python@2579b15ca68461f578371e132f29747ad26130d6: Fire active environment change event if selected environment is deleted (microsoft/vscode-python#22113) Closes https://github.com/microsoft/vscode-python/issues/22066 -------------------- Commit message for microsoft/vscode-python@c3214c0344fdb96dde38ff424f0abfe56af13a51: switch to verbose for raw data logs (microsoft/vscode-python#22110) fixes https://github.com/microsoft/vscode-python/issues/22095 -------------------- Commit message for microsoft/vscode-python@0fe920f067ce67632a820d22687ad182c1550610: ignore payload key-value if value is empty (microsoft/vscode-python#22105) fixes https://github.com/microsoft/vscode-python/issues/22104 -------------------- Commit message for microsoft/vscode-python@cc2a5678047a396d6d36092f8ff21182ca18e6b7: fix regex split for subtest names (microsoft/vscode-python#22107) fixes https://github.com/microsoft/vscode-python/issues/21733. Handles both cases of subtest naming as described here by ChatGPT: When you use self.subTest(i=i), you're explicitly naming the argument i. This causes subTest to use the key=value format for the sub-test's description. Therefore, the sub-test name becomes: `test_subtests.NumbersTest2.test_even2 (i='h i')` However, when you use self.subTest(i), you're passing a positional argument. In this case, subTest doesn't have a key for the argument, so it simply uses the value in square brackets: `test_subtests.NumbersTest2.test_even2 [h i]` -------------------- Commit message for microsoft/vscode-python@6d74f8d0c5a26a7b51429e44d9f19439d4aaddb2: Ensure we don't show version selection when user selects useExisting (microsoft/vscode-python#22099) Closes https://github.com/microsoft/vscode-python/issues/22084 -------------------- Commit message for microsoft/vscode-python@2d3ce9839f35b7aeb26655956b2dc21fb52793c7: Bump jedi-language-server and jedi (microsoft/vscode-python#22069) This picks up the latest versions of each of these, removing pydantic as a dependency and adding support for Python 3.11. Fixes https://github.com/microsoft/vscode-python/issues/22011 Note: this doesn't yet include Jedi support for Python 3.12 so it's likely we'll want to bump Jedi again once that support is released. -------------------- Commit message for microsoft/vscode-python@bd3590d3b6e78aa47de978cc4103ead9accb560e: Fix "reactivating terminals..." for global interpreters (microsoft/vscode-python#22096) Closes https://github.com/microsoft/vscode-python/issues/22085 closes https://github.com/microsoft/vscode-python/issues/22087 Will add tests in a follow up PR -------------------- Commit message for microsoft/vscode-python@8aad45710a2b1df45978aeac3b45c321b50e6897: Fix progress indicator when reactivating terminals (microsoft/vscode-python#22082) -------------------- Commit message for microsoft/vscode-python@4f44fa917a1fe2c451490b1d252a195dbc9e08ee: Calculate PS1 instead of using PS1 returned by shell (microsoft/vscode-python#22078) Closes https://github.com/microsoft/vscode-python/issues/22056 `PS1` returned by shell is not predictable, it can be `(.venv) ` or already have the context of the terminal: ``` (venv) [\u@\h \W]\[\e[91m\]$(parse_git_branch)\[\e[00m\]$ ``` Calculate it to be safe and not double prepend it. -------------------- Commit message for microsoft/vscode-python@b3c5698cee0abe48e4fe471ebbc47ba34c5a7eb5: Explicitly continue execution after timeout on launching conda binary is reached (microsoft/vscode-python#22072) Closes https://github.com/microsoft/vscode-python/issues/22050 -------------------- Commit message for microsoft/vscode-python@998a0a54a8107b2033a25df36569d090a64244d4: Add await for stdout (microsoft/vscode-python#22049) Add await so all output is read before ending the run instance. -------------------- Commit message for microsoft/vscode-python@337b8626c80b0d066e9865e07b9e520771637945: Show a prompt asking users if they want to create environment (microsoft/vscode-python#22071) Criteria for showing prompts: 1. It has to be a workspace or multiroot workspace. 2. The workspace or workspace folder should not have ".venv" or ".conda" environments. 3. The selected python should be a global python, i.e., there is no workspace specific environment selected. 4. The workspace should **not** have any `pipfile`, `poetry.lock` etc. 5. The workspace should have files that match `*requirements*.txt` or `requirements/*.txt` pattern. There is a setting to enable this behavior: `python.createEnvironment.trigger` and default is `off` closes https://github.com/microsoft/vscode-python/issues/21965 -------------------- Commit message for microsoft/vscode-python@3b6c47b4816eff2de44f5a99892b3a8bdf7f2535: Pytest to pytest (microsoft/vscode-python#22062) -------------------- Commit message for microsoft/vscode-python@dfc939b87db1466aae7334f6d21797fecaed78b7: Remove sort imports from command palette and context menu (microsoft/vscode-python#22058) Fixes https://github.com/microsoft/vscode-python/issues/20233 -------------------- Commit message for microsoft/vscode-python@4ed3fa06b6a7e45b29c736c82c81821c2e1df330: Changed order of options in Create Environment flow when .venv exists… (microsoft/vscode-python#22055) closes https://github.com/microsoft/vscode-python/issues/22038 -------------------- Commit message for microsoft/vscode-python@7693fcb3aeb08d5e3d6c6d66afda70dc1c99188a: Respect conda changeps1 config when setting PS1 (microsoft/vscode-python#22054) For https://github.com/microsoft/vscode-python/issues/22048 -------------------- Commit message for microsoft/vscode-python@242a333787db32066fc31579fc1bb2b1475e597a: Respect `VIRTUAL_ENV_DISABLE_PROMPT` when activating virtual envs (microsoft/vscode-python#22053) -------------------- Commit message for microsoft/vscode-python@00b198af9cc41a674243958ee4290de71968130a: Fix bugs related to discovery blocking other features (microsoft/vscode-python#22041) For microsoft/vscode-python#21755 -------------------- Commit message for microsoft/vscode-python@42cdaf302d5c5bc5db341aba06539ae1c6ef1670: Make sure `PATH` ends with a separator before prepending (microsoft/vscode-python#22046) Introduced with https://github.com/microsoft/vscode-python/pull/21906 For microsoft/vscode-python#20950 Fixes https://github.com/microsoft/vscode-python/issues/22047 -------------------- Commit message for microsoft/vscode-python@849be34d4772086ae7bfef6986c7aef36887f7a5: De-duplicate directories at the very end in Global virtual env locators (microsoft/vscode-python#22040) -------------------- Commit message for microsoft/vscode-python@f38ea44affa7ab62d7a17bb4d1835dba5bab71bf: Update language to encourage reading "Migration to Python Tools Extensions" (microsoft/vscode-python#22019) There are other formatter options besides Black and Autopep8, but the language of this notice suggests otherwise. -------------------- Commit message for microsoft/vscode-python@ae81fcbab9b2a3cd9346bfde5adc32e0d1eaf419: handle exceptions during test discovery (microsoft/vscode-python#22026) closes https://github.com/microsoft/vscode-python/issues/21999 and https://github.com/microsoft/vscode-python/issues/21826 -------------------- Commit message for microsoft/vscode-python@f2600075d40eba14d30bbdfd4a1a41e53f5511d8: Fix duplicate environments showing up on macOS (microsoft/vscode-python#22030) Closes https://github.com/microsoft/vscode-python/issues/22006 -------------------- Commit message for microsoft/vscode-python@b41fee72564e569808fb617328b58b0364d1995a: Remove old linter and formatter prompts and commands (microsoft/vscode-python#21979) -------------------- Commit message for microsoft/vscode-python@05ae2660c10ca8f94cdddc3c50aea18eba853eaa: Add python 3.8 and 3x specific runs (microsoft/vscode-python#22023) Closes https://github.com/microsoft/vscode-python/issues/22024 -------------------- Commit message for microsoft/vscode-python@2df331be969635bf8c425b5e26c01858561b7ed2: switch | to unions to be 3.8 compatible (microsoft/vscode-python#22025) fixes https://github.com/microsoft/vscode-python/issues/22020 -------------------- Commit message for microsoft/vscode-python@7291d303078d6adbf367160781673c0f697f9487: Remove repo labels corresponding to removing unrecognized label workflow (microsoft/vscode-python#22022) Workflow has been removed: https://github.com/microsoft/vscode-github-triage-actions/pull/188 -------------------- Commit message for microsoft/vscode-python@a9d4df919d0039dc3e8006d5eb6c1b09fe1a9273: Allow publish of pre-releases to VS Code stable this iteration (microsoft/vscode-python#22009) https://github.com/microsoft/vscode-python/pull/21997#discussion_r1327940466 For https://github.com/microsoft/vscode-python/issues/22005 -------------------- Commit message for microsoft/vscode-python@6b3dec49fa5d9fe3046d3c4bdbefaec7e7d3b806: Support EOT for testing (microsoft/vscode-python#21876) Adds support for the end of transmission (EOT) operator to all pytest and unittest responses. this PR includes: - addition of an EOT that is added to run and discovery returns and processed by the extension to initiate cleanup after run/discovery finishes - updates to all tests to support the use of EOT - redesign of how cleanup works following run/discover to make it more streamlined - full functional tests that check multiple different types of payload splitting from the buffer - tests for the cancellation token during run and debug modes -------------------- Commit message for microsoft/vscode-python@5838ea64a365f9abc1fa623cb8e372d4d9628a50: pytest complicated parameterize test parsing (microsoft/vscode-python#22001) fixes https://github.com/microsoft/vscode-python/issues/22000 -------------------- Commit message for microsoft/vscode-python@187ca86a5411fbf886128b585fe4e58439c5bd47: Do not upper case custom env variables (microsoft/vscode-python#22004) For microsoft/vscode-python#20950 closes https://github.com/microsoft/vscode-python/issues/22005 -------------------- Commit message for microsoft/vscode-python@f3f48a2e662759b04857aa801d7b8abf7cdbca30: Remove envShellEvent proposal usage (microsoft/vscode-python#21997) It's been finalized Part of microsoft/vscode#193181 -------------------- Commit message for microsoft/vscode-python@203f58bc169afc5a731557633d25722bed038bd1: Fix bug in rawprocess where stdinStr was not passed (microsoft/vscode-python#21993) -------------------- Commit message for microsoft/vscode-python@1040f3c842798fd0efb1c51aafd31395179ac153: Use stdin if workspace has large number of requirements (microsoft/vscode-python#21988) Closes https://github.com/microsoft/vscode-python/issues/21480 -------------------- Commit message for microsoft/vscode-python@221b769c084462952a724e42e314f26506f76688: Open requirement files (microsoft/vscode-python#21917) Closes https://github.com/microsoft/vscode-python/issues/21984 ![image](https://github.com/microsoft/vscode-python/assets/3840081/a5cc4991-7d65-4980-b35e-6453a85f516d) -------------------- Commit message for microsoft/vscode-python@9ebc5eb3a26f16cb963935ff01d86b03e1f34e7e: Fix `${command:python.interpreterPath}` in tasks.json in multiroot workspaces (microsoft/vscode-python#21980) Closes https://github.com/microsoft/vscode-python/issues/21915 -------------------- Commit message for microsoft/vscode-python@2268d5382b31b5500286f02bff2f12590b069369: Add support to delete and re-create .conda environments (microsoft/vscode-python#21977) Fix https://github.com/microsoft/vscode-python/issues/21828 -------------------- Commit message for microsoft/vscode-python@df0b493b0a342ae03bfa377025578f450575fab2: handle subprocess segfaults for testAdapters (microsoft/vscode-python#21963) closes: https://github.com/microsoft/vscode-python/issues/21662 Not only does this make sure segfaults are correct for unittest but also for pytest. -------------------- Commit message for microsoft/vscode-python@7aa6660d58f66afa929169c7436a992fdaefe93f: Clear environment collection only after all async operations are done (microsoft/vscode-python#21975) For microsoft/vscode-python#20950 -------------------- Commit message for microsoft/vscode-python@91b2c113f168069f76a2f26c37a2d2f8f759a682: Drop Python 3.7 support (microsoft/vscode-python#21962) Drop Python 3.7 support, and replace with Python 3.8 Resolves: microsoft/vscode-python#21532 /vscode-python/requirements.txt generated same hash even when running: ```pip-compile --generate-hashes requirements.in``` from the Python3.8 virtual environment. - Same result with pythonFiles/jedilsp_requirements/requirements.txt when running: ```pip-compile --generate-hashes pythonFiles/jedilsp_requirements/requirements.in``` -------------------- Commit message for microsoft/vscode-python@d9a23181279e39905fb7969fcd930fb46085c900: Do not assume casing of activated environment variables Python returns (microsoft/vscode-python#21970) For microsoft/vscode-python#20950 -------------------- Commit message for microsoft/vscode-python@e32657f83eb5d9c65f5190b91573405e91f12e6c: incorrect print included for absolute path calculations (microsoft/vscode-python#21932) an additional print statement was left in the pytest plugin which unnecessarily printed all absolute paths calculated. -------------------- Commit message for microsoft/vscode-python@8543dd356312fb3ea6b4b66561d896408de7faab: Fix unittest subtest names that have spaces (microsoft/vscode-python#21947) fixes https://github.com/microsoft/vscode-python/issues/21733#issuecomment-1707804763 -------------------- Commit message for microsoft/vscode-python@30c83a3d32b6ab0225ec06a06904285c93efd48c: Added git settings for branch name suggestion, protection, pull, and mergeEditor (microsoft/vscode-python#21954) VS Code repository, specifically in the .vscode/settings.json, has some nice git features such as: Issue: microsoft/vscode-python#21955 "git.branchRandomName.enable" (for suggesting random branch name when creating a new branch, comes in very handy when person wants to make and try quick changes in Codespaces), "git.branchProtection" (for branch protection), "git.pullBeforeCheckout": (for pulling before checking out a branch), "git.mergeEditor": (for making easier when in times of resolving merge conflicts) which I found could be useful to the Python extension repository as well. Credits to @karrtikr for suggesting random name, and branch protection. -------------------- Commit message for microsoft/vscode-python@7196a36b98d92726fc9bc5901cd06e9cd05be05f: Update Python extension API version (microsoft/vscode-python#21953) -------------------- Commit message for microsoft/vscode-python@69e8e7d13b96c55ba38f9c3c6599fde9be0bdf83: Catch errors when looking up python binaries in a PATH (microsoft/vscode-python#21948) Closes https://github.com/microsoft/vscode-python/issues/21944 -------------------- Commit message for microsoft/vscode-python@b4c545d52a6e0a91d995c0adc171799705d258c3: Update telemetry package (microsoft/vscode-python#21914) This PR updates the [telemetry package](https://github.com/microsoft/vscode-extension-telemetry) to the latest version and fixes the formatting of a line -------------------- Commit message for microsoft/vscode-python@a72ebb2ca4c8f6ad48c7652769e9ae9df9f92bf6: Bump actions/checkout from 3 to 4 (microsoft/vscode-python#21912) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
Release notes

Sourced from actions/checkout's releases.

v4.0.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3...v4.0.0

v3.6.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3.5.3...v3.6.0

v3.5.3

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3...v3.5.3

v3.5.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v3.5.1...v3.5.2

v3.5.1

What's Changed

New Contributors

... (truncated)

Changelog

Sourced from actions/checkout's changelog.

Changelog

v4.0.0

v3.6.0

v3.5.3

v3.5.2

v3.5.1

v3.5.0

v3.4.0

v3.3.0

v3.2.0

v3.1.0

v3.0.2

v3.0.1

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -------------------- Commit message for microsoft/vscode-python@0cf25478c70ee25c8fbb1720ab2aa754b6e02a87: Bump actions/setup-python from 2 to 4 in /.github/actions/build-vsix (microsoft/vscode-python#21926) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 4.
Release notes

Sourced from actions/setup-python's releases.

v4.0.0

What's Changed

  • Support for python-version-file input: #336

Example of usage:

- uses: actions/setup-python@v4
  with:
python-version-file: '.python-version' # Read python version from a file
- run: python my_script.py

There is no default python version for this setup-python major version, the action requires to specify either python-version input or python-version-file input. If the python-version input is not specified the action will try to read required version from file from python-version-file input.

  • Use pypyX.Y for PyPy python-version input: #349

Example of usage:

- uses: actions/setup-python@v4
  with:
    python-version: 'pypy3.9' # pypy-X.Y kept for backward compatibility
- run: python my_script.py
  • RUNNER_TOOL_CACHE environment variable is equal AGENT_TOOLSDIRECTORY: #338

  • Bugfix: create missing pypyX.Y symlinks: #347

  • PKG_CONFIG_PATH environment variable: #400

  • Added python-path output: #405 python-path output contains Python executable path.

  • Updated zeit/ncc to vercel/ncc package: #393

  • Bugfix: fixed output for prerelease version of poetry: #409

  • Made pythonLocation environment variable consistent for Python and PyPy: #418

  • Bugfix for 3.x-dev syntax: #417

  • Other improvements: #318 #396 #384 #387 #388

v3.1.4

What's Changed

In the scope of this patch release, the warning for deprecating Python 2.x was added in actions/setup-python#674 by @​dmitry-shibanov

For more information, check out actions/setup-python#672

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=2&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -------------------- Commit message for microsoft/vscode-python@d5c077c696dc3f08707317b898ee7e30b1e70bf3: Bump actions/setup-node from 2 to 3 in /.github/actions/build-vsix (microsoft/vscode-python#21927) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2 to 3.
Release notes

Sourced from actions/setup-node's releases.

Add support for asdf format and update actions/cache version to 3.0.0

In scope of this release we updated actions/cache package as the new version contains fixes for caching error handling. Moreover, we added support for asdf format as Node.js version file actions/setup-node#373. Besides, we introduced new output node-version and added npm-shrinkwrap.json to dependency file patterns: actions/setup-node#439

Update actions/cache version to 2.0.2

In scope of this release we updated actions/cache package as the new version contains fixes related to GHES 3.5 (actions/setup-node#460)

v3.0.0

In scope of this release we changed version of the runtime Node.js for the setup-node action and updated package-lock.json file to v2.

Breaking Changes

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=2&new-version=3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -------------------- Commit message for microsoft/vscode-python@835bc16859c76e04f57decbe359d0aed03438626: Bump actions/setup-python from 2 to 4 in /.github/actions/lint (microsoft/vscode-python#21925) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 4.
Release notes

Sourced from actions/setup-python's releases.

v4.0.0

What's Changed

  • Support for python-version-file input: #336

Example of usage:

- uses: actions/setup-python@v4
  with:
python-version-file: '.python-version' # Read python version from a file
- run: python my_script.py

There is no default python version for this setup-python major version, the action requires to specify either python-version input or python-version-file input. If the python-version input is not specified the action will try to read required version from file from python-version-file input.

  • Use pypyX.Y for PyPy python-version input: #349

Example of usage:

- uses: actions/setup-python@v4
  with:
    python-version: 'pypy3.9' # pypy-X.Y kept for backward compatibility
- run: python my_script.py
  • RUNNER_TOOL_CACHE environment variable is equal AGENT_TOOLSDIRECTORY: #338

  • Bugfix: create missing pypyX.Y symlinks: #347

  • PKG_CONFIG_PATH environment variable: #400

  • Added python-path output: #405 python-path output contains Python executable path.

  • Updated zeit/ncc to vercel/ncc package: #393

  • Bugfix: fixed output for prerelease version of poetry: #409

  • Made pythonLocation environment variable consistent for Python and PyPy: #418

  • Bugfix for 3.x-dev syntax: #417

  • Other improvements: #318 #396 #384 #387 #388

v3.1.4

What's Changed

In the scope of this patch release, the warning for deprecating Python 2.x was added in actions/setup-python#674 by @​dmitry-shibanov

For more information, check out actions/setup-python#672

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=2&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -------------------- Commit message for microsoft/vscode-python@b5d4249c2b810b38d7b179e49470fab3ef155475: Bump actions/upload-artifact from 2 to 3 in /.github/actions/build-vsix (microsoft/vscode-python#21924) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3.
Release notes

Sourced from actions/upload-artifact's releases.

v3.0.0

What's Changed

  • Update default runtime to node16 (#293)
  • Update package-lock.json file version to 2 (#302)

Breaking Changes

With the update to Node 16, all scripts will now be run with Node 16 rather than Node 12.

v2.3.1

Fix for empty fails on Windows failing on upload #281

v2.3.0 Upload Artifact

  • Optimizations for faster uploads of larger files that are already compressed
  • Significantly improved logging when there are chunked uploads
  • Clarifications in logs around the upload size and prohibited characters that aren't allowed in the artifact name or any uploaded files
  • Various other small bugfixes & optimizations

v2.2.4

  • Retry on HTTP 500 responses from the service

v2.2.3

  • Fixes for proxy related issues

v2.2.2

  • Improved retryability and error handling

v2.2.1

  • Update used actions/core package to the latest version

v2.2.0

  • Support for artifact retention

v2.1.4

  • Add Third Party License Information

v2.1.3

  • Use updated version of the @action/artifact NPM package

v2.1.2

  • Increase upload chunk size from 4MB to 8MB
  • Detect case insensitive file uploads

v2.1.1

  • Fix for certain symlinks not correctly being identified as directories before starting uploads

v2.1.0

  • Support for uploading artifacts with multiple paths
  • Support for using exclude paths
  • Updates to dependencies

... (truncated)

Commits
  • 0b7f8ab ci(github): update action/download-artifact from v1 to v3 (#312)
  • 013d2b8 Create devcontainer for codespaces + update all dev dependencies (#375)
  • 055b8b3 Bump Actions NPM dependencies (#374)
  • 7a5d483 ci(github): update action/checkout from v2 to v3 (#315)
  • e0057a5 README: Bump actions/checkout to v3 (#352)
  • 7fe6c13 Update to latest actions/publish-action (#363)
  • 83fd05a Bump actions-core to v1.10.0 (#356)
  • 3cea537 Merge pull request #327 from actions/robherley/artifact-1.1.0
  • 849aa77 nvm use 12 & npm run release
  • 4d39869 recompile with correct ncc version
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=2&new-version=3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -------------------- Commit message for microsoft/vscode-python@2cbafbbb052b16229db5d9d9e236ed9e8615212e: Bump actions/setup-node from 2 to 3 in /.github/actions/lint (microsoft/vscode-python#21923) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2 to 3.
Release notes

Sourced from actions/setup-node's releases.

Add support for asdf format and update actions/cache version to 3.0.0

In scope of this release we updated actions/cache package as the new version contains fixes for caching error handling. Moreover, we added support for asdf format as Node.js version file actions/setup-node#373. Besides, we introduced new output node-version and added npm-shrinkwrap.json to dependency file patterns: actions/setup-node#439

Update actions/cache version to 2.0.2

In scope of this release we updated actions/cache package as the new version contains fixes related to GHES 3.5 (actions/setup-node#460)

v3.0.0

In scope of this release we changed version of the runtime Node.js for the setup-node action and updated package-lock.json file to v2.

Breaking Changes

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=2&new-version=3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -------------------- Commit message for microsoft/vscode-python@6e9e656eca2c6b7557ec3337d84a132ec9d25b3b: Remove activation triggers that are handled automatically (microsoft/vscode-python#21918) Closes https://github.com/microsoft/vscode-python/issues/21901 -------------------- Commit message for microsoft/vscode-python@c45036bc4e0717af62f2e3b293cc8133dc396e42: Update paths for dependabot (microsoft/vscode-python#21920) For https://github.com/microsoft/vscode-python/issues/21139 -------------------- Commit message for microsoft/vscode-python@e8d0ee5bb668b92c75cea40a1395c190d16391cf: Update main to next pre-release version (microsoft/vscode-python#21921) Lead-authored-by: Kartik Raj Co-authored-by: Luciana Abud <45497113+luabud@users.noreply.github.com> Co-authored-by: Anna Burlyaeva Co-authored-by: Himani Madan Co-authored-by: Kyle Gottfried Co-authored-by: Ludi Zhan Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Co-authored-by: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Co-authored-by: Peter Law Co-authored-by: Pete Farland Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Karthik Nadig Co-authored-by: Eleanor Boyd Signed-off-by: GitHub --- .../positron-python/.devcontainer/Dockerfile | 2 +- .../.github/actions/build-vsix/action.yml | 10 +- .../.github/actions/lint/action.yml | 4 +- .../positron-python/.github/dependabot.yml | 21 + .../.github/workflows/build.yml | 58 +- .../.github/workflows/codeql-analysis.yml | 2 +- .../.github/workflows/getLabels.js | 25 - .../.github/workflows/info-needed-closer.yml | 2 +- .../.github/workflows/issue-labels.yml | 9 +- .../.github/workflows/pr-check.yml | 64 +- .../workflows/test-plan-item-validator.yml | 2 +- .../.github/workflows/triage-info-needed.yml | 4 +- extensions/positron-python/.nvmrc | 2 +- .../positron-python/.vscode/settings.json | 9 +- extensions/positron-python/README.md | 5 +- .../build/azure-pipeline.pre-release.yml | 4 +- .../build/azure-pipeline.stable.yml | 4 +- .../build/azure-pipelines/pipeline.yml | 6 +- .../positron-python/build/ci/conda_env_1.yml | 2 +- extensions/positron-python/gulpfile.js | 21 +- extensions/positron-python/package.json | 78 +-- extensions/positron-python/package.nls.json | 14 +- .../pythonExtensionApi/package-lock.json | 107 ++- .../pythonExtensionApi/package.json | 7 +- .../pythonFiles/create_venv.py | 24 +- .../pythonFiles/install_debugpy.py | 2 +- .../jedilsp_requirements/requirements.in | 2 +- .../jedilsp_requirements/requirements.txt | 91 +-- .../testing_tools/socket_manager.py | 8 +- .../tests/positron/test_environment.py | 5 +- .../pytestadapter/.data/parametrize_tests.py | 8 +- .../expected_discovery_test_output.py | 20 +- .../tests/pytestadapter/helpers.py | 4 + .../tests/pytestadapter/test_discovery.py | 129 ++-- .../tests/pytestadapter/test_execution.py | 110 ++- .../pythonFiles/tests/test_create_venv.py | 51 ++ .../tests/test_data/missing-deps.data | 2 +- .../tests/test_data/no-missing-deps.data | 2 +- .../test_data/pyproject-missing-deps.data | 2 +- .../test_data/pyproject-no-missing-deps.data | 2 +- .../tests/unittestadapter/test_discovery.py | 30 +- .../tests/unittestadapter/test_execution.py | 40 +- .../pythonFiles/unittestadapter/discovery.py | 67 +- .../pythonFiles/unittestadapter/execution.py | 82 ++- .../pythonFiles/vscode_pytest/__init__.py | 159 +++-- .../vscode_pytest/run_pytest_script.py | 2 +- extensions/positron-python/requirements.txt | 12 +- .../scripts/onCreateCommand.sh | 8 +- .../checks/macPythonInterpreter.ts | 4 +- .../src/client/common/application/commands.ts | 3 - .../src/client/common/constants.ts | 4 +- .../common/installer/productInstaller.ts | 170 +---- .../src/client/common/persistentState.ts | 45 +- .../client/common/process/rawProcessApis.ts | 4 + .../src/client/common/utils/localize.ts | 28 +- .../src/client/common/utils/multiStepInput.ts | 10 +- .../client/common/vscodeApis/windowApis.ts | 12 + .../launch.json/interpreterPathCommand.ts | 2 +- .../configuration/resolvers/launch.ts | 8 + .../debugger/extension/debugCommands.ts | 6 + .../src/client/extensionActivation.ts | 7 +- .../terminalEnvVarCollectionService.ts | 85 ++- .../client/interpreter/autoSelection/index.ts | 13 +- .../commands/setInterpreter.ts | 11 +- .../client/interpreter/interpreterService.ts | 29 +- .../linters/errorHandlers/errorHandler.ts | 6 +- .../linters/errorHandlers/notInstalled.ts | 29 - .../client/linters/errorHandlers/standard.ts | 18 + .../src/client/linters/linterCommands.ts | 114 --- .../pythonEnvironments/base/info/envKind.ts | 6 +- .../composite/envsCollectionService.ts | 4 +- .../globalVirtualEnvronmentLocator.ts | 16 +- .../common/environmentManagers/conda.ts | 19 +- .../pythonEnvironments/common/posixUtils.ts | 7 +- .../creation/common/commonUtils.ts | 8 + .../creation/common/createEnvTriggerUtils.ts | 113 +++ .../creation/createEnvironmentTrigger.ts | 160 +++++ .../provider/condaCreationProvider.ts | 114 ++- .../creation/provider/condaDeleteUtils.ts | 37 + .../creation/provider/condaUtils.ts | 95 ++- .../creation/provider/venvCreationProvider.ts | 37 +- .../creation/provider/venvUtils.ts | 47 +- .../client/pythonEnvironments/info/index.ts | 8 +- .../src/client/telemetry/constants.ts | 3 + .../src/client/telemetry/importTracker.ts | 4 +- .../src/client/telemetry/index.ts | 28 + .../codeExecution/codeExecutionManager.ts | 12 + .../testController/common/resultResolver.ts | 72 +- .../testing/testController/common/server.ts | 193 ++++-- .../testing/testController/common/types.ts | 23 +- .../testing/testController/common/utils.ts | 169 ++++- .../testing/testController/controller.ts | 2 +- .../testController/pytest/pytestController.ts | 2 +- .../pytest/pytestDiscoveryAdapter.ts | 52 +- .../pytest/pytestExecutionAdapter.ts | 112 ++- .../unittest/testDiscoveryAdapter.ts | 11 +- .../unittest/testExecutionAdapter.ts | 30 +- .../src/test/common/installer.test.ts | 12 +- .../installer.invalidPath.unit.test.ts | 5 +- .../common/installer/installer.unit.test.ts | 297 +------- .../installer/productInstaller.unit.test.ts | 264 +------ .../common/installer/productPath.unit.test.ts | 7 +- .../src/test/common/productsToTest.ts | 25 + .../commands/setInterpreter.unit.test.ts | 31 +- .../interpreterPathCommand.unit.test.ts | 14 +- .../resolvers/launch.unit.test.ts | 7 + .../extension/debugCommands.unit.test.ts | 7 + ...rminalEnvVarCollectionService.unit.test.ts | 117 +++- .../src/test/linters/lint.multilinter.test.ts | 126 ---- .../test/linters/linterCommands.unit.test.ts | 182 ----- .../src/test/mocks/mockChildProcess.ts | 6 +- .../environmentManagers/conda.unit.test.ts | 1 + .../pipenv/project1/CustomPipfileName | 2 +- .../common/envlayouts/pipenv/project2/Pipfile | 2 +- .../common/envlayouts/pipenv/project3/Pipfile | 2 +- .../envlayouts/workspace/folder1/Pipfile | 2 +- .../createEnvironmentTrigger.unit.test.ts | 285 ++++++++ .../installedPackagesDiagnostics.unit.test.ts | 4 +- .../condaCreationProvider.unit.test.ts | 31 + .../provider/condaDeleteUtils.unit.test.ts | 71 ++ .../creation/provider/condaUtils.unit.test.ts | 69 +- .../venvCreationProvider.unit.test.ts | 155 ++++- .../creation/provider/venvUtils.unit.test.ts | 71 +- .../positron-python/src/test/standardTest.ts | 7 +- .../codeExecutionManager.unit.test.ts | 9 + .../testing/common/testingAdapter.test.ts | 652 +++++++++++++----- .../testing/common/testingPayloadsEot.test.ts | 214 ++++++ .../testController/payloadTestCases.ts | 173 +++++ .../pytestExecutionAdapter.unit.test.ts | 44 +- .../resultResolver.unit.test.ts | 46 +- .../testController/server.unit.test.ts | 552 ++++++--------- .../testCancellationRunAdapters.unit.test.ts | 379 ++++++++++ .../testing/testController/utils.unit.test.ts | 116 +++- .../test_seg_fault_discovery.py | 16 + .../errorWorkspace/test_seg_fault.py | 18 + .../test_parameterized_subtest.py | 6 +- extensions/positron-python/yarn.lock | 466 +++++++------ 137 files changed, 4957 insertions(+), 2852 deletions(-) delete mode 100644 extensions/positron-python/.github/workflows/getLabels.js delete mode 100644 extensions/positron-python/src/client/linters/errorHandlers/notInstalled.ts delete mode 100644 extensions/positron-python/src/client/linters/linterCommands.ts create mode 100644 extensions/positron-python/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts create mode 100644 extensions/positron-python/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts create mode 100644 extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaDeleteUtils.ts create mode 100644 extensions/positron-python/src/test/common/productsToTest.ts delete mode 100644 extensions/positron-python/src/test/linters/lint.multilinter.test.ts delete mode 100644 extensions/positron-python/src/test/linters/linterCommands.unit.test.ts create mode 100644 extensions/positron-python/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts create mode 100644 extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts create mode 100644 extensions/positron-python/src/test/testing/common/testingPayloadsEot.test.ts create mode 100644 extensions/positron-python/src/test/testing/testController/payloadTestCases.ts create mode 100644 extensions/positron-python/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts create mode 100644 extensions/positron-python/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py create mode 100644 extensions/positron-python/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py diff --git a/extensions/positron-python/.devcontainer/Dockerfile b/extensions/positron-python/.devcontainer/Dockerfile index 5fbf068de65f..3e7e9e9cf091 100644 --- a/extensions/positron-python/.devcontainer/Dockerfile +++ b/extensions/positron-python/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/devcontainers/typescript-node:16-bookworm +FROM mcr.microsoft.com/devcontainers/typescript-node:18-bookworm RUN apt-get install -y wget bzip2 diff --git a/extensions/positron-python/.github/actions/build-vsix/action.yml b/extensions/positron-python/.github/actions/build-vsix/action.yml index 6c4621c7eb9b..ae7b8fddba69 100644 --- a/extensions/positron-python/.github/actions/build-vsix/action.yml +++ b/extensions/positron-python/.github/actions/build-vsix/action.yml @@ -16,16 +16,16 @@ runs: using: 'composite' steps: - name: Install Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ inputs.node_version }} cache: 'npm' # Jedi LS depends on dataclasses which is not in the stdlib in Python 3.7. - - name: Use Python 3.7 for JediLSP - uses: actions/setup-python@v2 + - name: Use Python 3.8 for JediLSP + uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.8 cache: 'pip' cache-dependency-path: | requirements.txt @@ -84,7 +84,7 @@ runs: shell: bash - name: Upload VSIX - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ inputs.artifact_name }} path: ${{ inputs.vsix_name }} diff --git a/extensions/positron-python/.github/actions/lint/action.yml b/extensions/positron-python/.github/actions/lint/action.yml index 1efa6aab79a5..1d302b055bee 100644 --- a/extensions/positron-python/.github/actions/lint/action.yml +++ b/extensions/positron-python/.github/actions/lint/action.yml @@ -10,7 +10,7 @@ runs: using: 'composite' steps: - name: Install Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ inputs.node_version }} cache: 'npm' @@ -36,7 +36,7 @@ runs: shell: bash - name: Install Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.x' cache: 'pip' diff --git a/extensions/positron-python/.github/dependabot.yml b/extensions/positron-python/.github/dependabot.yml index d54cf6b74a53..de5ebfe9158b 100644 --- a/extensions/positron-python/.github/dependabot.yml +++ b/extensions/positron-python/.github/dependabot.yml @@ -7,6 +7,27 @@ updates: labels: - 'no-changelog' + - package-ecosystem: 'github-actions' + directory: .github/actions/build-vsix + schedule: + interval: daily + labels: + - 'no-changelog' + + - package-ecosystem: 'github-actions' + directory: .github/actions/lint + schedule: + interval: daily + labels: + - 'no-changelog' + + - package-ecosystem: 'github-actions' + directory: .github/actions/smoke-test + schedule: + interval: daily + labels: + - 'no-changelog' + # Not skipping the news for some Python dependencies in case it's actually useful to communicate to users. - package-ecosystem: 'pip' directory: / diff --git a/extensions/positron-python/.github/workflows/build.yml b/extensions/positron-python/.github/workflows/build.yml index 24d91b94da10..56d9c04f0cd1 100644 --- a/extensions/positron-python/.github/workflows/build.yml +++ b/extensions/positron-python/.github/workflows/build.yml @@ -9,7 +9,7 @@ on: - 'release-*' env: - NODE_VERSION: 16.17.1 + NODE_VERSION: 18.17.1 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 # Force a path with spaces and to test extension works in these scenarios # Unicode characters are causing 2.7 failures so skip that for now. @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build VSIX uses: ./.github/actions/build-vsix @@ -64,7 +64,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Lint uses: ./.github/actions/lint @@ -82,7 +82,7 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install core Python requirements uses: brettcannon/pip-secure-install@v1 @@ -106,7 +106,45 @@ jobs: version: 1.1.308 working-directory: 'pythonFiles' - ### Non-smoke tests + python-tests: + name: Python Tests + # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ${{ env.special-working-directory }} + strategy: + fail-fast: false + matrix: + # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, + # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. + os: [ubuntu-latest, windows-latest] + # Run the tests on the oldest and most recent versions of Python. + python: ['3.8', '3.x', '3.12-dev'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + path: ${{ env.special-working-directory-relative }} + + - name: Use Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Install base Python requirements + uses: brettcannon/pip-secure-install@v1 + with: + requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' + options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/python" --no-cache-dir --implementation py' + + - name: Install test requirements + run: python -m pip install --upgrade -r build/test-requirements.txt + + - name: Run Python unit tests + run: python pythonFiles/tests/run_all.py + tests: name: Tests if: github.repository == 'microsoft/vscode-python' @@ -122,10 +160,10 @@ jobs: # and we assume that Ubuntu is enough to cover the UNIX case. os: [ubuntu-latest, windows-latest] python: ['3.x'] - test-suite: [ts-unit, python-unit, venv, single-workspace, multi-workspace, debugger, functional] + test-suite: [ts-unit, venv, single-workspace, multi-workspace, debugger, functional] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: ${{ env.special-working-directory-relative }} @@ -268,10 +306,6 @@ jobs: run: npm run test:unittests if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, '3.') - - name: Run Python unit tests - run: python pythonFiles/tests/run_all.py - if: matrix.test-suite == 'python-unit' - # The virtual environment based tests use the `testSingleWorkspace` set of tests # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`, # which is set in the "Prepare environment for venv tests" step. @@ -333,7 +367,7 @@ jobs: os: [ubuntu-latest, windows-latest] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Smoke tests uses: ./.github/actions/smoke-tests diff --git a/extensions/positron-python/.github/workflows/codeql-analysis.yml b/extensions/positron-python/.github/workflows/codeql-analysis.yml index 278c2cf22e4a..5b037d5a1d0b 100644 --- a/extensions/positron-python/.github/workflows/codeql-analysis.yml +++ b/extensions/positron-python/.github/workflows/codeql-analysis.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/extensions/positron-python/.github/workflows/getLabels.js b/extensions/positron-python/.github/workflows/getLabels.js deleted file mode 100644 index 99060e7205eb..000000000000 --- a/extensions/positron-python/.github/workflows/getLabels.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * To run this file: - * * npm install @octokit/rest - * * node .github/workflows/getLabels.js - * - * This script assumes the maximum number of labels to be 100. - */ - -const { Octokit } = require('@octokit/rest'); -const github = new Octokit(); -github.rest.issues - .listLabelsForRepo({ - owner: 'microsoft', - repo: 'vscode-python', - per_page: 100, - }) - .then((result) => { - const labels = result.data.map((label) => label.name); - console.log( - '\nNumber of labels found:', - labels.length, - ", verify that it's the same as number of labels listed in https://github.com/microsoft/vscode-python/labels\n", - ); - console.log(JSON.stringify(labels), '\n'); - }); diff --git a/extensions/positron-python/.github/workflows/info-needed-closer.yml b/extensions/positron-python/.github/workflows/info-needed-closer.yml index c0b130be803b..442799cd7a16 100644 --- a/extensions/positron-python/.github/workflows/info-needed-closer.yml +++ b/extensions/positron-python/.github/workflows/info-needed-closer.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions diff --git a/extensions/positron-python/.github/workflows/issue-labels.yml b/extensions/positron-python/.github/workflows/issue-labels.yml index d54015d94e46..8b084aef409f 100644 --- a/extensions/positron-python/.github/workflows/issue-labels.yml +++ b/extensions/positron-python/.github/workflows/issue-labels.yml @@ -5,8 +5,6 @@ on: types: [opened, reopened] env: - # To update the list of labels, see `getLabels.js`. - REPO_LABELS: '["area-api","area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-repl","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux","macos","meta","needs community feedback","needs PR","needs proposal","needs spike","no-changelog","on-testplan","partner ask","regression","release-plan","reports-wanted","skip package*.json","skip tests","tensorboard","testplan-item","triage-needed","verification-found","verification-needed","verification-steps-needed","verified","windows"]' TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd","anthonykim1"]' permissions: @@ -15,11 +13,11 @@ permissions: jobs: # From https://github.com/marketplace/actions/github-script#apply-a-label-to-an-issue. add-classify-label: - name: "Add 'triage-needed' and remove unrecognizable labels & assignees" + name: "Add 'triage-needed' and remove assignees" runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable @@ -28,9 +26,8 @@ jobs: - name: Install Actions run: npm install --production --prefix ./actions - - name: "Add 'triage-needed' and remove unrecognizable labels & assignees" + - name: "Add 'triage-needed' and remove assignees" uses: ./actions/python-issue-labels with: triagers: ${{ env.TRIAGERS }} token: ${{secrets.GITHUB_TOKEN}} - repo_labels: ${{ env.REPO_LABELS }} diff --git a/extensions/positron-python/.github/workflows/pr-check.yml b/extensions/positron-python/.github/workflows/pr-check.yml index aa9cae2aa474..9229393ce5cc 100644 --- a/extensions/positron-python/.github/workflows/pr-check.yml +++ b/extensions/positron-python/.github/workflows/pr-check.yml @@ -8,7 +8,7 @@ on: - release* env: - NODE_VERSION: 16.17.1 + NODE_VERSION: 18.17.1 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 MOCHA_REPORTER_JUNIT: true # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). Also enables a reporter which exits the process running the tests if it haven't already. ARTIFACT_NAME_VSIX: ms-python-insiders-vsix @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build VSIX uses: ./.github/actions/build-vsix @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Lint uses: ./.github/actions/lint @@ -56,7 +56,7 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install base Python requirements uses: brettcannon/pip-secure-install@v1 @@ -80,7 +80,45 @@ jobs: version: 1.1.308 working-directory: 'pythonFiles' - ### Non-smoke tests + python-tests: + name: Python Tests + # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ${{ env.special-working-directory }} + strategy: + fail-fast: false + matrix: + # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, + # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. + os: [ubuntu-latest, windows-latest] + # Run the tests on the oldest and most recent versions of Python. + python: ['3.8', '3.x', '3.12-dev'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + path: ${{ env.special-working-directory-relative }} + + - name: Use Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Install base Python requirements + uses: brettcannon/pip-secure-install@v1 + with: + requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' + options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/python" --no-cache-dir --implementation py' + + - name: Install test requirements + run: python -m pip install --upgrade -r build/test-requirements.txt + + - name: Run Python unit tests + run: python pythonFiles/tests/run_all.py + tests: name: Tests # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. @@ -96,11 +134,11 @@ jobs: os: [ubuntu-latest, windows-latest] # Run the tests on the oldest and most recent versions of Python. python: ['3.x'] - test-suite: [ts-unit, python-unit, venv, single-workspace, debugger, functional] + test-suite: [ts-unit, venv, single-workspace, debugger, functional] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: ${{ env.special-working-directory-relative }} @@ -139,14 +177,12 @@ jobs: with: requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/python" --no-cache-dir --implementation py' - if: startsWith(matrix.python, 3.) - name: Install Jedi requirements uses: brettcannon/pip-secure-install@v1 with: requirements-file: '"${{ env.special-working-directory-relative }}/pythonFiles/jedilsp_requirements/requirements.txt"' options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/jedilsp" --no-cache-dir --implementation py' - if: startsWith(matrix.python, 3.) - name: Install test requirements run: python -m pip install --upgrade -r build/test-requirements.txt @@ -243,12 +279,6 @@ jobs: run: npm run test:unittests if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, 3.) - # Run the Python tests in our codebase. - - name: Run Python unit tests - run: | - python pythonFiles/tests/run_all.py - if: matrix.test-suite == 'python-unit' - # The virtual environment based tests use the `testSingleWorkspace` set of tests # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`, # which is set in the "Prepare environment for venv tests" step. @@ -302,7 +332,7 @@ jobs: steps: # Need the source to have the tests available. - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Smoke tests uses: ./.github/actions/smoke-tests @@ -323,7 +353,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Node uses: actions/setup-node@v3 diff --git a/extensions/positron-python/.github/workflows/test-plan-item-validator.yml b/extensions/positron-python/.github/workflows/test-plan-item-validator.yml index 9d0805a9db9b..17f1740345f2 100644 --- a/extensions/positron-python/.github/workflows/test-plan-item-validator.yml +++ b/extensions/positron-python/.github/workflows/test-plan-item-validator.yml @@ -12,7 +12,7 @@ jobs: if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions diff --git a/extensions/positron-python/.github/workflows/triage-info-needed.yml b/extensions/positron-python/.github/workflows/triage-info-needed.yml index c717d7ec94b3..24ad2ed2c480 100644 --- a/extensions/positron-python/.github/workflows/triage-info-needed.yml +++ b/extensions/positron-python/.github/workflows/triage-info-needed.yml @@ -13,7 +13,7 @@ jobs: if: contains(github.event.issue.labels.*.name, 'triage-needed') && !contains(github.event.issue.labels.*.name, 'info-needed') steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable diff --git a/extensions/positron-python/.nvmrc b/extensions/positron-python/.nvmrc index e0325e5adb60..860cc5000ae6 100644 --- a/extensions/positron-python/.nvmrc +++ b/extensions/positron-python/.nvmrc @@ -1 +1 @@ -v16.17.1 +v18.17.1 diff --git a/extensions/positron-python/.vscode/settings.json b/extensions/positron-python/.vscode/settings.json index 487b463dd252..86b34bfd81d9 100644 --- a/extensions/positron-python/.vscode/settings.json +++ b/extensions/positron-python/.vscode/settings.json @@ -72,5 +72,12 @@ "pythonFiles/tests" ], "typescript.preferences.importModuleSpecifier": "relative", - "debug.javascript.usePreview": false + "debug.javascript.usePreview": false, + // Branch name suggestion. + "git.branchProtectionPrompt": "alwaysCommitToNewBranch", + "git.branchRandomName.enable": true, + "git.branchProtection": ["main", "release/*"], + "git.pullBeforeCheckout": true, + // Open merge editor for resolving conflicts. + "git.mergeEditor": true } diff --git a/extensions/positron-python/README.md b/extensions/positron-python/README.md index 8a5df6720717..0a8766f086af 100644 --- a/extensions/positron-python/README.md +++ b/extensions/positron-python/README.md @@ -37,7 +37,7 @@ Extensions installed through the marketplace are subject to the [Marketplace Ter ## Jupyter Notebook quick start -The Python extension offers support for Jupyter notebooks via the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) to provide you a great Python notebook experience in VS Code. +The Python extension offers support for Jupyter notebooks via the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) to provide you a great Python notebook experience in VS Code. - Install the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter). @@ -60,7 +60,6 @@ Open the Command Palette (Command+Shift+P on macOS and Ctrl+Shift+P on Windows/L | `Python: Select Interpreter` | Switch between Python interpreters, versions, and environments. | | `Python: Start REPL` | Start an interactive Python REPL using the selected interpreter in the VS Code terminal. | | `Python: Run Python File in Terminal` | Runs the active Python file in the VS Code terminal. You can also run a Python file by right-clicking on the file and selecting `Run Python File in Terminal`. | -| `Python: Select Linter` | Switch from Pylint to Flake8 or other supported linters. | | `Format Document` | Formats code using the provided [formatter](https://code.visualstudio.com/docs/python/editing#_formatting) in the `settings.json` file. | | `Python: Configure Tests` | Select a test framework and configure it to display the Test Explorer. | @@ -82,7 +81,7 @@ Learn more about the rich features of the Python extension: - [Environments](https://code.visualstudio.com/docs/python/environments): Automatically activate and switch between virtualenv, venv, pipenv, conda and pyenv environments -- [Refactoring](https://code.visualstudio.com/docs/python/editing#_refactoring): Restructure your Python code with variable extraction and method extraction. Additionally, there is componentized support to enable additional refactoring, such as import sorting, through extensions including [isort](https://marketplace.visualstudio.com/items?itemName=ms-python.isort) and [Ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff). +- [Refactoring](https://code.visualstudio.com/docs/python/editing#_refactoring): Restructure your Python code with variable extraction and method extraction. Additionally, there is componentized support to enable additional refactoring, such as import sorting, through extensions including [isort](https://marketplace.visualstudio.com/items?itemName=ms-python.isort) and [Ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff). diff --git a/extensions/positron-python/build/azure-pipeline.pre-release.yml b/extensions/positron-python/build/azure-pipeline.pre-release.yml index d4fe0ac376fb..bb52f983d02e 100644 --- a/extensions/positron-python/build/azure-pipeline.pre-release.yml +++ b/extensions/positron-python/build/azure-pipeline.pre-release.yml @@ -33,12 +33,12 @@ extends: buildSteps: - task: NodeTool@0 inputs: - versionSpec: '16.17.1' + versionSpec: '18.17.1' displayName: Select Node version - task: UsePythonVersion@0 inputs: - versionSpec: '3.7' + versionSpec: '3.8' addToPath: true architecture: 'x64' displayName: Select Python version diff --git a/extensions/positron-python/build/azure-pipeline.stable.yml b/extensions/positron-python/build/azure-pipeline.stable.yml index 05f83aa81824..02f8bd38cf81 100644 --- a/extensions/positron-python/build/azure-pipeline.stable.yml +++ b/extensions/positron-python/build/azure-pipeline.stable.yml @@ -28,12 +28,12 @@ extends: buildSteps: - task: NodeTool@0 inputs: - versionSpec: '16.17.1' + versionSpec: '18.17.1' displayName: Select Node version - task: UsePythonVersion@0 inputs: - versionSpec: '3.7' + versionSpec: '3.8' addToPath: true architecture: 'x64' displayName: Select Python version diff --git a/extensions/positron-python/build/azure-pipelines/pipeline.yml b/extensions/positron-python/build/azure-pipelines/pipeline.yml index 85b41c16efc0..adb2fa5d1c30 100644 --- a/extensions/positron-python/build/azure-pipelines/pipeline.yml +++ b/extensions/positron-python/build/azure-pipelines/pipeline.yml @@ -37,13 +37,13 @@ extends: testPlatforms: - name: Linux nodeVersions: - - 16.17.1 + - 18.17.1 - name: MacOS nodeVersions: - - 16.17.1 + - 18.17.1 - name: Windows nodeVersions: - - 16.17.1 + - 18.17.1 testSteps: - template: /build/azure-pipelines/templates/test-steps.yml@self parameters: diff --git a/extensions/positron-python/build/ci/conda_env_1.yml b/extensions/positron-python/build/ci/conda_env_1.yml index df5c917dcf4f..e9d08d0820a4 100644 --- a/extensions/positron-python/build/ci/conda_env_1.yml +++ b/extensions/positron-python/build/ci/conda_env_1.yml @@ -1,4 +1,4 @@ name: conda_env_1 dependencies: - - python=3.7 + - python=3.8 - pip diff --git a/extensions/positron-python/gulpfile.js b/extensions/positron-python/gulpfile.js index 41d8355882c4..d5579b381d3a 100644 --- a/extensions/positron-python/gulpfile.js +++ b/extensions/positron-python/gulpfile.js @@ -43,18 +43,19 @@ gulp.task('compileCore', (done) => { .on('finish', () => (failed ? done(new Error('TypeScript compilation errors')) : done())); }); -const apiTsProject = ts.createProject('./pythonExtensionApi/tsconfig.json', { typescript }); - gulp.task('compileApi', (done) => { - let failed = false; - apiTsProject - .src() - .pipe(apiTsProject()) - .on('error', () => { - failed = true; + spawnAsync('npm', ['run', 'compileApi'], undefined, true) + .then((stdout) => { + if (stdout.includes('error')) { + done(new Error(stdout)); + } else { + done(); + } }) - .js.pipe(gulp.dest('./pythonExtensionApi/out')) - .on('finish', () => (failed ? done(new Error('TypeScript compilation errors')) : done())); + .catch((ex) => { + console.log(ex); + done(new Error('TypeScript compilation errors', ex)); + }); }); gulp.task('compile', gulp.series('compileCore', 'compileApi')); diff --git a/extensions/positron-python/package.json b/extensions/positron-python/package.json index 8d5ea3882b40..37a6ff91ec10 100644 --- a/extensions/positron-python/package.json +++ b/extensions/positron-python/package.json @@ -20,7 +20,6 @@ "enabledApiProposals": [ "contribEditorContentMenu", "quickPickSortByLabel", - "envShellEvent", "testObserver", "quickPickItemTooltip", "saveEditor" @@ -44,7 +43,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.82.0-20230830" + "vscode": "^1.82.0" }, "enableTelemetry": false, "keywords": [ @@ -68,9 +67,6 @@ "onLanguage:python", "onDebugDynamicConfigurations:python", "onDebugResolve:python", - "onWalkthrough:pythonWelcome", - "onWalkthrough:pythonWelcome2", - "onWalkthrough:pythonDataScienceWelcome", "workspaceContains:mspythonconfig.json", "workspaceContains:pyproject.toml", "workspaceContains:Pipfile", @@ -369,11 +365,6 @@ "command": "python.createEnvironment-button", "title": "%python.command.python.createEnvironment.title%" }, - { - "category": "Python", - "command": "python.enableLinting", - "title": "%python.command.python.enableLinting.title%" - }, { "category": "Python", "command": "python.enableSourceMapSupport", @@ -435,26 +426,11 @@ "icon": "$(run-errors)", "title": "%python.command.testing.rerunFailedTests.title%" }, - { - "category": "Python", - "command": "python.runLinting", - "title": "%python.command.python.runLinting.title%" - }, { "category": "Python", "command": "python.setInterpreter", "title": "%python.command.python.setInterpreter.title%" }, - { - "category": "Python", - "command": "python.setLinter", - "title": "%python.command.python.setLinter.title%" - }, - { - "category": "Python Refactor", - "command": "python.sortImports", - "title": "%python.command.python.sortImports.title%" - }, { "category": "Python", "command": "python.startREPL", @@ -509,6 +485,19 @@ "experimental" ] }, + "python.createEnvironment.trigger": { + "default": "off", + "markdownDescription": "%python.createEnvironment.trigger.description%", + "scope": "machine-overridable", + "type": "string", + "enum": [ + "off", + "prompt" + ], + "tags": [ + "experimental" + ] + }, "python.condaPath": { "default": "", "description": "%python.condaPath.description%", @@ -1147,7 +1136,7 @@ "scope": "machine", "type": "string" }, - "python.missingPackage.severity":{ + "python.missingPackage.severity": { "default": "Hint", "description": "%python.missingPackage.severity.description%", "enum": [ @@ -1819,12 +1808,6 @@ "title": "%python.command.python.createTerminal.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, - { - "category": "Python", - "command": "python.enableLinting", - "title": "%python.command.python.enableLinting.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, { "category": "Python", "command": "python.enableSourceMapSupport", @@ -1896,30 +1879,12 @@ "title": "%python.command.testing.rerunFailedTests.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, - { - "category": "Python", - "command": "python.runLinting", - "title": "%python.command.python.runLinting.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, { "category": "Python", "command": "python.setInterpreter", "title": "%python.command.python.setInterpreter.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, - { - "category": "Python", - "command": "python.setLinter", - "title": "%python.command.python.setLinter.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, - { - "category": "Python Refactor", - "command": "python.sortImports", - "title": "%python.command.python.sortImports.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, { "category": "Python", "command": "python.startREPL", @@ -1958,12 +1923,6 @@ "group": "Python", "when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported && isWorkspaceTrusted" }, - { - "command": "python.sortImports", - "group": "Refactor", - "title": "%python.command.python.sortImports.title%", - "when": "editorLangId == python && !notebookEditorFocused && !virtualWorkspace && shellExecutionSupported" - }, { "submenu": "python.runFileInteractive", "group": "Jupyter2", @@ -2102,6 +2061,7 @@ "package": "gulp clean && gulp prePublishBundle && vsce package -o ms-python-insiders.vsix", "prePublish": "gulp clean && gulp prePublishNonBundle", "compile": "tsc -watch -p ./", + "compileApi": "node ./node_modules/typescript/lib/tsc.js -b ./pythonExtensionApi/tsconfig.json", "compiled": "deemon npm run compile", "kill-compiled": "deemon --kill npm run compile", "checkDependencies": "gulp checkDependencies", @@ -2137,7 +2097,7 @@ }, "dependencies": { "@iarna/toml": "^2.2.5", - "@vscode/extension-telemetry": "^0.7.7", + "@vscode/extension-telemetry": "^0.8.4", "@vscode/jupyter-lsp-middleware": "^0.2.50", "arch": "^2.1.0", "diff-match-patch": "^1.0.0", @@ -2189,7 +2149,7 @@ "@types/md5": "^2.1.32", "@types/mocha": "^9.1.0", "@types/nock": "^10.0.3", - "@types/node": "^16.17.0", + "@types/node": "^18.17.1", "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", "@types/sinon": "^10.0.11", @@ -2202,7 +2162,7 @@ "@types/xml2js": "0.4.9", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", - "@vscode/test-electron": "^2.1.3", + "@vscode/test-electron": "^2.3.4", "@vscode/vsce": "^2.18.0", "bent": "^7.3.12", "chai": "^4.1.2", diff --git a/extensions/positron-python/package.nls.json b/extensions/positron-python/package.nls.json index bfb0a2061339..0160d70e4521 100644 --- a/extensions/positron-python/package.nls.json +++ b/extensions/positron-python/package.nls.json @@ -21,15 +21,13 @@ "python.command.python.execSelectionInTerminal.title": "Run Selection/Line in Python Terminal", "python.command.python.execSelectionInDjangoShell.title": "Run Selection/Line in Django Shell", "python.command.python.reportIssue.title": "Report Issue...", - "python.command.python.setLinter.title": "Select Linter", - "python.command.python.enableLinting.title": "Enable/Disable Linting", - "python.command.python.runLinting.title": "Run Linting", "python.command.python.enableSourceMapSupport.title": "Enable Source Map Support For Extension Debugging", "python.command.python.clearCacheAndReload.title": "Clear Cache and Reload Window", "python.command.python.analysis.restartLanguageServer.title": "Restart Language Server", "python.command.python.launchTensorBoard.title": "Launch TensorBoard", "python.command.python.refreshTensorBoard.title": "Refresh TensorBoard", "python.createEnvironment.contentButton.description": "Show or hide Create Environment button in the editor for `requirements.txt` or other dependency files.", + "python.createEnvironment.trigger.description": "Detect if environment creation is required for the current project", "python.menu.createNewFile.title": "Python File", "python.editor.context.submenu.runPython": "Run Python", "python.editor.context.submenu.runPythonInteractive": "Run in Interactive window", @@ -60,14 +58,14 @@ "python.formatting.blackPath.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", "python.formatting.blackPath.deprecationMessage": "This setting will soon be deprecated. Please use the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.provider.description": "Provider for formatting. Possible options include 'autopep8', 'black', and 'yapf'.", - "python.formatting.provider.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8) or the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.provider.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension or the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", + "python.formatting.provider.markdownDeprecationMessage": "This setting will soon be deprecated. Please use a dedicated formatter extension.
Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.provider.deprecationMessage": "This setting will soon be deprecated. Please use a dedicated formatter extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.yapfArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.formatting.yapfArgs.markdownDeprecationMessage": "Yapf support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.yapfArgs.deprecationMessage": "Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", + "python.formatting.yapfArgs.markdownDeprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.yapfArgs.deprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.yapfPath.description": "Path to yapf, you can use a custom version of yapf by modifying this setting to include the full path.", "python.formatting.yapfPath.markdownDeprecationMessage": "Yapf support will soon be deprecated.
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.yapfPath.deprecationMessage": "Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", + "python.formatting.yapfPath.deprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.globalModuleInstallation.description": "Whether to install Python modules globally when not using an environment.", "python.languageServerDebug.description": "Whether debug should be enabled for Positron's Python language server.", "python.languageServerLogLevel.description": "Controls the [logging level](https://docs.python.org/3/library/logging.html#levels) of Positron's Python language server. Requires a restart to take effect.", diff --git a/extensions/positron-python/pythonExtensionApi/package-lock.json b/extensions/positron-python/pythonExtensionApi/package-lock.json index 9b4847457b20..ef6914e0e786 100644 --- a/extensions/positron-python/pythonExtensionApi/package-lock.json +++ b/extensions/positron-python/pythonExtensionApi/package-lock.json @@ -1,19 +1,20 @@ { "name": "@vscode/python-extension", - "version": "1.0.4", + "version": "1.0.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@vscode/python-extension", - "version": "1.0.4", + "version": "1.0.5", "license": "MIT", "devDependencies": { "@types/vscode": "^1.78.0", + "source-map": "^0.8.0-beta.0", "typescript": "5.0.4" }, "engines": { - "node": ">=16.17.1", + "node": ">=18.17.1", "vscode": "^1.78.0" } }, @@ -23,6 +24,42 @@ "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==", "dev": true }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/typescript": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", @@ -35,6 +72,23 @@ "engines": { "node": ">=12.20" } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } } }, "dependencies": { @@ -44,11 +98,58 @@ "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==", "dev": true }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "requires": { + "whatwg-url": "^7.0.0" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, "typescript": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } } } } diff --git a/extensions/positron-python/pythonExtensionApi/package.json b/extensions/positron-python/pythonExtensionApi/package.json index 86ac58f42f20..9e58f1a2400c 100644 --- a/extensions/positron-python/pythonExtensionApi/package.json +++ b/extensions/positron-python/pythonExtensionApi/package.json @@ -1,7 +1,7 @@ { "name": "@vscode/python-extension", "description": "An API facade for the Python extension in VS Code", - "version": "1.0.4", + "version": "1.0.5", "author": { "name": "Microsoft Corporation" }, @@ -13,7 +13,7 @@ "main": "./out/main.js", "types": "./out/main.d.ts", "engines": { - "node": ">=16.17.1", + "node": ">=18.17.1", "vscode": "^1.78.0" }, "license": "MIT", @@ -27,7 +27,8 @@ }, "devDependencies": { "typescript": "5.0.4", - "@types/vscode": "^1.78.0" + "@types/vscode": "^1.78.0", + "source-map": "^0.8.0-beta.0" }, "scripts": { "prepublishOnly": "echo \"⛔ Can only publish from a secure pipeline ⛔\" && node ../build/fail", diff --git a/extensions/positron-python/pythonFiles/create_venv.py b/extensions/positron-python/pythonFiles/create_venv.py index cac084fd2222..68f21a38b980 100644 --- a/extensions/positron-python/pythonFiles/create_venv.py +++ b/extensions/positron-python/pythonFiles/create_venv.py @@ -3,6 +3,7 @@ import argparse import importlib.util as import_util +import json import os import pathlib import subprocess @@ -56,6 +57,12 @@ def parse_args(argv: Sequence[str]) -> argparse.Namespace: metavar="NAME", action="store", ) + parser.add_argument( + "--stdin", + action="store_true", + default=False, + help="Read arguments from stdin.", + ) return parser.parse_args(argv) @@ -152,6 +159,16 @@ def install_pip(name: str): ) +def get_requirements_from_args(args: argparse.Namespace) -> List[str]: + requirements = [] + if args.stdin: + data = json.loads(sys.stdin.read()) + requirements = data.get("requirements", []) + if args.requirements: + requirements.extend(args.requirements) + return requirements + + def main(argv: Optional[Sequence[str]] = None) -> None: if argv is None: argv = [] @@ -223,9 +240,10 @@ def main(argv: Optional[Sequence[str]] = None) -> None: print(f"VENV_INSTALLING_PYPROJECT: {args.toml}") install_toml(venv_path, args.extras) - if args.requirements: - print(f"VENV_INSTALLING_REQUIREMENTS: {args.requirements}") - install_requirements(venv_path, args.requirements) + requirements = get_requirements_from_args(args) + if requirements: + print(f"VENV_INSTALLING_REQUIREMENTS: {requirements}") + install_requirements(venv_path, requirements) if __name__ == "__main__": diff --git a/extensions/positron-python/pythonFiles/install_debugpy.py b/extensions/positron-python/pythonFiles/install_debugpy.py index cabb620ea1f2..9377d00237d7 100644 --- a/extensions/positron-python/pythonFiles/install_debugpy.py +++ b/extensions/positron-python/pythonFiles/install_debugpy.py @@ -13,7 +13,7 @@ DEBUGGER_DEST = os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "python") DEBUGGER_PACKAGE = "debugpy" DEBUGGER_PYTHON_ABI_VERSIONS = ("cp310",) -DEBUGGER_VERSION = "1.6.7" # can also be "latest" +DEBUGGER_VERSION = "1.8.0" # can also be "latest" def _contains(s, parts=()): diff --git a/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.in b/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.in index 6e99ec89fa10..ec99326f5cba 100644 --- a/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.in +++ b/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.in @@ -1,6 +1,6 @@ # This file is used to generate requirements.txt. # To update requirements.txt, run the following commands. -# Use Python 3.7 when creating the environment or using pip-tools +# Use Python 3.8 when creating the environment or using pip-tools # 1) pip install pip-tools # 2) pip-compile --generate-hashes --upgrade pythonFiles\jedilsp_requirements\requirements.in diff --git a/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt b/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt index e7c726540f45..f2f0cbf3291e 100644 --- a/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt +++ b/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt @@ -13,33 +13,32 @@ attrs==23.1.0 \ cattrs==23.1.2 \ --hash=sha256:b2bb14311ac17bed0d58785e5a60f022e5431aca3932e3fc5cc8ed8639de50a4 \ --hash=sha256:db1c821b8c537382b2c7c66678c3790091ca0275ac486c76f3c8f3920e83c657 - # via lsprotocol + # via + # jedi-language-server + # lsprotocol docstring-to-markdown==0.12 \ --hash=sha256:40004224b412bd6f64c0f3b85bb357a41341afd66c4b4896709efa56827fb2bb \ --hash=sha256:7df6311a887dccf9e770f51242ec002b19f0591994c4783be49d24cdc1df3737 # via jedi-language-server -exceptiongroup==1.1.2 \ - --hash=sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5 \ - --hash=sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f +exceptiongroup==1.1.3 \ + --hash=sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9 \ + --hash=sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3 # via cattrs -importlib-metadata==3.10.1 \ - --hash=sha256:2ec0faae539743ae6aaa84b49a169670a465f7f5d64e6add98388cc29fd1f2f6 \ - --hash=sha256:c9356b657de65c53744046fa8f7358afe0714a1af7d570c00c3835c2d724a7c1 - # via - # attrs - # jedi-language-server - # typeguard -jedi==0.18.2 \ - --hash=sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e \ - --hash=sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612 +importlib-metadata==6.8.0 \ + --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \ + --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743 + # via typeguard +jedi==0.19.1 \ + --hash=sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd \ + --hash=sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0 # via jedi-language-server -jedi-language-server==0.40.0 \ - --hash=sha256:53e590400b5cd2f6e363e77a4d824b1883798994b731cb0b4370d103748d30e2 \ - --hash=sha256:bacbae2930b6a8a0f1f284c211672fceec94b4808b0415d1c3352fa4b1ac5ad6 +jedi-language-server==0.41.1 \ + --hash=sha256:3f15ca5cc28e728564f7d63583e171b418025582447ce023512e3f2b2d71ebae \ + --hash=sha256:ca9b3e7f48b70f0988d85ffde4f01dd1ab94c8e0f69e8c6424e6657117b44f91 # via -r pythonFiles\jedilsp_requirements\requirements.in -lsprotocol==2023.0.0a2 \ - --hash=sha256:80aae7e39171b49025876a524937c10be2eb986f4be700ca22ee7d186b8488aa \ - --hash=sha256:c4f2f77712b50d065b17f9b50d2b88c480dc2ce4bbaa56eea8269dbf54bc9701 +lsprotocol==2023.0.0b1 \ + --hash=sha256:ade2cd0fa0ede7965698cb59cd05d3adbd19178fd73e83f72ef57a032fbb9d62 \ + --hash=sha256:f7a2d4655cbd5639f373ddd1789807450c543341fa0a32b064ad30dbb9f510d4 # via # jedi-language-server # pygls @@ -55,44 +54,6 @@ parso==0.8.3 \ --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \ --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75 # via jedi -pydantic==1.10.12 \ - --hash=sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303 \ - --hash=sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe \ - --hash=sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47 \ - --hash=sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494 \ - --hash=sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33 \ - --hash=sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86 \ - --hash=sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d \ - --hash=sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c \ - --hash=sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a \ - --hash=sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565 \ - --hash=sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb \ - --hash=sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62 \ - --hash=sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62 \ - --hash=sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0 \ - --hash=sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523 \ - --hash=sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d \ - --hash=sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405 \ - --hash=sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f \ - --hash=sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b \ - --hash=sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718 \ - --hash=sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed \ - --hash=sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb \ - --hash=sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5 \ - --hash=sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc \ - --hash=sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942 \ - --hash=sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe \ - --hash=sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246 \ - --hash=sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350 \ - --hash=sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303 \ - --hash=sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09 \ - --hash=sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33 \ - --hash=sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8 \ - --hash=sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a \ - --hash=sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1 \ - --hash=sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6 \ - --hash=sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d - # via jedi-language-server pygls==1.0.2 \ --hash=sha256:6d278d29fa6559b0f7a448263c85cb64ec6e9369548b02f1a7944060848b21f9 \ --hash=sha256:888ed63d1f650b4fc64d603d73d37545386ec533c0caac921aed80f80ea946a4 @@ -103,14 +64,14 @@ typeguard==3.0.2 \ --hash=sha256:bbe993854385284ab42fd5bd3bee6f6556577ce8b50696d6cb956d704f286c8e \ --hash=sha256:fee5297fdb28f8e9efcb8142b5ee219e02375509cd77ea9d270b5af826358d5a # via pygls -typing-extensions==4.7.1 \ - --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ - --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 +typing-extensions==4.8.0 \ + --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ + --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef # via # cattrs - # pydantic + # jedi-language-server # typeguard -zipp==3.15.0 \ - --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ - --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 +zipp==3.17.0 \ + --hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \ + --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0 # via importlib-metadata diff --git a/extensions/positron-python/pythonFiles/testing_tools/socket_manager.py b/extensions/positron-python/pythonFiles/testing_tools/socket_manager.py index 372a50b5e012..b2afbf0e5a17 100644 --- a/extensions/positron-python/pythonFiles/testing_tools/socket_manager.py +++ b/extensions/positron-python/pythonFiles/testing_tools/socket_manager.py @@ -23,6 +23,12 @@ def __init__(self, addr): self.socket = None def __enter__(self): + return self.connect() + + def __exit__(self, *_): + self.close() + + def connect(self): self.socket = socket.socket( socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP ) @@ -35,7 +41,7 @@ def __enter__(self): return self - def __exit__(self, *_): + def close(self): if self.socket: try: self.socket.shutdown(socket.SHUT_RDWR) diff --git a/extensions/positron-python/pythonFiles/tests/positron/test_environment.py b/extensions/positron-python/pythonFiles/tests/positron/test_environment.py index f4c1c52612d3..145eae0b0b07 100644 --- a/extensions/positron-python/pythonFiles/tests/positron/test_environment.py +++ b/extensions/positron-python/pythonFiles/tests/positron/test_environment.py @@ -3,8 +3,8 @@ # from __future__ import annotations -import asyncio +import asyncio import inspect import math import pprint @@ -28,8 +28,8 @@ EnvironmentVariable, EnvironmentVariableValueKind, ) -from positron.positron_ipkernel import PositronIPyKernel from positron.inspectors import get_inspector +from positron.positron_ipkernel import PositronIPyKernel from .conftest import DummyComm @@ -837,6 +837,7 @@ def test_numpy_assign_and_update(shell: TerminalInteractiveShell, env_comm: Dumm } +@pytest.mark.skip() def test_torch_assign_and_update(shell: TerminalInteractiveShell, env_comm: DummyComm) -> None: """ Test environment change detection for pytorch tensors. diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py index a39b7c26de9f..c4dbadc32d6e 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py @@ -15,6 +15,8 @@ def test_adding(actual, expected): # Testing pytest with parametrized tests. All three pass. # The tests ids are parametrize_tests.py::test_under_ten[1] and so on. -@pytest.mark.parametrize("num", range(1, 3)) # test_marker--test_under_ten -def test_under_ten(num): - assert num < 10 +@pytest.mark.parametrize( # test_marker--test_string + "string", ["hello", "complicated split [] ()"] +) +def test_string(string): + assert string == "hello" diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 2b2c07ab8ea7..31686d2b3b5d 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -594,46 +594,46 @@ ], }, { - "name": "test_under_ten", + "name": "test_string", "path": os.fspath(parameterize_tests_path), "type_": "function", "children": [ { - "name": "[1]", + "name": "[hello]", "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( - "test_under_ten[1]", + "test_string[hello]", parameterize_tests_path, ), "type_": "test", "id_": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[1]", + "parametrize_tests.py::test_string[hello]", parameterize_tests_path, ), "runID": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[1]", + "parametrize_tests.py::test_string[hello]", parameterize_tests_path, ), }, { - "name": "[2]", + "name": "[complicated split [] ()]", "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( - "test_under_ten[2]", + "test_string[1]", parameterize_tests_path, ), "type_": "test", "id_": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[2]", + "parametrize_tests.py::test_string[complicated split [] ()]", parameterize_tests_path, ), "runID": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[2]", + "parametrize_tests.py::test_string[complicated split [] ()]", parameterize_tests_path, ), }, ], - "id_": "parametrize_tests.py::test_under_ten", + "id_": "parametrize_tests.py::test_string", }, ], }, diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/helpers.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/helpers.py index 7195cfe43ea5..b534e950945a 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/helpers.py +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/helpers.py @@ -12,6 +12,10 @@ import uuid from typing import Any, Dict, List, Optional, Tuple +script_dir = pathlib.Path(__file__).parent.parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) + TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" from typing_extensions import TypedDict diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_discovery.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/test_discovery.py index 8d785be27c8b..674d92ac0545 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/test_discovery.py @@ -29,15 +29,26 @@ def test_import_error(tmp_path): temp_dir.mkdir() p = temp_dir / "error_pytest_import.py" shutil.copyfile(file_path, p) - actual_list: Optional[List[Dict[str, Any]]] = runner( - ["--collect-only", os.fspath(p)] - ) - assert actual_list - for actual in actual_list: - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 + actual: Optional[List[Dict[str, Any]]] = runner(["--collect-only", os.fspath(p)]) + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + assert False def test_syntax_error(tmp_path): @@ -60,13 +71,25 @@ def test_syntax_error(tmp_path): p = temp_dir / "error_syntax_discovery.py" shutil.copyfile(file_path, p) actual = runner(["--collect-only", os.fspath(p)]) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + assert False def test_parameterized_error_collect(): @@ -76,12 +99,25 @@ def test_parameterized_error_collect(): """ file_path_str = "error_parametrize_discovery.py" actual = runner(["--collect-only", file_path_str]) - if actual: - actual = actual[0] - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + assert False @pytest.mark.parametrize( @@ -146,13 +182,16 @@ def test_pytest_collect(file, expected_const): os.fspath(TEST_DATA_PATH / file), ] ) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert actual["tests"] == expected_const + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + actual_item = actual_list.pop(0) + assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + assert actual_item.get("tests") == expected_const def test_pytest_root_dir(): @@ -168,14 +207,16 @@ def test_pytest_root_dir(): ], TEST_DATA_PATH / "root", ) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + actual_item = actual_list.pop(0) + assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH / "root") assert ( - actual["tests"] + actual_item.get("tests") == expected_discovery_test_output.root_with_config_expected_output ) @@ -193,13 +234,15 @@ def test_pytest_config_file(): ], TEST_DATA_PATH / "root", ) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + actual_item = actual_list.pop(0) + assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH / "root") assert ( - actual["tests"] + actual_item.get("tests") == expected_discovery_test_output.root_with_config_expected_output ) diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py index 07354b01709b..37a392f66d4b 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import os import shutil +from typing import Any, Dict, List import pytest @@ -23,14 +24,19 @@ def test_config_file(): expected_execution_test_output.config_file_pytest_expected_execution_output ) assert actual - assert len(actual) == len(expected_const) + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + assert len(actual_list) == len(expected_const) actual_result_dict = dict() - for a in actual: - assert all(item in a for item in ("status", "cwd", "result")) - assert a["status"] == "success" - assert a["cwd"] == os.fspath(new_cwd) - actual_result_dict.update(a["result"]) - assert actual_result_dict == expected_const + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "result") + ) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(new_cwd) + actual_result_dict.update(actual_item["result"]) + assert actual_result_dict == expected_const def test_rootdir_specified(): @@ -43,14 +49,19 @@ def test_rootdir_specified(): expected_execution_test_output.config_file_pytest_expected_execution_output ) assert actual - assert len(actual) == len(expected_const) + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + assert len(actual_list) == len(expected_const) actual_result_dict = dict() - for a in actual: - assert all(item in a for item in ("status", "cwd", "result")) - assert a["status"] == "success" - assert a["cwd"] == os.fspath(new_cwd) - actual_result_dict.update(a["result"]) - assert actual_result_dict == expected_const + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "result") + ) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(new_cwd) + actual_result_dict.update(actual_item["result"]) + assert actual_result_dict == expected_const def test_syntax_error_execution(tmp_path): @@ -73,13 +84,23 @@ def test_syntax_error_execution(tmp_path): p = temp_dir / "error_syntax_discovery.py" shutil.copyfile(file_path, p) actual = runner(["error_syntax_discover.py::test_function"]) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 1 + assert actual + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 1 + else: + assert False def test_bad_id_error_execution(): @@ -88,13 +109,23 @@ def test_bad_id_error_execution(): The json should still be returned but the errors list should be present. """ actual = runner(["not/a/real::test_id"]) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 1 + assert actual + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 1 + else: + assert False @pytest.mark.parametrize( @@ -195,7 +226,8 @@ def test_pytest_execution(test_ids, expected_const): 3. uf_single_method_execution_expected_output: test run on a single method in a file. 4. uf_non_adjacent_tests_execution_expected_output: test run on unittests in two files with single selection in test explorer. 5. unit_pytest_same_file_execution_expected_output: test run on a file with both unittest and pytest tests. - 6. dual_level_nested_folder_execution_expected_output: test run on a file with one test file at the top level and one test file in a nested folder. + 6. dual_level_nested_folder_execution_expected_output: test run on a file with one test file + at the top level and one test file in a nested folder. 7. double_nested_folder_expected_execution_output: test run on a double nested folder. 8. parametrize_tests_expected_execution_output: test run on a parametrize test with 3 inputs. 9. single_parametrize_tests_expected_execution_output: test run on single parametrize test. @@ -205,18 +237,22 @@ def test_pytest_execution(test_ids, expected_const): Keyword arguments: test_ids -- an array of test_ids to run. expected_const -- a dictionary of the expected output from running pytest discovery on the files. - """ # noqa: E501 + """ args = test_ids actual = runner(args) assert actual - print(actual) - assert len(actual) == len(expected_const) + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + assert len(actual_list) == len(expected_const) actual_result_dict = dict() - for a in actual: - assert all(item in a for item in ("status", "cwd", "result")) - assert a["status"] == "success" - assert a["cwd"] == os.fspath(TEST_DATA_PATH) - actual_result_dict.update(a["result"]) + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "result") + ) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + actual_result_dict.update(actual_item["result"]) for key in actual_result_dict: if ( actual_result_dict[key]["outcome"] == "failure" diff --git a/extensions/positron-python/pythonFiles/tests/test_create_venv.py b/extensions/positron-python/pythonFiles/tests/test_create_venv.py index ae3f18be6f3c..772fc02708f8 100644 --- a/extensions/positron-python/pythonFiles/tests/test_create_venv.py +++ b/extensions/positron-python/pythonFiles/tests/test_create_venv.py @@ -1,7 +1,11 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import argparse +import contextlib import importlib +import io +import json import os import sys @@ -224,3 +228,50 @@ def run_process(args, error_message): create_venv.run_process = run_process create_venv.main([]) + + +@contextlib.contextmanager +def redirect_io(stream: str, new_stream): + """Redirect stdio streams to a custom stream.""" + old_stream = getattr(sys, stream) + setattr(sys, stream, new_stream) + yield + setattr(sys, stream, old_stream) + + +class CustomIO(io.TextIOWrapper): + """Custom stream object to replace stdio.""" + + name: str = "customio" + + def __init__(self, name: str, encoding="utf-8", newline=None): + self._buffer = io.BytesIO() + self._buffer.name = name + super().__init__(self._buffer, encoding=encoding, newline=newline) + + def close(self): + """Provide this close method which is used by some tools.""" + # This is intentionally empty. + + def get_value(self) -> str: + """Returns value from the buffer as string.""" + self.seek(0) + return self.read() + + +def test_requirements_from_stdin(): + importlib.reload(create_venv) + + cli_requirements = [f"cli-requirement{i}.txt" for i in range(3)] + args = argparse.Namespace() + args.__dict__.update({"stdin": True, "requirements": cli_requirements}) + + stdin_requirements = [f"stdin-requirement{i}.txt" for i in range(20)] + text = json.dumps({"requirements": stdin_requirements}) + str_input = CustomIO("", encoding="utf-8", newline="\n") + with redirect_io("stdin", str_input): + str_input.write(text) + str_input.seek(0) + actual = create_venv.get_requirements_from_args(args) + + assert actual == stdin_requirements + cli_requirements diff --git a/extensions/positron-python/pythonFiles/tests/test_data/missing-deps.data b/extensions/positron-python/pythonFiles/tests/test_data/missing-deps.data index c42d23c7dd67..c8c911f218a8 100644 --- a/extensions/positron-python/pythonFiles/tests/test_data/missing-deps.data +++ b/extensions/positron-python/pythonFiles/tests/test_data/missing-deps.data @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.7 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # pip-compile --generate-hashes --resolver=backtracking requirements-test.in diff --git a/extensions/positron-python/pythonFiles/tests/test_data/no-missing-deps.data b/extensions/positron-python/pythonFiles/tests/test_data/no-missing-deps.data index 5c2f1178bbdf..d5d04476dec0 100644 --- a/extensions/positron-python/pythonFiles/tests/test_data/no-missing-deps.data +++ b/extensions/positron-python/pythonFiles/tests/test_data/no-missing-deps.data @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.7 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # pip-compile --generate-hashes --resolver=backtracking requirements-test.in diff --git a/extensions/positron-python/pythonFiles/tests/test_data/pyproject-missing-deps.data b/extensions/positron-python/pythonFiles/tests/test_data/pyproject-missing-deps.data index f217a0bdade6..e4d6f9eb10d3 100644 --- a/extensions/positron-python/pythonFiles/tests/test_data/pyproject-missing-deps.data +++ b/extensions/positron-python/pythonFiles/tests/test_data/pyproject-missing-deps.data @@ -5,5 +5,5 @@ build-backend = "flit_core.buildapi" [project] name = "something" version = "2023.0.0" -requires-python = ">=3.7" +requires-python = ">=3.8" dependencies = ["pytest==7.3.1", "flake8-csv"] diff --git a/extensions/positron-python/pythonFiles/tests/test_data/pyproject-no-missing-deps.data b/extensions/positron-python/pythonFiles/tests/test_data/pyproject-no-missing-deps.data index 729bc9169e6f..64dadf6fdf2e 100644 --- a/extensions/positron-python/pythonFiles/tests/test_data/pyproject-no-missing-deps.data +++ b/extensions/positron-python/pythonFiles/tests/test_data/pyproject-no-missing-deps.data @@ -5,5 +5,5 @@ build-backend = "flit_core.buildapi" [project] name = "something" version = "2023.0.0" -requires-python = ">=3.7" +requires-python = ">=3.8" dependencies = [jedi-language-server"] diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py b/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py index c4778aa85852..67e52f43b70c 100644 --- a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py +++ b/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py @@ -6,39 +6,13 @@ from typing import List import pytest -from unittestadapter.discovery import ( - DEFAULT_PORT, - discover_tests, - parse_discovery_cli_args, -) +from unittestadapter.discovery import discover_tests from unittestadapter.utils import TestNodeTypeEnum, parse_unittest_args + from . import expected_discovery_test_output from .helpers import TEST_DATA_PATH, is_same_tree -@pytest.mark.parametrize( - "args, expected", - [ - (["--port", "6767", "--uuid", "some-uuid"], (6767, "some-uuid")), - (["--foo", "something", "--bar", "another"], (int(DEFAULT_PORT), None)), - (["--port", "4444", "--foo", "something", "--port", "9999"], (9999, None)), - ( - ["--uuid", "first-uuid", "--bar", "other", "--uuid", "second-uuid"], - (int(DEFAULT_PORT), "second-uuid"), - ), - ], -) -def test_parse_cli_args(args: List[str], expected: List[str]) -> None: - """The parse_cli_args function should parse and return the port and uuid passed as command-line options. - - If there were no --port or --uuid command-line option, it should return default values). - If there are multiple options, the last one wins. - """ - actual = parse_discovery_cli_args(args) - - assert expected == actual - - @pytest.mark.parametrize( "args, expected", [ diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_execution.py b/extensions/positron-python/pythonFiles/tests/unittestadapter/test_execution.py index 057f64d7396a..f7306e37662e 100644 --- a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_execution.py +++ b/extensions/positron-python/pythonFiles/tests/unittestadapter/test_execution.py @@ -4,55 +4,17 @@ import os import pathlib import sys -from typing import List import pytest script_dir = pathlib.Path(__file__).parent.parent sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -from unittestadapter.execution import parse_execution_cli_args, run_tests +from unittestadapter.execution import run_tests TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" -@pytest.mark.parametrize( - "args, expected", - [ - ( - [ - "--port", - "111", - "--uuid", - "fake-uuid", - ], - (111, "fake-uuid"), - ), - ( - ["--port", "111", "--uuid", "fake-uuid"], - (111, "fake-uuid"), - ), - ( - [ - "--port", - "111", - "--uuid", - "fake-uuid", - "-v", - "-s", - ], - (111, "fake-uuid"), - ), - ], -) -def test_parse_execution_cli_args(args: List[str], expected: List[str]) -> None: - """The parse_execution_cli_args function should return values for the port, uuid, and testids arguments - when passed as command-line options, and ignore unrecognized arguments. - """ - actual = parse_execution_cli_args(args) - assert actual == expected - - def test_no_ids_run() -> None: """This test runs on an empty array of test_ids, therefore it should return an empty dict for the result. diff --git a/extensions/positron-python/pythonFiles/unittestadapter/discovery.py b/extensions/positron-python/pythonFiles/unittestadapter/discovery.py index cbad40ad1838..7e07e45d1202 100644 --- a/extensions/positron-python/pythonFiles/unittestadapter/discovery.py +++ b/extensions/positron-python/pythonFiles/unittestadapter/discovery.py @@ -1,46 +1,27 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import argparse import json import os import pathlib import sys import traceback import unittest -from typing import List, Optional, Tuple, Union +from typing import List, Optional, Union script_dir = pathlib.Path(__file__).parent.parent sys.path.append(os.fspath(script_dir)) sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) from testing_tools import socket_manager +from typing_extensions import Literal, NotRequired, TypedDict # If I use from utils then there will be an import error in test_discovery.py. from unittestadapter.utils import TestNode, build_test_tree, parse_unittest_args -from typing_extensions import NotRequired, TypedDict, Literal - DEFAULT_PORT = "45454" -def parse_discovery_cli_args(args: List[str]) -> Tuple[int, Union[str, None]]: - """Parse command-line arguments that should be processed by the script. - - So far this includes the port number that it needs to connect to, and the uuid passed by the TS side. - The port is passed to the discovery.py script when it is executed, and - defaults to DEFAULT_PORT if it can't be parsed. - The uuid should be passed to the discovery.py script when it is executed, and defaults to None if it can't be parsed. - If the arguments appear several times, the value returned by parse_cli_args will be the value of the last argument. - """ - arg_parser = argparse.ArgumentParser() - arg_parser.add_argument("--port", default=DEFAULT_PORT) - arg_parser.add_argument("--uuid") - parsed_args, _ = arg_parser.parse_known_args(args) - - return int(parsed_args.port), parsed_args.uuid - - class PayloadDict(TypedDict): cwd: str status: Literal["success", "error"] @@ -48,6 +29,13 @@ class PayloadDict(TypedDict): error: NotRequired[List[str]] +class EOTPayloadDict(TypedDict): + """A dictionary that is used to send a end of transmission post request to the server.""" + + command_type: Union[Literal["discovery"], Literal["execution"]] + eot: bool + + def discover_tests( start_dir: str, pattern: str, top_level_dir: Optional[str], uuid: Optional[str] ) -> PayloadDict: @@ -106,17 +94,9 @@ def discover_tests( return payload -if __name__ == "__main__": - # Get unittest discovery arguments. - argv = sys.argv[1:] - index = argv.index("--udiscovery") - - start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) - - # Perform test discovery. - port, uuid = parse_discovery_cli_args(argv[:index]) - payload = discover_tests(start_dir, pattern, top_level_dir, uuid) - +def post_response( + payload: Union[PayloadDict, EOTPayloadDict], port: int, uuid: str +) -> None: # Build the request data (it has to be a POST request or the Node side will not process it), and send it. addr = ("localhost", port) data = json.dumps(payload) @@ -132,3 +112,26 @@ def discover_tests( except Exception as e: print(f"Error sending response: {e}") print(f"Request data: {request}") + + +if __name__ == "__main__": + # Get unittest discovery arguments. + argv = sys.argv[1:] + index = argv.index("--udiscovery") + + start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) + + # Perform test discovery. + testPort = int(os.environ.get("TEST_PORT", DEFAULT_PORT)) + testUuid = os.environ.get("TEST_UUID") + # Post this discovery payload. + if testUuid is not None: + payload = discover_tests(start_dir, pattern, top_level_dir, testUuid) + post_response(payload, testPort, testUuid) + # Post EOT token. + eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} + post_response(eot_payload, testPort, testUuid) + else: + print("Error: no uuid provided or parsed.") + eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} + post_response(eot_payload, testPort, "") diff --git a/extensions/positron-python/pythonFiles/unittestadapter/execution.py b/extensions/positron-python/pythonFiles/unittestadapter/execution.py index f239f81c2d87..0684ada8e44b 100644 --- a/extensions/positron-python/pythonFiles/unittestadapter/execution.py +++ b/extensions/positron-python/pythonFiles/unittestadapter/execution.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import argparse +import atexit import enum import json import os @@ -17,40 +17,17 @@ sys.path.append(os.fspath(script_dir)) sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -from typing_extensions import NotRequired, TypeAlias, TypedDict - from testing_tools import process_json_util, socket_manager +from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict from unittestadapter.utils import parse_unittest_args DEFAULT_PORT = "45454" - -def parse_execution_cli_args( - args: List[str], -) -> Tuple[int, Union[str, None]]: - """Parse command-line arguments that should be processed by the script. - - So far this includes the port number that it needs to connect to, the uuid passed by the TS side, - and the list of test ids to report. - The port is passed to the execution.py script when it is executed, and - defaults to DEFAULT_PORT if it can't be parsed. - The list of test ids is passed to the execution.py script when it is executed, and defaults to an empty list if it can't be parsed. - The uuid should be passed to the execution.py script when it is executed, and defaults to None if it can't be parsed. - If the arguments appear several times, the value returned by parse_cli_args will be the value of the last argument. - """ - arg_parser = argparse.ArgumentParser() - arg_parser.add_argument("--port", default=DEFAULT_PORT) - arg_parser.add_argument("--uuid") - parsed_args, _ = arg_parser.parse_known_args(args) - - return (int(parsed_args.port), parsed_args.uuid) - - ErrorType = Union[ Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None] ] -PORT = 0 -UUID = 0 +testPort = 0 +testUuid = 0 START_DIR = "" @@ -147,9 +124,9 @@ def formatResult( "subtest": subtest.id() if subtest else None, } self.formatted[test_id] = result - if PORT == 0 or UUID == 0: + if testPort == 0 or testUuid == 0: print("Error sending response, port or uuid unknown to python server.") - send_run_data(result, PORT, UUID) + send_run_data(result, testPort, testUuid) class TestExecutionStatus(str, enum.Enum): @@ -168,6 +145,13 @@ class PayloadDict(TypedDict): error: NotRequired[str] +class EOTPayloadDict(TypedDict): + """A dictionary that is used to send a end of transmission post request to the server.""" + + command_type: Union[Literal["discovery"], Literal["execution"]] + eot: bool + + # Args: start_path path to a directory or a file, list of ids that may be empty. # Edge cases: # - if tests got deleted since the VS Code side last ran discovery and the current test run, @@ -225,8 +209,11 @@ def run_tests( return payload +__socket = None +atexit.register(lambda: __socket.close() if __socket else None) + + def send_run_data(raw_data, port, uuid): - # Build the request data (it has to be a POST request or the Node side will not process it), and send it. status = raw_data["outcome"] cwd = os.path.abspath(START_DIR) if raw_data["subtest"]: @@ -236,7 +223,22 @@ def send_run_data(raw_data, port, uuid): test_dict = {} test_dict[test_id] = raw_data payload: PayloadDict = {"cwd": cwd, "status": status, "result": test_dict} + post_response(payload, port, uuid) + + +def post_response( + payload: Union[PayloadDict, EOTPayloadDict], port: int, uuid: str +) -> None: + # Build the request data (it has to be a POST request or the Node side will not process it), and send it. addr = ("localhost", port) + global __socket + if __socket is None: + try: + __socket = socket_manager.SocketManager(addr) + __socket.connect() + except Exception as error: + print(f"Plugin error connection error[vscode-pytest]: {error}") + __socket = None data = json.dumps(payload) request = f"""Content-Length: {len(data)} Content-Type: application/json @@ -244,11 +246,10 @@ def send_run_data(raw_data, port, uuid): {data}""" try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Error sending response: {e}") + if __socket is not None and __socket.socket is not None: + __socket.socket.sendall(request.encode("utf-8")) + except Exception as ex: + print(f"Error sending response: {ex}") print(f"Request data: {request}") @@ -297,11 +298,12 @@ def send_run_data(raw_data, port, uuid): print(f"Error: Could not connect to runTestIdsPort: {e}") print("Error: Could not connect to runTestIdsPort") - PORT, UUID = parse_execution_cli_args(argv[:index]) + testPort = int(os.environ.get("TEST_PORT", DEFAULT_PORT)) + testUuid = os.environ.get("TEST_UUID") if test_ids_from_buffer: # Perform test execution. payload = run_tests( - start_dir, test_ids_from_buffer, pattern, top_level_dir, UUID + start_dir, test_ids_from_buffer, pattern, top_level_dir, testUuid ) else: cwd = os.path.abspath(start_dir) @@ -312,3 +314,9 @@ def send_run_data(raw_data, port, uuid): "error": "No test ids received from buffer", "result": None, } + eot_payload: EOTPayloadDict = {"command_type": "execution", "eot": True} + if testUuid is None: + print("Error sending response, uuid unknown to python server.") + post_response(eot_payload, testPort, "unknown") + else: + post_response(eot_payload, testPort, testUuid) diff --git a/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py b/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py index adf72c134119..2fab4d77c2f8 100644 --- a/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py +++ b/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py @@ -1,7 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import atexit import json import os import pathlib import sys +import time import traceback import pytest @@ -126,7 +131,6 @@ def get_absolute_test_id(test_id: str, testPath: pathlib.Path) -> str: """ split_id = test_id.split("::")[1:] absolute_test_id = "::".join([str(testPath), *split_id]) - print("absolute path", absolute_test_id) return absolute_test_id @@ -196,8 +200,9 @@ def pytest_report_teststatus(report, config): elif report.failed: report_value = "failure" message = report.longreprtext - node_path = map_id_to_path[report.nodeid] - if not node_path: + try: + node_path = map_id_to_path[report.nodeid] + except KeyError: node_path = cwd # Calculate the absolute test id and use this as the ID moving forward. absolute_node_id = get_absolute_test_id(report.nodeid, node_path) @@ -302,12 +307,6 @@ def pytest_sessionfinish(session, exitstatus): 4: Pytest encountered an internal error or exception during test execution. 5: Pytest was unable to find any tests to run. """ - print( - "pytest session has finished, exit status: ", - exitstatus, - "in discovery? ", - IS_DISCOVERY, - ) cwd = pathlib.Path.cwd() if IS_DISCOVERY: if not (exitstatus == 0 or exitstatus == 1 or exitstatus == 5): @@ -353,6 +352,10 @@ def pytest_sessionfinish(session, exitstatus): exitstatus_bool, None, ) + # send end of transmission token + command_type = "discovery" if IS_DISCOVERY else "execution" + payload: EOTPayloadDict = {"command_type": command_type, "eot": True} + send_post_request(payload) def build_test_tree(session: pytest.Session) -> TestNode: @@ -393,9 +396,9 @@ def build_test_tree(session: pytest.Session) -> TestNode: elif hasattr(test_case, "callspec"): # This means it is a parameterized test. function_name: str = "" # parameterized test cases cut the repetitive part of the name off. - name_split = test_node["name"].split("[") - test_node["name"] = "[" + name_split[1] - parent_path = os.fspath(get_node_path(test_case)) + "::" + name_split[0] + parent_part, parameterized_section = test_node["name"].split("[", 1) + test_node["name"] = "[" + parameterized_section + parent_path = os.fspath(get_node_path(test_case)) + "::" + parent_part try: function_name = test_case.originalname # type: ignore function_test_case = function_nodes_dict[parent_path] @@ -604,44 +607,60 @@ class ExecutionPayloadDict(Dict): error: Union[str, None] # Currently unused need to check +class EOTPayloadDict(TypedDict): + """A dictionary that is used to send a end of transmission post request to the server.""" + + command_type: Union[Literal["discovery"], Literal["execution"]] + eot: bool + + def get_node_path(node: Any) -> pathlib.Path: + """A function that returns the path of a node given the switch to pathlib.Path.""" return getattr(node, "path", pathlib.Path(node.fspath)) +__socket = None +atexit.register(lambda: __socket.close() if __socket else None) + + def execution_post( - cwd: str, - status: Literal["success", "error"], - tests: Union[testRunResultDict, None], + cwd: str, status: Literal["success", "error"], tests: Union[testRunResultDict, None] ): """ - Sends a post request to the server after the tests have been executed. - Keyword arguments: - cwd -- the current working directory. - session_node -- the status of running the tests - tests -- the tests that were run and their status. + Sends a POST request with execution payload details. + + Args: + cwd (str): Current working directory. + status (Literal["success", "error"]): Execution status indicating success or error. + tests (Union[testRunResultDict, None]): Test run results, if available. """ - testPort = os.getenv("TEST_PORT", 45454) - testuuid = os.getenv("TEST_UUID") + payload: ExecutionPayloadDict = ExecutionPayloadDict( cwd=cwd, status=status, result=tests, not_found=None, error=None ) if ERRORS: payload["error"] = ERRORS + send_post_request(payload) - addr = ("localhost", int(testPort)) - data = json.dumps(payload) - request = f"""Content-Length: {len(data)} -Content-Type: application/json -Request-uuid: {testuuid} -{data}""" - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Plugin error connection error[vscode-pytest]: {e}") - print(f"[vscode-pytest] data: {request}") +def post_response(cwd: str, session_node: TestNode) -> None: + """ + Sends a POST request with test session details in payload. + + Args: + cwd (str): Current working directory. + session_node (TestNode): Node information of the test session. + """ + + payload: DiscoveryPayloadDict = { + "cwd": cwd, + "status": "success" if not ERRORS else "error", + "tests": session_node, + "error": [], + } + if ERRORS is not None: + payload["error"] = ERRORS + send_post_request(payload, cls_encoder=PathEncoder) class PathEncoder(json.JSONEncoder): @@ -653,35 +672,55 @@ def default(self, obj): return super().default(obj) -def post_response(cwd: str, session_node: TestNode) -> None: - """Sends a post request to the server. +def send_post_request( + payload: Union[ExecutionPayloadDict, DiscoveryPayloadDict, EOTPayloadDict], + cls_encoder=None, +): + """ + Sends a post request to the server. Keyword arguments: - cwd -- the current working directory. - session_node -- the session node, which is the top of the testing tree. - errors -- a list of errors that occurred during test collection. + payload -- the payload data to be sent. + cls_encoder -- a custom encoder if needed. """ - payload: DiscoveryPayloadDict = { - "cwd": cwd, - "status": "success" if not ERRORS else "error", - "tests": session_node, - "error": [], - } - if ERRORS is not None: - payload["error"] = ERRORS - test_port: Union[str, int] = os.getenv("TEST_PORT", 45454) - test_uuid: Union[str, None] = os.getenv("TEST_UUID") - addr = "localhost", int(test_port) - data = json.dumps(payload, cls=PathEncoder) + testPort = os.getenv("TEST_PORT", 45454) + testUuid = os.getenv("TEST_UUID") + addr = ("localhost", int(testPort)) + global __socket + + if __socket is None: + try: + __socket = socket_manager.SocketManager(addr) + __socket.connect() + except Exception as error: + print(f"Plugin error connection error[vscode-pytest]: {error}") + __socket = None + + data = json.dumps(payload, cls=cls_encoder) request = f"""Content-Length: {len(data)} Content-Type: application/json -Request-uuid: {test_uuid} +Request-uuid: {testUuid} {data}""" - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Plugin error connection error[vscode-pytest]: {e}") - print(f"[vscode-pytest] data: {request}") + + max_retries = 3 + retries = 0 + while retries < max_retries: + try: + if __socket is not None and __socket.socket is not None: + __socket.socket.sendall(request.encode("utf-8")) + # print("Post request sent successfully!") + # print("data sent", payload, "end of data") + break # Exit the loop if the send was successful + else: + print("Plugin error connection error[vscode-pytest]") + print(f"[vscode-pytest] data: {request}") + except Exception as error: + print(f"Plugin error connection error[vscode-pytest]: {error}") + print(f"[vscode-pytest] data: {request}") + retries += 1 # Increment retry counter + if retries < max_retries: + print(f"Retrying ({retries}/{max_retries}) in 2 seconds...") + time.sleep(2) # Wait for a short duration before retrying + else: + print("Maximum retry attempts reached. Cannot send post request.") diff --git a/extensions/positron-python/pythonFiles/vscode_pytest/run_pytest_script.py b/extensions/positron-python/pythonFiles/vscode_pytest/run_pytest_script.py index ffb4d0c55b16..0fca8208a406 100644 --- a/extensions/positron-python/pythonFiles/vscode_pytest/run_pytest_script.py +++ b/extensions/positron-python/pythonFiles/vscode_pytest/run_pytest_script.py @@ -53,7 +53,7 @@ buffer = b"" # Process the JSON data - print(f"Received JSON data: {test_ids_from_buffer}") + print("Received JSON data in run script") break except json.JSONDecodeError: # JSON decoding error, the complete JSON object is not yet received diff --git a/extensions/positron-python/requirements.txt b/extensions/positron-python/requirements.txt index f2af0ca4204b..205b9fc4804c 100644 --- a/extensions/positron-python/requirements.txt +++ b/extensions/positron-python/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.7 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # pip-compile --generate-hashes requirements.in @@ -12,9 +12,9 @@ microvenv==2023.2.0 \ --hash=sha256:5b46296d6a65992946da504bd9e724a5becf5c256091f2f9383e5b4e9f567f23 \ --hash=sha256:a07e88a8fb5ee90219b86dd90095cb5646462d45d30285ea3b1a3c7cf33616d3 # via -r requirements.in -packaging==23.1 \ - --hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \ - --hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f +packaging==23.2 \ + --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ + --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 # via -r requirements.in tomli==2.0.1 \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ @@ -23,9 +23,7 @@ tomli==2.0.1 \ typing-extensions==4.7.1 \ --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 - # via - # -r requirements.in - # importlib-metadata + # via -r requirements.in zipp==3.15.0 \ --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 diff --git a/extensions/positron-python/scripts/onCreateCommand.sh b/extensions/positron-python/scripts/onCreateCommand.sh index a90a5366417d..6303d21ef486 100644 --- a/extensions/positron-python/scripts/onCreateCommand.sh +++ b/extensions/positron-python/scripts/onCreateCommand.sh @@ -12,15 +12,15 @@ command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH" source ~/.bashrc # Install Python via pyenv . -pyenv install 3.7:latest 3.8:latest 3.9:latest 3.10:latest 3.11:latest +pyenv install 3.8:latest 3.9:latest 3.10:latest 3.11:latest -# Set default Python version to 3.7 . -pyenv global 3.7.17 +# Set default Python version to 3.8 . +pyenv global 3.8.18 npm ci # Create Virutal environment. -pyenv exec python3.7 -m venv .venv +pyenv exec python3.8 -m venv .venv # Activate Virtual environment. source /workspaces/vscode-python/.venv/bin/activate diff --git a/extensions/positron-python/src/client/application/diagnostics/checks/macPythonInterpreter.ts b/extensions/positron-python/src/client/application/diagnostics/checks/macPythonInterpreter.ts index 19ccc2f8beb9..d44876acd4b9 100644 --- a/extensions/positron-python/src/client/application/diagnostics/checks/macPythonInterpreter.ts +++ b/extensions/positron-python/src/client/application/diagnostics/checks/macPythonInterpreter.ts @@ -40,7 +40,9 @@ export const InvalidMacPythonInterpreterServiceId = 'InvalidMacPythonInterpreter export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { protected changeThrottleTimeout = 1000; - private timeOut?: NodeJS.Timer | number; + // --- Start Positron --- + private timeOut?: NodeJS.Timeout | number; + // --- End Positron --- constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer, diff --git a/extensions/positron-python/src/client/common/application/commands.ts b/extensions/positron-python/src/client/common/application/commands.ts index 2567eca4a69d..b408aae52181 100644 --- a/extensions/positron-python/src/client/common/application/commands.ts +++ b/extensions/positron-python/src/client/common/application/commands.ts @@ -23,8 +23,6 @@ interface ICommandNameWithoutArgumentTypeMapping { [Commands.ClearWorkspaceInterpreter]: []; [Commands.Set_Interpreter]: []; [Commands.Set_ShebangInterpreter]: []; - [Commands.Run_Linter]: []; - [Commands.Enable_Linter]: []; ['workbench.action.showCommands']: []; ['workbench.action.debug.continue']: []; ['workbench.action.debug.stepOver']: []; @@ -35,7 +33,6 @@ interface ICommandNameWithoutArgumentTypeMapping { ['editor.action.formatDocument']: []; ['editor.action.rename']: []; [Commands.ViewOutput]: []; - [Commands.Set_Linter]: []; [Commands.Start_REPL]: []; [Commands.Enable_SourceMap_Support]: []; [Commands.Exec_Selection_In_Terminal]: []; diff --git a/extensions/positron-python/src/client/common/constants.ts b/extensions/positron-python/src/client/common/constants.ts index 9239e5fec4bf..07a283011281 100644 --- a/extensions/positron-python/src/client/common/constants.ts +++ b/extensions/positron-python/src/client/common/constants.ts @@ -38,9 +38,9 @@ export namespace Commands { export const ClearWorkspaceInterpreter = 'python.clearWorkspaceInterpreter'; export const Create_Environment = 'python.createEnvironment'; export const Create_Environment_Button = 'python.createEnvironment-button'; + export const Create_Environment_Check = 'python.createEnvironmentCheck'; export const Create_Terminal = 'python.createTerminal'; export const Debug_In_Terminal = 'python.debugInTerminal'; - export const Enable_Linter = 'python.enableLinting'; export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; export const Exec_In_Terminal = 'python.execInTerminal'; export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon'; @@ -59,9 +59,7 @@ export namespace Commands { export const PickLocalProcess = 'python.pickLocalProcess'; export const RefreshTensorBoard = 'python.refreshTensorBoard'; export const ReportIssue = 'python.reportIssue'; - export const Run_Linter = 'python.runLinting'; export const Set_Interpreter = 'python.setInterpreter'; - export const Set_Linter = 'python.setLinter'; export const Set_ShebangInterpreter = 'python.setShebangInterpreter'; export const Sort_Imports = 'python.sortImports'; export const Start_REPL = 'python.startREPL'; diff --git a/extensions/positron-python/src/client/common/installer/productInstaller.ts b/extensions/positron-python/src/client/common/installer/productInstaller.ts index f6d27b9edabe..b18733c86100 100644 --- a/extensions/positron-python/src/client/common/installer/productInstaller.ts +++ b/extensions/positron-python/src/client/common/installer/productInstaller.ts @@ -6,12 +6,10 @@ import { CancellationToken, l10n, MessageOptions, Uri } from 'vscode'; import '../extensions'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { LinterId } from '../../linters/types'; import { EnvironmentType, ModuleInstallerType, PythonEnvironment } from '../../pythonEnvironments/info'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../application/types'; -import { Commands } from '../constants'; +import { IApplicationShell, IWorkspaceService } from '../application/types'; import { IProcessServiceFactory, IPythonExecutionFactory } from '../process/types'; import { IConfigurationService, @@ -22,7 +20,7 @@ import { Product, ProductType, } from '../types'; -import { Common, Linters } from '../utils/localize'; +import { Common } from '../utils/localize'; import { isResource, noop } from '../utils/misc'; import { translateProductToModule } from './moduleInstaller'; import { ProductNames } from './productNames'; @@ -235,166 +233,6 @@ abstract class BaseInstaller implements IBaseInstaller { } } -const doNotDisplayFormatterPromptStateKey = 'FORMATTER_NOT_INSTALLED_KEY'; - -export class FormatterInstaller extends BaseInstaller { - protected async promptToInstallImplementation( - product: Product, - resource?: Uri, - cancel?: CancellationToken, - _flags?: ModuleInstallFlags, - // --- Start Positron --- - _options?: InstallOptions, - _messageOptions?: MessageOptions, - // --- End Positron --- - ): Promise { - const neverShowAgain = this.persistentStateFactory.createGlobalPersistentState( - doNotDisplayFormatterPromptStateKey, - false, - ); - - if (neverShowAgain.value) { - return InstallerResponse.Ignore; - } - - // Hard-coded on purpose because the UI won't necessarily work having - // another formatter. - const formatters = [Product.autopep8, Product.black, Product.yapf]; - const formatterNames = formatters.map((formatter) => ProductNames.get(formatter)!); - const productName = ProductNames.get(product)!; - formatterNames.splice(formatterNames.indexOf(productName), 1); - const useOptions = formatterNames.map((name) => l10n.t('Use {0}', name)); - const yesChoice = Common.bannerLabelYes; - - const options = [...useOptions, Common.doNotShowAgain]; - let message = l10n.t('Formatter {0} is not installed. Install?', productName); - if (this.isExecutableAModule(product, resource)) { - options.splice(0, 0, yesChoice); - } else { - const executable = this.getExecutableNameFromSettings(product, resource); - message = l10n.t('Path to the {0} formatter is invalid ({1})', productName, executable); - } - - const item = await this.appShell.showErrorMessage(message, ...options); - if (item === yesChoice) { - return this.install(product, resource, cancel); - } - - if (item === Common.doNotShowAgain) { - neverShowAgain.updateValue(true); - return InstallerResponse.Ignore; - } - - if (typeof item === 'string') { - for (const formatter of formatters) { - const formatterName = ProductNames.get(formatter)!; - - if (item.endsWith(formatterName)) { - await this.configService.updateSetting('formatting.provider', formatterName, resource); - return this.install(formatter, resource, cancel); - } - } - } - - return InstallerResponse.Ignore; - } -} - -export class LinterInstaller extends BaseInstaller { - constructor(protected serviceContainer: IServiceContainer) { - super(serviceContainer); - } - - protected async promptToInstallImplementation( - product: Product, - resource?: Uri, - cancel?: CancellationToken, - _flags?: ModuleInstallFlags, - // --- Start Positron --- - _options?: InstallOptions, - _messageOptions?: MessageOptions, - // --- End Positron --- - ): Promise { - return this.oldPromptForInstallation(product, resource, cancel); - } - - /** - * For installers that want to avoid prompting the user over and over, they can make use of a - * persisted true/false value representing user responses to 'stop showing this prompt'. This method - * gets the persisted value given the installer-defined key. - * - * @param key Key to use to get a persisted response value, each installer must define this for themselves. - * @returns Boolean: The current state of the stored response key given. - */ - protected getStoredResponse(key: string): boolean { - const factory = this.serviceContainer.get(IPersistentStateFactory); - const state = factory.createGlobalPersistentState(key, undefined); - return state.value === true; - } - - private async oldPromptForInstallation(product: Product, resource?: Uri, cancel?: CancellationToken) { - const productName = ProductNames.get(product)!; - const { install } = Common; - const { doNotShowAgain } = Common; - const disableLinterInstallPromptKey = `${productName}_DisableLinterInstallPrompt`; - const { selectLinter } = Linters; - - if (this.getStoredResponse(disableLinterInstallPromptKey) === true) { - return InstallerResponse.Ignore; - } - - const options = [selectLinter, doNotShowAgain]; - - let message = l10n.t('Linter {0} is not installed.', productName); - if (this.isExecutableAModule(product, resource)) { - options.splice(0, 0, install); - } else { - const executable = this.getExecutableNameFromSettings(product, resource); - message = l10n.t('Path to the {0} linter is invalid ({1})', productName, executable); - } - const response = await this.appShell.showErrorMessage(message, ...options); - if (response === install) { - sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { - tool: productName as LinterId, - action: 'install', - }); - return this.install(product, resource, cancel); - } - if (response === doNotShowAgain) { - await this.setStoredResponse(disableLinterInstallPromptKey, true); - sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { - tool: productName as LinterId, - action: 'disablePrompt', - }); - return InstallerResponse.Ignore; - } - - if (response === selectLinter) { - sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { action: 'select' }); - const commandManager = this.serviceContainer.get(ICommandManager); - await commandManager.executeCommand(Commands.Set_Linter); - } - return InstallerResponse.Ignore; - } - - /** - * For installers that want to avoid prompting the user over and over, they can make use of a - * persisted true/false value representing user responses to 'stop showing this prompt'. This - * method will set that persisted value given the installer-defined key. - * - * @param key Key to use to get a persisted response value, each installer must define this for themselves. - * @param value Boolean value to store for the user - if they choose to not be prompted again for instance. - * @returns Boolean: The current state of the stored response key given. - */ - private async setStoredResponse(key: string, value: boolean): Promise { - const factory = this.serviceContainer.get(IPersistentStateFactory); - const state = factory.createGlobalPersistentState(key, undefined); - if (state && state.value !== value) { - await state.updateValue(value); - } - } -} - export class TestFrameworkInstaller extends BaseInstaller { protected async promptToInstallImplementation( product: Product, @@ -733,10 +571,6 @@ export class ProductInstaller implements IInstaller { private createInstaller(product: Product): IBaseInstaller { const productType = this.productService.getProductType(product); switch (productType) { - case ProductType.Formatter: - return new FormatterInstaller(this.serviceContainer); - case ProductType.Linter: - return new LinterInstaller(this.serviceContainer); case ProductType.TestFramework: return new TestFrameworkInstaller(this.serviceContainer); case ProductType.DataScience: diff --git a/extensions/positron-python/src/client/common/persistentState.ts b/extensions/positron-python/src/client/common/persistentState.ts index 76f6d2112fe0..2959a2dc8216 100644 --- a/extensions/positron-python/src/client/common/persistentState.ts +++ b/extensions/positron-python/src/client/common/persistentState.ts @@ -20,6 +20,46 @@ import { import { cache } from './utils/decorators'; import { noop } from './utils/misc'; +let _workspaceState: Memento | undefined; +const _workspaceKeys: string[] = []; +export function initializePersistentStateForTriggers(context: IExtensionContext) { + _workspaceState = context.workspaceState; +} + +export function getWorkspaceStateValue(key: string, defaultValue?: T): T | undefined { + if (!_workspaceState) { + throw new Error('Workspace state not initialized'); + } + if (defaultValue === undefined) { + return _workspaceState.get(key); + } + return _workspaceState.get(key, defaultValue); +} + +export async function updateWorkspaceStateValue(key: string, value: T): Promise { + if (!_workspaceState) { + throw new Error('Workspace state not initialized'); + } + try { + _workspaceKeys.push(key); + await _workspaceState.update(key, value); + const after = getWorkspaceStateValue(key); + if (JSON.stringify(after) !== JSON.stringify(value)) { + await _workspaceState.update(key, undefined); + await _workspaceState.update(key, value); + traceError('Error while updating workspace state for key:', key); + } + } catch (ex) { + traceError(`Error while updating workspace state for key [${key}]:`, ex); + } +} + +async function clearWorkspaceState(): Promise { + if (_workspaceState !== undefined) { + await Promise.all(_workspaceKeys.map((key) => updateWorkspaceStateValue(key, undefined))); + } +} + export class PersistentState implements IPersistentState { constructor( public readonly storage: Memento, @@ -93,7 +133,10 @@ export class PersistentStateFactory implements IPersistentStateFactory, IExtensi ) {} public async activate(): Promise { - this.cmdManager?.registerCommand(Commands.ClearStorage, this.cleanAllPersistentStates.bind(this)); + this.cmdManager?.registerCommand(Commands.ClearStorage, async () => { + await clearWorkspaceState(); + await this.cleanAllPersistentStates(); + }); const globalKeysStorageDeprecated = this.createGlobalPersistentState(GLOBAL_PERSISTENT_KEYS_DEPRECATED, []); const workspaceKeysStorageDeprecated = this.createWorkspacePersistentState( WORKSPACE_PERSISTENT_KEYS_DEPRECATED, diff --git a/extensions/positron-python/src/client/common/process/rawProcessApis.ts b/extensions/positron-python/src/client/common/process/rawProcessApis.ts index 6f3e40d68736..23c2f3253bb1 100644 --- a/extensions/positron-python/src/client/common/process/rawProcessApis.ts +++ b/extensions/positron-python/src/client/common/process/rawProcessApis.ts @@ -257,6 +257,10 @@ export function execObservable( subscriber.error(ex); internalDisposables.forEach((d) => d.dispose()); }); + if (options.stdinStr !== undefined) { + proc.stdin?.write(options.stdinStr); + proc.stdin?.end(); + } }); return { diff --git a/extensions/positron-python/src/client/common/utils/localize.ts b/extensions/positron-python/src/client/common/utils/localize.ts index 4cda15e15ec0..bc32c1078cad 100644 --- a/extensions/positron-python/src/client/common/utils/localize.ts +++ b/extensions/positron-python/src/client/common/utils/localize.ts @@ -415,7 +415,7 @@ export namespace Testing { export const cancelUnittestExecution = l10n.t('Canceled unittest test execution'); export const errorUnittestExecution = l10n.t('Unittest test execution error'); export const cancelPytestExecution = l10n.t('Canceled pytest test execution'); - export const errorPytestExecution = l10n.t('Pytest test execution error'); + export const errorPytestExecution = l10n.t('pytest test execution error'); } export namespace OutdatedDebugger { @@ -464,8 +464,10 @@ export namespace CreateEnv { export const error = l10n.t('Creating virtual environment failed with error.'); export const tomlExtrasQuickPickTitle = l10n.t('Select optional dependencies to install from pyproject.toml'); export const requirementsQuickPickTitle = l10n.t('Select dependencies to install'); - export const recreate = l10n.t('Recreate'); - export const recreateDescription = l10n.t('Delete existing ".venv" environment and create a new one'); + export const recreate = l10n.t('Delete and Recreate'); + export const recreateDescription = l10n.t( + 'Delete existing ".venv" directory and create a new ".venv" environment', + ); export const useExisting = l10n.t('Use Existing'); export const useExistingDescription = l10n.t('Use existing ".venv" environment with no changes to it'); export const existingVenvQuickPickPlaceholder = l10n.t( @@ -473,6 +475,7 @@ export namespace CreateEnv { ); export const deletingEnvironmentProgress = l10n.t('Deleting existing ".venv" environment...'); export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".venv" environment.'); + export const openRequirementsFile = l10n.t('Open requirements file'); } export namespace Conda { @@ -485,6 +488,25 @@ export namespace CreateEnv { ); export const creating = l10n.t('Creating conda environment...'); export const providerDescription = l10n.t('Creates a `.conda` Conda environment in the current workspace'); + + export const recreate = l10n.t('Delete and Recreate'); + export const recreateDescription = l10n.t('Delete existing ".conda" environment and create a new one'); + export const useExisting = l10n.t('Use Existing'); + export const useExistingDescription = l10n.t('Use existing ".conda" environment with no changes to it'); + export const existingCondaQuickPickPlaceholder = l10n.t( + 'Choose an option to handle the existing ".conda" environment', + ); + export const deletingEnvironmentProgress = l10n.t('Deleting existing ".conda" environment...'); + export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".conda" environment.'); + } + + export namespace Trigger { + export const workspaceTriggerMessage = l10n.t( + 'A virtual environment is not currently selected for your Python interpreter. Would you like to create a virtual environment?', + ); + export const createEnvironment = l10n.t('Create'); + export const disableCheck = l10n.t('Disable'); + export const disableCheckWorkspace = l10n.t('Disable (Workspace)'); } } diff --git a/extensions/positron-python/src/client/common/utils/multiStepInput.ts b/extensions/positron-python/src/client/common/utils/multiStepInput.ts index e44879e8bbbb..e2b2567b5b4e 100644 --- a/extensions/positron-python/src/client/common/utils/multiStepInput.ts +++ b/extensions/positron-python/src/client/common/utils/multiStepInput.ts @@ -47,7 +47,7 @@ export interface IQuickPickParameters { totalSteps?: number; canGoBack?: boolean; items: T[]; - activeItem?: T | Promise; + activeItem?: T | ((quickPick: QuickPick) => Promise); placeholder: string | undefined; customButtonSetups?: QuickInputButtonSetup[]; matchOnDescription?: boolean; @@ -156,7 +156,13 @@ export class MultiStepInput implements IMultiStepInput { initialize(input); } if (activeItem) { - input.activeItems = [await activeItem]; + if (typeof activeItem === 'function') { + activeItem(input).then((item) => { + if (input.activeItems.length === 0) { + input.activeItems = [item]; + } + }); + } } else { input.activeItems = []; } diff --git a/extensions/positron-python/src/client/common/vscodeApis/windowApis.ts b/extensions/positron-python/src/client/common/vscodeApis/windowApis.ts index 1c242314cb87..c761ff60fa65 100644 --- a/extensions/positron-python/src/client/common/vscodeApis/windowApis.ts +++ b/extensions/positron-python/src/client/common/vscodeApis/windowApis.ts @@ -16,9 +16,15 @@ import { TextEditor, window, Disposable, + QuickPickItemButtonEvent, + Uri, } from 'vscode'; import { createDeferred, Deferred } from '../utils/async'; +export function showTextDocument(uri: Uri): Thenable { + return window.showTextDocument(uri); +} + export function showQuickPick( items: readonly T[] | Thenable, options?: QuickPickOptions, @@ -91,6 +97,7 @@ export async function showQuickPickWithBack( items: readonly T[], options?: QuickPickOptions, token?: CancellationToken, + itemButtonHandler?: (e: QuickPickItemButtonEvent) => void, ): Promise { const quickPick: QuickPick = window.createQuickPick(); const disposables: Disposable[] = [quickPick]; @@ -130,6 +137,11 @@ export async function showQuickPickWithBack( deferred.resolve(undefined); } }), + quickPick.onDidTriggerItemButton((e) => { + if (itemButtonHandler) { + itemButtonHandler(e); + } + }), ); if (token) { disposables.push( diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts b/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts index e4c14de407c9..21c8d0f1147b 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts @@ -41,7 +41,7 @@ export class InterpreterPathCommand implements IExtensionSingleActivationService let workspaceFolderUri; try { - workspaceFolderUri = workspaceFolder ? Uri.parse(workspaceFolder) : undefined; + workspaceFolderUri = workspaceFolder ? Uri.file(workspaceFolder) : undefined; } catch (ex) { workspaceFolderUri = undefined; } diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/launch.ts b/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/launch.ts index c4ae6a204d71..274758797eb9 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -16,6 +16,12 @@ import { DebuggerTypeName } from '../../../constants'; import { DebugOptions, DebugPurpose, LaunchRequestArguments } from '../../../types'; import { BaseConfigurationResolver } from './base'; import { getProgram, IDebugEnvironmentVariablesService } from './helper'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../../../pythonEnvironments/creation/createEnvironmentTrigger'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; @injectable() export class LaunchConfigurationResolver extends BaseConfigurationResolver { @@ -84,6 +90,8 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver debugConfiguration.debugOptions!.indexOf(item) === pos, ); } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.Workspace, workspaceFolder); return debugConfiguration; } diff --git a/extensions/positron-python/src/client/debugger/extension/debugCommands.ts b/extensions/positron-python/src/client/debugger/extension/debugCommands.ts index 14a108d27793..b3322e8e7dd1 100644 --- a/extensions/positron-python/src/client/debugger/extension/debugCommands.ts +++ b/extensions/positron-python/src/client/debugger/extension/debugCommands.ts @@ -14,6 +14,10 @@ import { DebugPurpose, LaunchRequestArguments } from '../types'; import { IInterpreterService } from '../../interpreter/contracts'; import { noop } from '../../common/utils/misc'; import { getConfigurationsByUri } from './configuration/launch.json/launchJsonReader'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../pythonEnvironments/creation/createEnvironmentTrigger'; @injectable() export class DebugCommands implements IExtensionSingleActivationService { @@ -35,6 +39,8 @@ export class DebugCommands implements IExtensionSingleActivationService { this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); return; } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug-in-terminal' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); const config = await DebugCommands.getDebugConfiguration(file); this.debugService.startDebugging(undefined, config); }), diff --git a/extensions/positron-python/src/client/extensionActivation.ts b/extensions/positron-python/src/client/extensionActivation.ts index 8dcea063676a..807698f3ec29 100644 --- a/extensions/positron-python/src/client/extensionActivation.ts +++ b/extensions/positron-python/src/client/extensionActivation.ts @@ -28,7 +28,6 @@ import { IDebugConfigurationService, IDynamicDebugConfigurationService } from '. import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; import { IInterpreterService } from './interpreter/contracts'; import { getLanguageConfiguration } from './language/languageConfiguration'; -import { LinterCommands } from './linters/linterCommands'; import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; import { PythonFormattingEditProvider } from './providers/formatProvider'; import { ReplProvider } from './providers/replProvider'; @@ -54,6 +53,8 @@ import { DynamicPythonDebugConfigurationService } from './debugger/extension/con import { IInterpreterQuickPick } from './interpreter/configuration/types'; import { registerInstallFormatterPrompt } from './providers/prompts/installFormatterPrompt'; import { registerAllCreateEnvironmentFeatures } from './pythonEnvironments/creation/registrations'; +import { registerCreateEnvironmentTriggers } from './pythonEnvironments/creation/createEnvironmentTrigger'; +import { initializePersistentStateForTriggers } from './common/persistentState'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -168,8 +169,6 @@ async function activateLegacy(ext: ExtensionState): Promise { serviceManager.get(ICodeExecutionManager).registerCommands(); - disposables.push(new LinterCommands(serviceManager)); - if ( pythonSettings && pythonSettings.formatting && @@ -202,6 +201,8 @@ async function activateLegacy(ext: ExtensionState): Promise { ); registerInstallFormatterPrompt(serviceContainer); + registerCreateEnvironmentTriggers(disposables); + initializePersistentStateForTriggers(ext.context); } } diff --git a/extensions/positron-python/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/extensions/positron-python/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 9015dd7b9388..c11ec221d4d7 100644 --- a/extensions/positron-python/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/extensions/positron-python/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -36,6 +36,8 @@ import { getSearchPathEnvVarNames } from '../../common/utils/exec'; import { EnvironmentVariables } from '../../common/variables/types'; import { TerminalShellType } from '../../common/terminal/types'; import { OSType } from '../../common/utils/platform'; +import { normCase } from '../../common/platform/fs-paths'; +import { PythonEnvType } from '../../pythonEnvironments/base/info'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { @@ -62,6 +64,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ */ private processEnvVars: EnvironmentVariables | undefined; + private separator: string; + constructor( @inject(IPlatformService) private readonly platform: IPlatformService, @inject(IInterpreterService) private interpreterService: IInterpreterService, @@ -74,7 +78,9 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(IConfigurationService) private readonly configurationService: IConfigurationService, @inject(IPathUtils) private readonly pathUtils: IPathUtils, - ) {} + ) { + this.separator = platform.osType === OSType.Windows ? ';' : ':'; + } public async activate(resource: Resource): Promise { try { @@ -96,21 +102,17 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (!this.registeredOnce) { this.interpreterService.onDidChangeInterpreter( async (r) => { - this.showProgress(); await this._applyCollection(r).ignoreErrors(); - this.hideProgress(); }, this, this.disposables, ); this.applicationEnvironment.onDidChangeShell( async (shell: string) => { - this.showProgress(); this.processEnvVars = undefined; // Pass in the shell where known instead of relying on the application environment, because of bug // on VSCode: https://github.com/microsoft/vscode/issues/160694 await this._applyCollection(undefined, shell).ignoreErrors(); - this.hideProgress(); }, this, this.disposables, @@ -123,22 +125,28 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } } - public async _applyCollection(resource: Resource, shell = this.applicationEnvironment.shell): Promise { + public async _applyCollection(resource: Resource, shell?: string): Promise { + this.showProgress(); + await this._applyCollectionImpl(resource, shell); + this.hideProgress(); + } + + private async _applyCollectionImpl(resource: Resource, shell = this.applicationEnvironment.shell): Promise { const workspaceFolder = this.getWorkspaceFolder(resource); const settings = this.configurationService.getSettings(resource); const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); - // Clear any previously set env vars from collection - envVarCollection.clear(); if (!settings.terminal.activateEnvironment) { + envVarCollection.clear(); traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); return; } - const env = await this.environmentActivationService.getActivatedEnvironmentVariables( + const activatedEnv = await this.environmentActivationService.getActivatedEnvironmentVariables( resource, undefined, undefined, shell, ); + const env = activatedEnv ? normCaseKeys(activatedEnv) : undefined; if (!env) { const shellType = identifyShellFromShellPath(shell); const defaultShell = defaultShells[this.platform.osType]; @@ -149,6 +157,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ return; } await this.trackTerminalPrompt(shell, resource, env); + envVarCollection.clear(); this.processEnvVars = undefined; return; } @@ -158,11 +167,13 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ shell, ); } - const processEnv = this.processEnvVars; + const processEnv = normCaseKeys(this.processEnvVars); // PS1 in some cases is a shell variable (not an env variable) so "env" might not contain it, calculate it in that case. env.PS1 = await this.getPS1(shell, resource, env); + // Clear any previously set env vars from collection + envVarCollection.clear(); Object.keys(env).forEach((key) => { if (shouldSkip(key)) { return; @@ -192,6 +203,9 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ applyAtProcessCreation: true, }); } else { + if (!value.endsWith(this.separator)) { + value = value.concat(this.separator); + } traceVerbose(`Prepending environment variable ${key} in collection to ${value}`); envVarCollection.prepend(key, value, { applyAtShellIntegration: true, @@ -253,8 +267,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (this.platform.osType !== OSType.Windows) { // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. const interpreter = await this.interpreterService.getActiveInterpreter(resource); - const shouldPS1BeSet = interpreter?.type !== undefined; - if (shouldPS1BeSet && !env.PS1) { + const shouldSetPS1 = shouldPS1BeSet(interpreter?.type, env); + if (shouldSetPS1 && !env.PS1) { // PS1 should be set but no PS1 was set. return; } @@ -270,22 +284,25 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } private async getPS1(shell: string, resource: Resource, env: EnvironmentVariables) { - if (env.PS1) { - return env.PS1; - } const customShellType = identifyShellFromShellPath(shell); if (this.noPromptVariableShells.includes(customShellType)) { - return undefined; + return env.PS1; } if (this.platform.osType !== OSType.Windows) { // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. const interpreter = await this.interpreterService.getActiveInterpreter(resource); - const shouldPS1BeSet = interpreter?.type !== undefined; - if (shouldPS1BeSet && !env.PS1) { - // PS1 should be set but no PS1 was set. - return getPromptForEnv(interpreter); + const shouldSetPS1 = shouldPS1BeSet(interpreter?.type, env); + if (shouldSetPS1) { + const prompt = getPromptForEnv(interpreter); + if (prompt) { + return prompt; + } } } + if (env.PS1) { + // Prefer PS1 set by env vars, as env.PS1 may or may not contain the full PS1: #22056. + return env.PS1; + } return undefined; } @@ -356,6 +373,26 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } } +function shouldPS1BeSet(type: PythonEnvType | undefined, env: EnvironmentVariables): boolean { + if (env.PS1) { + // Activated variables contain PS1, meaning it was supposed to be set. + return true; + } + if (type === PythonEnvType.Virtual) { + const promptDisabledVar = env.VIRTUAL_ENV_DISABLE_PROMPT; + const isPromptDisabled = promptDisabledVar && promptDisabledVar !== undefined; + return !isPromptDisabled; + } + if (type === PythonEnvType.Conda) { + // Instead of checking config value using `conda config --get changeps1`, simply check + // `CONDA_PROMPT_MODIFER` to avoid the cost of launching the conda binary. + const promptEnabledVar = env.CONDA_PROMPT_MODIFIER; + const isPromptEnabled = promptEnabledVar && promptEnabledVar !== ''; + return !!isPromptEnabled; + } + return false; +} + function shouldSkip(env: string) { return ['_', 'SHLVL'].includes(env); } @@ -376,3 +413,11 @@ function getPromptForEnv(interpreter: PythonEnvironment | undefined) { } return undefined; } + +function normCaseKeys(env: EnvironmentVariables): EnvironmentVariables { + const result: EnvironmentVariables = {}; + Object.keys(env).forEach((key) => { + result[normCase(key)] = env[key]; + }); + return result; +} diff --git a/extensions/positron-python/src/client/interpreter/autoSelection/index.ts b/extensions/positron-python/src/client/interpreter/autoSelection/index.ts index a57577c8c918..7714c487ed30 100644 --- a/extensions/positron-python/src/client/interpreter/autoSelection/index.ts +++ b/extensions/positron-python/src/client/interpreter/autoSelection/index.ts @@ -181,6 +181,11 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio return this.stateFactory.createWorkspacePersistentState(key, undefined); } + private getAutoSelectionQueriedOnceState(): IPersistentState { + const key = `autoSelectionInterpretersQueriedOnce`; + return this.stateFactory.createWorkspacePersistentState(key, undefined); + } + /** * Auto-selection logic: * 1. If there are cached interpreters (not the first session in this workspace) @@ -200,7 +205,12 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio }); } - await this.interpreterService.refreshPromise; + const globalQueriedState = this.getAutoSelectionQueriedOnceState(); + if (!globalQueriedState.value) { + // Global interpreters are loaded the first time an extension loads, after which we don't need to + // wait on global interpreter promise refresh. + await this.interpreterService.refreshPromise; + } const interpreters = this.interpreterService.getInterpreters(resource); const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); @@ -215,6 +225,7 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio } queriedState.updateValue(true); + globalQueriedState.updateValue(true); this.didAutoSelectedInterpreterEmitter.fire(); } diff --git a/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index c0876ff518dd..9b8ecec74f9f 100644 --- a/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -50,7 +50,7 @@ import { BaseInterpreterSelectorCommand } from './base'; const untildify = require('untildify'); export type InterpreterStateArgs = { path?: string; workspace: Resource }; -type QuickPickType = IInterpreterQuickPickItem | ISpecialQuickPickItem | QuickPickItem; +export type QuickPickType = IInterpreterQuickPickItem | ISpecialQuickPickItem | QuickPickItem; function isInterpreterQuickPickItem(item: QuickPickType): item is IInterpreterQuickPickItem { return 'interpreter' in item; @@ -177,7 +177,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem items: suggestions, sortByLabel: !preserveOrderWhenFiltering, keepScrollPosition: true, - activeItem: this.getActiveItem(state.workspace, suggestions), // Use a promise here to ensure quickpick is initialized synchronously. + activeItem: (quickPick) => this.getActiveItem(state.workspace, quickPick), // Use a promise here to ensure quickpick is initialized synchronously. matchOnDetail: true, matchOnDescription: true, title, @@ -277,8 +277,9 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem return getGroupedQuickPickItems(items, recommended, workspaceFolder?.uri.fsPath); } - private async getActiveItem(resource: Resource, suggestions: QuickPickType[]) { + private async getActiveItem(resource: Resource, quickPick: QuickPick) { const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const suggestions = quickPick.items; const activeInterpreterItem = suggestions.find( (i) => isInterpreterQuickPickItem(i) && i.interpreter.id === interpreter?.id, ); @@ -339,7 +340,9 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem return false; }) : undefined; - quickPick.activeItems = activeItem ? [activeItem] : []; + if (activeItem) { + quickPick.activeItems = [activeItem]; + } } /** diff --git a/extensions/positron-python/src/client/interpreter/interpreterService.ts b/extensions/positron-python/src/client/interpreter/interpreterService.ts index b595fc2365a8..c97a35c4a973 100644 --- a/extensions/positron-python/src/client/interpreter/interpreterService.ts +++ b/extensions/positron-python/src/client/interpreter/interpreterService.ts @@ -85,6 +85,11 @@ export class InterpreterService implements Disposable, IInterpreterService { private readonly didChangeInterpreterInformation = new EventEmitter(); + private readonly activeInterpreterPaths = new Map< + string, + { path: string; workspaceFolder: WorkspaceFolder | undefined } + >(); + constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, @@ -100,10 +105,12 @@ export class InterpreterService implements Disposable, IInterpreterService { const workspaceFolder = this.serviceContainer .get(IWorkspaceService) .getWorkspaceFolder(resource); - this.ensureEnvironmentContainsPython( - this.configService.getSettings(resource).pythonPath, - workspaceFolder, - ).ignoreErrors(); + const path = this.configService.getSettings(resource).pythonPath; + const workspaceKey = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolderIdentifier(resource); + this.activeInterpreterPaths.set(workspaceKey, { path, workspaceFolder }); + this.ensureEnvironmentContainsPython(path, workspaceFolder).ignoreErrors(); } public initialize(): void { @@ -155,6 +162,16 @@ export class InterpreterService implements Disposable, IInterpreterService { const interpreter = e.old ?? e.new; if (interpreter) { this.didChangeInterpreterInformation.fire(interpreter); + for (const { path, workspaceFolder } of this.activeInterpreterPaths.values()) { + if (path === interpreter.path && !e.new) { + // If the active environment got deleted, notify it. + this.didChangeInterpreterEmitter.fire(workspaceFolder?.uri); + reportActiveInterpreterChanged({ + path, + resource: workspaceFolder, + }); + } + } } }), ); @@ -246,6 +263,10 @@ export class InterpreterService implements Disposable, IInterpreterService { path: pySettings.pythonPath, resource: workspaceFolder, }); + const workspaceKey = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolderIdentifier(resource); + this.activeInterpreterPaths.set(workspaceKey, { path: pySettings.pythonPath, workspaceFolder }); const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); interpreterDisplay.refresh().catch((ex) => traceError('Python Extension: display.refresh', ex)); await this.ensureEnvironmentContainsPython(this._pythonPathSetting, workspaceFolder); diff --git a/extensions/positron-python/src/client/linters/errorHandlers/errorHandler.ts b/extensions/positron-python/src/client/linters/errorHandlers/errorHandler.ts index dc884e97739c..af28dd61c3a4 100644 --- a/extensions/positron-python/src/client/linters/errorHandlers/errorHandler.ts +++ b/extensions/positron-python/src/client/linters/errorHandlers/errorHandler.ts @@ -3,17 +3,13 @@ import { ExecutionInfo, Product } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { IErrorHandler } from '../types'; import { BaseErrorHandler } from './baseErrorHandler'; -import { NotInstalledErrorHandler } from './notInstalled'; import { StandardErrorHandler } from './standard'; export class ErrorHandler implements IErrorHandler { private handler: BaseErrorHandler; constructor(product: Product, serviceContainer: IServiceContainer) { - // Create chain of handlers. - const standardErrorHandler = new StandardErrorHandler(product, serviceContainer); - this.handler = new NotInstalledErrorHandler(product, serviceContainer); - this.handler.setNextHandler(standardErrorHandler); + this.handler = new StandardErrorHandler(product, serviceContainer); } public handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { diff --git a/extensions/positron-python/src/client/linters/errorHandlers/notInstalled.ts b/extensions/positron-python/src/client/linters/errorHandlers/notInstalled.ts deleted file mode 100644 index 8c598ae5ece2..000000000000 --- a/extensions/positron-python/src/client/linters/errorHandlers/notInstalled.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Uri } from 'vscode'; -import { IPythonExecutionFactory } from '../../common/process/types'; -import { ExecutionInfo } from '../../common/types'; -import { traceError, traceLog, traceWarn } from '../../logging'; -import { ILinterManager } from '../types'; -import { BaseErrorHandler } from './baseErrorHandler'; - -export class NotInstalledErrorHandler extends BaseErrorHandler { - public async handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { - const pythonExecutionService = await this.serviceContainer - .get(IPythonExecutionFactory) - .create({ resource }); - const isModuleInstalled = await pythonExecutionService.isModuleInstalled(execInfo.moduleName!); - if (isModuleInstalled) { - return this.nextHandler ? this.nextHandler.handleError(error, resource, execInfo) : false; - } - - this.installer - .promptToInstall(this.product, resource) - .catch((ex) => traceError('NotInstalledErrorHandler.promptToInstall', ex)); - - const linterManager = this.serviceContainer.get(ILinterManager); - const info = linterManager.getLinterInfo(execInfo.product!); - const customError = `Linter '${info.id}' is not installed. Please install it or select another linter".`; - traceLog(`\n${customError}\n${error}`); - traceWarn(customError, error); - return true; - } -} diff --git a/extensions/positron-python/src/client/linters/errorHandlers/standard.ts b/extensions/positron-python/src/client/linters/errorHandlers/standard.ts index f6e04b50ff19..6367da7abe4a 100644 --- a/extensions/positron-python/src/client/linters/errorHandlers/standard.ts +++ b/extensions/positron-python/src/client/linters/errorHandlers/standard.ts @@ -18,6 +18,24 @@ export class StandardErrorHandler extends BaseErrorHandler { const info = linterManager.getLinterInfo(execInfo.product!); traceError(`There was an error in running the linter ${info.id}`, error); + if (info.id === LinterId.PyLint) { + traceError('Support for "pylint" is moved to ms-python.pylint extension.'); + traceError( + 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.pylint', + ); + } else if (info.id === LinterId.Flake8) { + traceError('Support for "flake8" is moved to ms-python.flake8 extension.'); + traceError( + 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.flake8', + ); + } else if (info.id === LinterId.MyPy) { + traceError('Support for "mypy" is moved to ms-python.mypy-type-checker extension.'); + traceError( + 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker', + ); + } + traceError(`If the error is due to missing ${info.id}, please install ${info.id} using pip manually.`); + traceError('Learn more here: https://aka.ms/AAlgvkb'); traceLog(`Linting with ${info.id} failed.`); traceLog(error.toString()); diff --git a/extensions/positron-python/src/client/linters/linterCommands.ts b/extensions/positron-python/src/client/linters/linterCommands.ts deleted file mode 100644 index cc35e80f26b1..000000000000 --- a/extensions/positron-python/src/client/linters/linterCommands.ts +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DiagnosticCollection, Disposable, l10n, QuickPickOptions, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager, IDocumentManager } from '../common/application/types'; -import { Commands } from '../common/constants'; -import { IDisposable } from '../common/types'; -import { Common } from '../common/utils/localize'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { ILinterManager, ILintingEngine, LinterId } from './types'; - -export class LinterCommands implements IDisposable { - private disposables: Disposable[] = []; - - private linterManager: ILinterManager; - - private readonly appShell: IApplicationShell; - - private readonly documentManager: IDocumentManager; - - constructor(private serviceContainer: IServiceContainer) { - this.linterManager = this.serviceContainer.get(ILinterManager); - this.appShell = this.serviceContainer.get(IApplicationShell); - this.documentManager = this.serviceContainer.get(IDocumentManager); - - const commandManager = this.serviceContainer.get(ICommandManager); - commandManager.registerCommand(Commands.Set_Linter, this.setLinterAsync.bind(this)); - commandManager.registerCommand(Commands.Enable_Linter, this.enableLintingAsync.bind(this)); - commandManager.registerCommand(Commands.Run_Linter, this.runLinting.bind(this)); - } - - public dispose(): void { - this.disposables.forEach((disposable) => disposable.dispose()); - } - - public async setLinterAsync(): Promise { - const linters = this.linterManager.getAllLinterInfos(); - const suggestions = linters.map((x) => x.id).sort(); - const linterList = ['Disable Linting', ...suggestions]; - const activeLinters = await this.linterManager.getActiveLinters(this.settingsUri); - - let current: string; - switch (activeLinters.length) { - case 0: - current = 'none'; - break; - case 1: - current = activeLinters[0].id; - break; - default: - current = 'multiple selected'; - break; - } - - const quickPickOptions: QuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${current}`, - }; - - const selection = await this.appShell.showQuickPick(linterList, quickPickOptions); - if (selection !== undefined) { - if (selection === 'Disable Linting') { - await this.linterManager.enableLintingAsync(false); - sendTelemetryEvent(EventName.SELECT_LINTER, undefined, { enabled: false }); - } else { - const index = linters.findIndex((x) => x.id === selection); - if (activeLinters.length > 1) { - const response = await this.appShell.showWarningMessage( - l10n.t("Multiple linters are enabled in settings. Replace with '{0}'?", selection), - Common.bannerLabelYes, - Common.bannerLabelNo, - ); - if (response !== Common.bannerLabelYes) { - return; - } - } - await this.linterManager.setActiveLintersAsync([linters[index].product], this.settingsUri); - sendTelemetryEvent(EventName.SELECT_LINTER, undefined, { tool: selection as LinterId, enabled: true }); - } - } - } - - public async enableLintingAsync(): Promise { - const options = ['Enable', 'Disable']; - const current = (await this.linterManager.isLintingEnabled(this.settingsUri)) ? options[0] : options[1]; - - const quickPickOptions: QuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${current}`, - }; - - const selection = await this.appShell.showQuickPick(options, quickPickOptions); - - if (selection !== undefined) { - const enable: boolean = selection === options[0]; - await this.linterManager.enableLintingAsync(enable, this.settingsUri); - } - } - - public runLinting(): Promise { - const engine = this.serviceContainer.get(ILintingEngine); - return engine.lintOpenPythonFiles('manual'); - } - - private get settingsUri(): Uri | undefined { - return this.documentManager.activeTextEditor ? this.documentManager.activeTextEditor.document.uri : undefined; - } -} diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/info/envKind.ts b/extensions/positron-python/src/client/pythonEnvironments/base/info/envKind.ts index 8828003c5ce7..ff53a57d2f45 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/info/envKind.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/info/envKind.ts @@ -12,15 +12,15 @@ export function getKindDisplayName(kind: PythonEnvKind): string { for (const [candidate, value] of [ // Note that Unknown is excluded here. [PythonEnvKind.System, 'system'], - [PythonEnvKind.MicrosoftStore, 'microsoft store'], + [PythonEnvKind.MicrosoftStore, 'Microsoft Store'], [PythonEnvKind.Pyenv, 'pyenv'], - [PythonEnvKind.Poetry, 'poetry'], + [PythonEnvKind.Poetry, 'Poetry'], [PythonEnvKind.Custom, 'custom'], // For now we treat OtherGlobal like Unknown. [PythonEnvKind.Venv, 'venv'], [PythonEnvKind.VirtualEnv, 'virtualenv'], [PythonEnvKind.VirtualEnvWrapper, 'virtualenv'], - [PythonEnvKind.Pipenv, 'pipenv'], + [PythonEnvKind.Pipenv, 'Pipenv'], [PythonEnvKind.Conda, 'conda'], [PythonEnvKind.ActiveState, 'ActiveState'], // For now we treat OtherVirtual like Unknown. diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts b/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts index 2567168c6325..fb1a791d07ed 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts @@ -115,9 +115,9 @@ export class EnvsCollectionService extends PythonEnvsWatcher this.sendTelemetry(query, stopWatch)); } - return refreshPromise.then(() => this.sendTelemetry(query, stopWatch)); + return refreshPromise; } private startRefresh(query: PythonLocatorQuery | undefined): Promise { diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts index d86b2182d50c..71f3d69e9067 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { uniq } from 'lodash'; +import { toLower, uniq, uniqBy } from 'lodash'; import * as path from 'path'; import { chain, iterable } from '../../../../common/utils/async'; import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../../common/utils/platform'; @@ -39,10 +39,14 @@ async function getGlobalVirtualEnvDirs(): Promise { const homeDir = getUserHomeDir(); if (homeDir && (await pathExists(homeDir))) { - const subDirs = ['Envs', '.direnv', '.venvs', '.virtualenvs', path.join('.local', 'share', 'virtualenvs')]; - if (getOSType() !== OSType.Windows) { - subDirs.push('envs'); - } + const subDirs = [ + 'envs', + 'Envs', + '.direnv', + '.venvs', + '.virtualenvs', + path.join('.local', 'share', 'virtualenvs'), + ]; const filtered = await asyncFilter( subDirs.map((d) => path.join(homeDir, d)), pathExists, @@ -50,7 +54,7 @@ async function getGlobalVirtualEnvDirs(): Promise { filtered.forEach((d) => venvDirs.push(d)); } - return uniq(venvDirs); + return [OSType.Windows, OSType.OSX].includes(getOSType()) ? uniqBy(venvDirs, toLower) : uniq(venvDirs); } /** diff --git a/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts b/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts index 88178d02d58a..8f048ddd0676 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -23,6 +23,7 @@ import { traceError, traceVerbose } from '../../../logging'; import { OUTPUT_MARKER_SCRIPT } from '../../../common/process/internal/scripts'; import { splitLines } from '../../../common/stringUtils'; import { SpawnOptions } from '../../../common/process/types'; +import { sleep } from '../../../common/utils/async'; export const AnacondaCompanyName = 'Anaconda, Inc.'; export const CONDAPATH_SETTING_KEY = 'condaPath'; @@ -238,7 +239,7 @@ export function getCondaInterpreterPath(condaEnvironmentPath: string): string { // Minimum version number of conda required to be able to use 'conda run' with '--no-capture-output' flag. export const CONDA_RUN_VERSION = '4.9.0'; export const CONDA_ACTIVATION_TIMEOUT = 45000; -const CONDA_GENERAL_TIMEOUT = 50000; +const CONDA_GENERAL_TIMEOUT = 45000; /** Wraps the "conda" utility, and exposes its functionality. */ @@ -439,9 +440,19 @@ export class Conda { if (shellPath) { options.shell = shellPath; } - const result = await exec(command, ['info', '--json'], options); - traceVerbose(`${command} info --json: ${result.stdout}`); - return JSON.parse(result.stdout); + const resultPromise = exec(command, ['info', '--json'], options); + // It has been observed that specifying a timeout is still not reliable to terminate the Conda process, see #27915. + // Hence explicitly continue execution after timeout has been reached. + const success = await Promise.race([ + resultPromise.then(() => true), + sleep(CONDA_GENERAL_TIMEOUT + 3000).then(() => false), + ]); + if (success) { + const result = await resultPromise; + traceVerbose(`${command} info --json: ${result.stdout}`); + return JSON.parse(result.stdout); + } + throw new Error(`Launching '${command} info --json' timed out`); } /** diff --git a/extensions/positron-python/src/client/pythonEnvironments/common/posixUtils.ts b/extensions/positron-python/src/client/pythonEnvironments/common/posixUtils.ts index eb60fc029949..0e79ec9d590e 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/common/posixUtils.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/common/posixUtils.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import { uniq } from 'lodash'; import { getSearchPathEntries } from '../../common/utils/exec'; import { resolveSymbolicLink } from './externalDependencies'; -import { traceError, traceInfo, traceVerbose } from '../../logging'; +import { traceError, traceInfo, traceVerbose, traceWarn } from '../../logging'; /** * Determine if the given filename looks like the simplest Python executable. @@ -117,7 +117,10 @@ function pickShortestPath(pythonPaths: string[]) { export async function getPythonBinFromPosixPaths(searchDirs: string[]): Promise { const binToLinkMap = new Map(); for (const searchDir of searchDirs) { - const paths = await findPythonBinariesInDir(searchDir); + const paths = await findPythonBinariesInDir(searchDir).catch((ex) => { + traceWarn('Looking for python binaries within', searchDir, 'failed with', ex); + return []; + }); for (const filepath of paths) { // Ensure that we have a collection of unique global binaries by diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/common/commonUtils.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/common/commonUtils.ts index b4d4a37eae9b..16d8015e3f26 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/common/commonUtils.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/common/commonUtils.ts @@ -32,3 +32,11 @@ export function getVenvExecutable(workspaceFolder: WorkspaceFolder): string { } return path.join(getVenvPath(workspaceFolder), 'bin', 'python'); } + +export function getPrefixCondaEnvPath(workspaceFolder: WorkspaceFolder): string { + return path.join(workspaceFolder.uri.fsPath, '.conda'); +} + +export async function hasPrefixCondaEnv(workspaceFolder: WorkspaceFolder): Promise { + return fs.pathExists(getPrefixCondaEnvPath(workspaceFolder)); +} diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts new file mode 100644 index 000000000000..0c1c2b38eab2 --- /dev/null +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as fsapi from 'fs-extra'; +import { ConfigurationTarget, Uri, WorkspaceFolder } from 'vscode'; +import { getPipRequirementsFiles } from '../provider/venvUtils'; +import { getExtension } from '../../../common/vscodeApis/extensionsApi'; +import { PVSC_EXTENSION_ID } from '../../../common/constants'; +import { PythonExtension } from '../../../api/types'; +import { traceVerbose } from '../../../logging'; +import { getConfiguration } from '../../../common/vscodeApis/workspaceApis'; +import { getWorkspaceStateValue, updateWorkspaceStateValue } from '../../../common/persistentState'; + +export const CREATE_ENV_TRIGGER_SETTING_PART = 'createEnvironment.trigger'; +export const CREATE_ENV_TRIGGER_SETTING = `python.${CREATE_ENV_TRIGGER_SETTING_PART}`; + +export async function fileContainsInlineDependencies(_uri: Uri): Promise { + // This is a placeholder for the real implementation of inline dependencies support + // For now we don't detect anything. Once PEP-722/PEP-723 are accepted we can implement + // this properly. + return false; +} + +export async function hasRequirementFiles(workspace: WorkspaceFolder): Promise { + const files = await getPipRequirementsFiles(workspace); + const found = (files?.length ?? 0) > 0; + if (found) { + traceVerbose(`Found requirement files: ${workspace.uri.fsPath}`); + } + return found; +} + +export async function hasKnownFiles(workspace: WorkspaceFolder): Promise { + const filePaths: string[] = [ + 'poetry.lock', + 'conda.yaml', + 'environment.yaml', + 'conda.yml', + 'environment.yml', + 'Pipfile', + 'Pipfile.lock', + ].map((fileName) => path.join(workspace.uri.fsPath, fileName)); + const result = await Promise.all(filePaths.map((f) => fsapi.pathExists(f))); + const found = result.some((r) => r); + if (found) { + traceVerbose(`Found known files: ${workspace.uri.fsPath}`); + } + return found; +} + +export async function isGlobalPythonSelected(workspace: WorkspaceFolder): Promise { + const extension = getExtension(PVSC_EXTENSION_ID); + if (!extension) { + return false; + } + const extensionApi: PythonExtension = extension.exports as PythonExtension; + const interpreter = extensionApi.environments.getActiveEnvironmentPath(workspace.uri); + const details = await extensionApi.environments.resolveEnvironment(interpreter); + const isGlobal = details?.environment === undefined; + if (isGlobal) { + traceVerbose(`Selected python for [${workspace.uri.fsPath}] is [global] type: ${interpreter.path}`); + } + return isGlobal; +} + +/** + * Checks the setting `python.createEnvironment.trigger` to see if we should perform the checks + * to prompt to create an environment. + * @export + * @returns : True if we should prompt to create an environment. + */ +export function shouldPromptToCreateEnv(): boolean { + const config = getConfiguration('python'); + if (config) { + const value = config.get(CREATE_ENV_TRIGGER_SETTING_PART, 'off'); + return value !== 'off'; + } + + return getWorkspaceStateValue(CREATE_ENV_TRIGGER_SETTING, 'off') !== 'off'; +} + +/** + * Sets `python.createEnvironment.trigger` to 'off' in the user settings. + */ +export function disableCreateEnvironmentTrigger(): void { + const config = getConfiguration('python'); + if (config) { + config.update('createEnvironment.trigger', 'off', ConfigurationTarget.Global); + } +} + +/** + * Sets trigger to 'off' in workspace persistent state. This disables trigger check + * for the current workspace only. In multi root case, it is disabled for all folders + * in the multi root workspace. + */ +export async function disableWorkspaceCreateEnvironmentTrigger(): Promise { + await updateWorkspaceStateValue(CREATE_ENV_TRIGGER_SETTING, 'off'); +} + +let _alreadyCreateEnvCriteriaCheck = false; +/** + * Run-once wrapper function for the workspace check to prompt to create an environment. + * @returns : True if we should prompt to c environment. + */ +export function isCreateEnvWorkspaceCheckNotRun(): boolean { + if (_alreadyCreateEnvCriteriaCheck) { + return false; + } + _alreadyCreateEnvCriteriaCheck = true; + return true; +} diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts new file mode 100644 index 000000000000..1737d351ca7b --- /dev/null +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable, Uri, WorkspaceFolder } from 'vscode'; +import { + fileContainsInlineDependencies, + hasKnownFiles, + hasRequirementFiles, + isGlobalPythonSelected, + shouldPromptToCreateEnv, + isCreateEnvWorkspaceCheckNotRun, + disableCreateEnvironmentTrigger, + disableWorkspaceCreateEnvironmentTrigger, +} from './common/createEnvTriggerUtils'; +import { getWorkspaceFolder } from '../../common/vscodeApis/workspaceApis'; +import { traceError, traceInfo, traceVerbose } from '../../logging'; +import { hasPrefixCondaEnv, hasVenv } from './common/commonUtils'; +import { showInformationMessage } from '../../common/vscodeApis/windowApis'; +import { CreateEnv } from '../../common/utils/localize'; +import { executeCommand, registerCommand } from '../../common/vscodeApis/commandApis'; +import { Commands } from '../../common/constants'; +import { Resource } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; + +export enum CreateEnvironmentCheckKind { + /** + * Checks if environment creation is needed based on file location and content. + */ + File = 'file', + + /** + * Checks if environment creation is needed based on workspace contents. + */ + Workspace = 'workspace', +} + +export interface CreateEnvironmentTriggerOptions { + force?: boolean; +} + +async function createEnvironmentCheckForWorkspace(uri: Uri): Promise { + const workspace = getWorkspaceFolder(uri); + if (!workspace) { + traceInfo(`CreateEnv Trigger - Workspace not found for ${uri.fsPath}`); + return; + } + + const missingRequirements = async (workspaceFolder: WorkspaceFolder) => + !(await hasRequirementFiles(workspaceFolder)); + + const isNonGlobalPythonSelected = async (workspaceFolder: WorkspaceFolder) => + !(await isGlobalPythonSelected(workspaceFolder)); + + // Skip showing the Create Environment prompt if one of the following is True: + // 1. The workspace already has a ".venv" or ".conda" env + // 2. The workspace does NOT have "requirements.txt" or "requirements/*.txt" files + // 3. The workspace has known files for other environment types like environment.yml, conda.yml, poetry.lock, etc. + // 4. The selected python is NOT classified as a global python interpreter + const skipPrompt: boolean = ( + await Promise.all([ + hasVenv(workspace), + hasPrefixCondaEnv(workspace), + missingRequirements(workspace), + hasKnownFiles(workspace), + isNonGlobalPythonSelected(workspace), + ]) + ).some((r) => r); + + if (skipPrompt) { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'criteria-not-met' }); + traceInfo(`CreateEnv Trigger - Skipping for ${uri.fsPath}`); + return; + } + + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'criteria-met' }); + const selection = await showInformationMessage( + CreateEnv.Trigger.workspaceTriggerMessage, + CreateEnv.Trigger.createEnvironment, + CreateEnv.Trigger.disableCheckWorkspace, + CreateEnv.Trigger.disableCheck, + ); + + if (selection === CreateEnv.Trigger.createEnvironment) { + try { + await executeCommand(Commands.Create_Environment); + } catch (error) { + traceError('CreateEnv Trigger - Error while creating environment: ', error); + } + } else if (selection === CreateEnv.Trigger.disableCheck) { + disableCreateEnvironmentTrigger(); + } else if (selection === CreateEnv.Trigger.disableCheckWorkspace) { + disableWorkspaceCreateEnvironmentTrigger(); + } +} + +function runOnceWorkspaceCheck(uri: Uri, options: CreateEnvironmentTriggerOptions = {}): Promise { + if (isCreateEnvWorkspaceCheckNotRun() || options?.force) { + return createEnvironmentCheckForWorkspace(uri); + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'already-ran' }); + traceVerbose('CreateEnv Trigger - skipping this because it was already run'); + return Promise.resolve(); +} + +async function createEnvironmentCheckForFile(uri: Uri, options?: CreateEnvironmentTriggerOptions): Promise { + if (await fileContainsInlineDependencies(uri)) { + // TODO: Handle create environment for each file here. + // pending acceptance of PEP-722/PEP-723 + + // For now we do the same thing as for workspace. + await runOnceWorkspaceCheck(uri, options); + } + + // If the file does not have any inline dependencies, then we do the same thing + // as for workspace. + await runOnceWorkspaceCheck(uri, options); +} + +export async function triggerCreateEnvironmentCheck( + kind: CreateEnvironmentCheckKind, + uri: Resource, + options?: CreateEnvironmentTriggerOptions, +): Promise { + if (!uri) { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'no-uri' }); + traceVerbose('CreateEnv Trigger - Skipping No URI provided'); + return; + } + + if (shouldPromptToCreateEnv()) { + if (kind === CreateEnvironmentCheckKind.File) { + await createEnvironmentCheckForFile(uri, options); + } else { + await runOnceWorkspaceCheck(uri, options); + } + } else { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'turned-off' }); + traceVerbose('CreateEnv Trigger - turned off in settings'); + } +} + +export function triggerCreateEnvironmentCheckNonBlocking( + kind: CreateEnvironmentCheckKind, + uri: Resource, + options?: CreateEnvironmentTriggerOptions, +): void { + // The Event loop for Node.js runs functions with setTimeout() with lower priority than setImmediate. + // This is done to intentionally avoid blocking anything that the user wants to do. + setTimeout(() => triggerCreateEnvironmentCheck(kind, uri, options).ignoreErrors(), 0); +} + +export function registerCreateEnvironmentTriggers(disposables: Disposable[]): void { + disposables.push( + registerCommand(Commands.Create_Environment_Check, (file: Resource) => { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'as-command' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file, { force: true }); + }), + ); +} diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index 7ca44c1b7eff..9dff50c5586d 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -9,11 +9,18 @@ import { CreateEnvironmentProgress } from '../types'; import { pickWorkspaceFolder } from '../common/workspaceSelection'; import { execObservable } from '../../../common/process/rawProcessApis'; import { createDeferred } from '../../../common/utils/async'; -import { getEnvironmentVariable, getOSType, OSType } from '../../../common/utils/platform'; +import { getOSType, OSType } from '../../../common/utils/platform'; import { createCondaScript } from '../../../common/process/internal/scripts'; import { Common, CreateEnv } from '../../../common/utils/localize'; -import { getCondaBaseEnv, pickPythonVersion } from './condaUtils'; -import { showErrorMessageWithLogs } from '../common/commonUtils'; +import { + ExistingCondaAction, + deleteEnvironment, + getCondaBaseEnv, + getPathEnvVariableForConda, + pickExistingCondaAction, + pickPythonVersion, +} from './condaUtils'; +import { getPrefixCondaEnvPath, showErrorMessageWithLogs } from '../common/commonUtils'; import { MultiStepAction, MultiStepNode, withProgress } from '../../../common/vscodeApis/windowApis'; import { EventName } from '../../../telemetry/constants'; import { sendTelemetryEvent } from '../../../telemetry'; @@ -83,22 +90,7 @@ async function createCondaEnv( }); const deferred = createDeferred(); - let pathEnv = getEnvironmentVariable('PATH') || getEnvironmentVariable('Path') || ''; - if (getOSType() === OSType.Windows) { - // On windows `conda.bat` is used, which adds the following bin directories to PATH - // then launches `conda.exe` which is a stub to `python.exe -m conda`. Here, we are - // instead using the `python.exe` that ships with conda to run a python script that - // handles conda env creation and package installation. - // See conda issue: https://github.com/conda/conda/issues/11399 - const root = path.dirname(command); - const libPath1 = path.join(root, 'Library', 'bin'); - const libPath2 = path.join(root, 'Library', 'mingw-w64', 'bin'); - const libPath3 = path.join(root, 'Library', 'usr', 'bin'); - const libPath4 = path.join(root, 'bin'); - const libPath5 = path.join(root, 'Scripts'); - const libPath = [libPath1, libPath2, libPath3, libPath4, libPath5].join(path.delimiter); - pathEnv = `${libPath}${path.delimiter}${pathEnv}`; - } + const pathEnv = getPathEnvVariableForConda(command); traceLog('Running Conda Env creation script: ', [command, ...args]); const { proc, out, dispose } = execObservable(command, args, { mergeStdOutErr: true, @@ -182,35 +174,93 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise { + if (workspace && context === MultiStepAction.Continue) { + try { + existingCondaAction = await pickExistingCondaAction(workspace); + return MultiStepAction.Continue; + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + } else if (context === MultiStepAction.Back) { + return MultiStepAction.Back; + } + return MultiStepAction.Continue; + }, + undefined, + ); + workspaceStep.next = existingEnvStep; + let version: string | undefined; const versionStep = new MultiStepNode( workspaceStep, - async () => { - try { - version = await pickPythonVersion(); - } catch (ex) { - if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { - return ex; + async (context) => { + if ( + existingCondaAction === ExistingCondaAction.Recreate || + existingCondaAction === ExistingCondaAction.Create + ) { + try { + version = await pickPythonVersion(); + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + if (version === undefined) { + traceError('Python version was not selected for creating conda environment.'); + return MultiStepAction.Cancel; + } + traceInfo(`Selected Python version ${version} for creating conda environment.`); + } else if (existingCondaAction === ExistingCondaAction.UseExisting) { + if (context === MultiStepAction.Back) { + return MultiStepAction.Back; } - throw ex; } - if (version === undefined) { - traceError('Python version was not selected for creating conda environment.'); - return MultiStepAction.Cancel; - } - traceInfo(`Selected Python version ${version} for creating conda environment.`); return MultiStepAction.Continue; }, undefined, ); - workspaceStep.next = versionStep; + existingEnvStep.next = versionStep; const action = await MultiStepNode.run(workspaceStep); if (action === MultiStepAction.Back || action === MultiStepAction.Cancel) { throw action; } + if (workspace) { + if (existingCondaAction === ExistingCondaAction.Recreate) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'conda', + status: 'triggered', + }); + if (await deleteEnvironment(workspace, getExecutableCommand(conda))) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'conda', + status: 'deleted', + }); + } else { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'conda', + status: 'failed', + }); + throw MultiStepAction.Cancel; + } + } else if (existingCondaAction === ExistingCondaAction.UseExisting) { + sendTelemetryEvent(EventName.ENVIRONMENT_REUSE, undefined, { + environmentType: 'conda', + }); + return { path: getPrefixCondaEnvPath(workspace), workspaceFolder: workspace }; + } + } + return withProgress( { location: ProgressLocation.Notification, diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaDeleteUtils.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaDeleteUtils.ts new file mode 100644 index 000000000000..e4f4784f15c8 --- /dev/null +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaDeleteUtils.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { WorkspaceFolder } from 'vscode'; +import { plainExec } from '../../../common/process/rawProcessApis'; +import { CreateEnv } from '../../../common/utils/localize'; +import { traceError, traceInfo } from '../../../logging'; +import { getPrefixCondaEnvPath, hasPrefixCondaEnv, showErrorMessageWithLogs } from '../common/commonUtils'; + +export async function deleteCondaEnvironment( + workspace: WorkspaceFolder, + interpreter: string, + pathEnvVar: string, +): Promise { + const condaEnvPath = getPrefixCondaEnvPath(workspace); + const command = interpreter; + const args = ['-m', 'conda', 'env', 'remove', '--prefix', condaEnvPath, '--yes']; + try { + traceInfo(`Deleting conda environment: ${condaEnvPath}`); + traceInfo(`Running command: ${command} ${args.join(' ')}`); + const result = await plainExec(command, args, { mergeStdOutErr: true }, { ...process.env, PATH: pathEnvVar }); + traceInfo(result.stdout); + if (await hasPrefixCondaEnv(workspace)) { + // If conda cannot delete files it will name the files as .conda_trash. + // These need to be deleted manually. + traceError(`Conda environment ${condaEnvPath} could not be deleted.`); + traceError(`Please delete the environment manually: ${condaEnvPath}`); + showErrorMessageWithLogs(CreateEnv.Conda.errorDeletingEnvironment); + return false; + } + } catch (err) { + showErrorMessageWithLogs(CreateEnv.Conda.errorDeletingEnvironment); + traceError(`Deleting conda environment ${condaEnvPath} Failed with error: `, err); + return false; + } + return true; +} diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaUtils.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaUtils.ts index e00a1c8dca09..51c55e414245 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaUtils.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -1,14 +1,22 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, QuickPickItem, Uri } from 'vscode'; -import { Common } from '../../../browser/localize'; -import { Octicons } from '../../../common/constants'; -import { CreateEnv } from '../../../common/utils/localize'; +import * as path from 'path'; +import { CancellationToken, ProgressLocation, QuickPickItem, Uri, WorkspaceFolder } from 'vscode'; +import { Commands, Octicons } from '../../../common/constants'; +import { Common, CreateEnv } from '../../../common/utils/localize'; import { executeCommand } from '../../../common/vscodeApis/commandApis'; -import { showErrorMessage, showQuickPickWithBack } from '../../../common/vscodeApis/windowApis'; +import { + MultiStepAction, + showErrorMessage, + showQuickPickWithBack, + withProgress, +} from '../../../common/vscodeApis/windowApis'; import { traceLog } from '../../../logging'; import { Conda } from '../../common/environmentManagers/conda'; +import { getPrefixCondaEnvPath, hasPrefixCondaEnv } from '../common/commonUtils'; +import { OSType, getEnvironmentVariable, getOSType } from '../../../common/utils/platform'; +import { deleteCondaEnvironment } from './condaDeleteUtils'; const RECOMMENDED_CONDA_PYTHON = '3.10'; @@ -39,7 +47,7 @@ export async function getCondaBaseEnv(): Promise { } export async function pickPythonVersion(token?: CancellationToken): Promise { - const items: QuickPickItem[] = ['3.10', '3.11', '3.9', '3.8', '3.7'].map((v) => ({ + const items: QuickPickItem[] = ['3.10', '3.11', '3.9', '3.8'].map((v) => ({ label: v === RECOMMENDED_CONDA_PYTHON ? `${Octicons.Star} Python` : 'Python', description: v, })); @@ -59,3 +67,78 @@ export async function pickPythonVersion(token?: CancellationToken): Promise { + const condaEnvPath = getPrefixCondaEnvPath(workspaceFolder); + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.Conda.deletingEnvironmentProgress} ([${Common.showLogs}](command:${Commands.ViewOutput})): ${condaEnvPath}`, + cancellable: false, + }, + async () => deleteCondaEnvironment(workspaceFolder, interpreter, getPathEnvVariableForConda(interpreter)), + ); +} + +export enum ExistingCondaAction { + Recreate, + UseExisting, + Create, +} + +export async function pickExistingCondaAction( + workspaceFolder: WorkspaceFolder | undefined, +): Promise { + if (workspaceFolder) { + if (await hasPrefixCondaEnv(workspaceFolder)) { + const items: QuickPickItem[] = [ + { label: CreateEnv.Conda.recreate, description: CreateEnv.Conda.recreateDescription }, + { + label: CreateEnv.Conda.useExisting, + description: CreateEnv.Conda.useExistingDescription, + }, + ]; + + const selection = (await showQuickPickWithBack( + items, + { + placeHolder: CreateEnv.Conda.existingCondaQuickPickPlaceholder, + ignoreFocusOut: true, + }, + undefined, + )) as QuickPickItem | undefined; + + if (selection?.label === CreateEnv.Conda.recreate) { + return ExistingCondaAction.Recreate; + } + + if (selection?.label === CreateEnv.Conda.useExisting) { + return ExistingCondaAction.UseExisting; + } + } else { + return ExistingCondaAction.Create; + } + } + + throw MultiStepAction.Cancel; +} diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index edbdcd7d84a6..61850e404c3d 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -32,8 +32,14 @@ import { CreateEnvironmentResult, } from '../proposed.createEnvApis'; -function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgnore?: boolean): string[] { +interface IVenvCommandArgs { + argv: string[]; + stdin: string | undefined; +} + +function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgnore?: boolean): IVenvCommandArgs { const command: string[] = [createVenvScript()]; + let stdin: string | undefined; if (addGitIgnore) { command.push('--git-ignore'); @@ -52,14 +58,21 @@ function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgn }); const requirements = installInfo.filter((i) => i.installType === 'requirements').map((i) => i.installItem); - requirements.forEach((r) => { - if (r) { - command.push('--requirements', r); - } - }); + + if (requirements.length < 10) { + requirements.forEach((r) => { + if (r) { + command.push('--requirements', r); + } + }); + } else { + command.push('--stdin'); + // Too many requirements can cause the command line to be too long error. + stdin = JSON.stringify({ requirements }); + } } - return command; + return { argv: command, stdin }; } function getVenvFromOutput(output: string): string | undefined { @@ -81,7 +94,7 @@ function getVenvFromOutput(output: string): string | undefined { async function createVenv( workspace: WorkspaceFolder, command: string, - args: string[], + args: IVenvCommandArgs, progress: CreateEnvironmentProgress, token?: CancellationToken, ): Promise { @@ -94,11 +107,15 @@ async function createVenv( }); const deferred = createDeferred(); - traceLog('Running Env creation script: ', [command, ...args]); - const { proc, out, dispose } = execObservable(command, args, { + traceLog('Running Env creation script: ', [command, ...args.argv]); + if (args.stdin) { + traceLog('Requirements passed in via stdin: ', args.stdin); + } + const { proc, out, dispose } = execObservable(command, args.argv, { mergeStdOutErr: true, token, cwd: workspace.uri.fsPath, + stdinStr: args.stdin, }); const progressAndTelemetry = new VenvProgressAndTelemetry(progress); diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvUtils.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvUtils.ts index d7a0be170f99..cc506a11a88f 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvUtils.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvUtils.ts @@ -5,12 +5,22 @@ import * as tomljs from '@iarna/toml'; import * as fs from 'fs-extra'; import { flatten, isArray } from 'lodash'; import * as path from 'path'; -import { CancellationToken, ProgressLocation, QuickPickItem, RelativePattern, WorkspaceFolder } from 'vscode'; +import { + CancellationToken, + ProgressLocation, + QuickPickItem, + QuickPickItemButtonEvent, + RelativePattern, + ThemeIcon, + Uri, + WorkspaceFolder, +} from 'vscode'; import { Common, CreateEnv } from '../../../common/utils/localize'; import { MultiStepAction, MultiStepNode, showQuickPickWithBack, + showTextDocument, withProgress, } from '../../../common/vscodeApis/windowApis'; import { findFiles } from '../../../common/vscodeApis/workspaceApis'; @@ -20,8 +30,12 @@ import { isWindows } from '../../../common/platform/platformService'; import { getVenvPath, hasVenv } from '../common/commonUtils'; import { deleteEnvironmentNonWindows, deleteEnvironmentWindows } from './venvDeleteUtils'; +export const OPEN_REQUIREMENTS_BUTTON = { + iconPath: new ThemeIcon('go-to-file'), + tooltip: CreateEnv.Venv.openRequirementsFile, +}; const exclude = '**/{.venv*,.git,.nox,.tox,.conda,site-packages,__pypackages__}/**'; -async function getPipRequirementsFiles( +export async function getPipRequirementsFiles( workspaceFolder: WorkspaceFolder, token?: CancellationToken, ): Promise { @@ -78,8 +92,13 @@ async function pickTomlExtras(extras: string[], token?: CancellationToken): Prom return undefined; } -async function pickRequirementsFiles(files: string[], token?: CancellationToken): Promise { +async function pickRequirementsFiles( + files: string[], + root: string, + token?: CancellationToken, +): Promise { const items: QuickPickItem[] = files + .map((p) => path.relative(root, p)) .sort((a, b) => { const al: number = a.split(/[\\\/]/).length; const bl: number = b.split(/[\\\/]/).length; @@ -91,7 +110,10 @@ async function pickRequirementsFiles(files: string[], token?: CancellationToken) } return al - bl; }) - .map((e) => ({ label: e })); + .map((e) => ({ + label: e, + buttons: [OPEN_REQUIREMENTS_BUTTON], + })); const selection = await showQuickPickWithBack( items, @@ -101,6 +123,11 @@ async function pickRequirementsFiles(files: string[], token?: CancellationToken) canPickMany: true, }, token, + async (e: QuickPickItemButtonEvent) => { + if (e.item.label) { + await showTextDocument(Uri.file(path.join(root, e.item.label))); + } + }, ); if (selection && isArray(selection)) { @@ -195,14 +222,11 @@ export async function pickPackagesToInstall( tomlStep, async (context?: MultiStepAction) => { traceVerbose('Looking for pip requirements.'); - const requirementFiles = (await getPipRequirementsFiles(workspaceFolder, token))?.map((p) => - path.relative(workspaceFolder.uri.fsPath, p), - ); - + const requirementFiles = await getPipRequirementsFiles(workspaceFolder, token); if (requirementFiles && requirementFiles.length > 0) { traceVerbose('Found pip requirements.'); try { - const result = await pickRequirementsFiles(requirementFiles, token); + const result = await pickRequirementsFiles(requirementFiles, workspaceFolder.uri.fsPath, token); const installList = result?.map((p) => path.join(workspaceFolder.uri.fsPath, p)); if (installList) { installList.forEach((i) => { @@ -268,11 +292,14 @@ export async function pickExistingVenvAction( if (workspaceFolder) { if (await hasVenv(workspaceFolder)) { const items: QuickPickItem[] = [ - { label: CreateEnv.Venv.recreate, description: CreateEnv.Venv.recreateDescription }, { label: CreateEnv.Venv.useExisting, description: CreateEnv.Venv.useExistingDescription, }, + { + label: CreateEnv.Venv.recreate, + description: CreateEnv.Venv.recreateDescription, + }, ]; const selection = (await showQuickPickWithBack( diff --git a/extensions/positron-python/src/client/pythonEnvironments/info/index.ts b/extensions/positron-python/src/client/pythonEnvironments/info/index.ts index ee2ff9d7cc22..17e8958f6310 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/info/index.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/info/index.ts @@ -98,7 +98,7 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType): string return 'conda'; } case EnvironmentType.Pipenv: { - return 'pipenv'; + return 'Pipenv'; } case EnvironmentType.Pyenv: { return 'pyenv'; @@ -110,16 +110,16 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType): string return 'virtualenv'; } case EnvironmentType.MicrosoftStore: { - return 'microsoft store'; + return 'Microsoft Store'; } case EnvironmentType.Poetry: { - return 'poetry'; + return 'Poetry'; } case EnvironmentType.VirtualEnvWrapper: { return 'virtualenvwrapper'; } case EnvironmentType.ActiveState: { - return 'activestate'; + return 'ActiveState'; } default: { return ''; diff --git a/extensions/positron-python/src/client/telemetry/constants.ts b/extensions/positron-python/src/client/telemetry/constants.ts index a729b3d491e8..c680b91094cb 100644 --- a/extensions/positron-python/src/client/telemetry/constants.ts +++ b/extensions/positron-python/src/client/telemetry/constants.ts @@ -115,6 +115,9 @@ export enum EventName { ENVIRONMENT_DELETE = 'ENVIRONMENT.DELETE', ENVIRONMENT_REUSE = 'ENVIRONMENT.REUSE', + ENVIRONMENT_CHECK_TRIGGER = 'ENVIRONMENT.CHECK.TRIGGER', + ENVIRONMENT_CHECK_RESULT = 'ENVIRONMENT.CHECK.RESULT', + TOOLS_EXTENSIONS_ALREADY_INSTALLED = 'TOOLS_EXTENSIONS.ALREADY_INSTALLED', TOOLS_EXTENSIONS_PROMPT_SHOWN = 'TOOLS_EXTENSIONS.PROMPT_SHOWN', TOOLS_EXTENSIONS_INSTALL_SELECTED = 'TOOLS_EXTENSIONS.INSTALL_SELECTED', diff --git a/extensions/positron-python/src/client/telemetry/importTracker.ts b/extensions/positron-python/src/client/telemetry/importTracker.ts index 06991a815140..e00714d24cbb 100644 --- a/extensions/positron-python/src/client/telemetry/importTracker.ts +++ b/extensions/positron-python/src/client/telemetry/importTracker.ts @@ -49,7 +49,9 @@ const testExecution = isTestExecution(); export class ImportTracker implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; - private pendingChecks = new Map(); + // --- Start Positron --- + private pendingChecks = new Map(); + // --- End Positron --- private static sentMatches: Set = new Set(); diff --git a/extensions/positron-python/src/client/telemetry/index.ts b/extensions/positron-python/src/client/telemetry/index.ts index f4947cd73f05..600f9a2d48ff 100644 --- a/extensions/positron-python/src/client/telemetry/index.ts +++ b/extensions/positron-python/src/client/telemetry/index.ts @@ -2144,6 +2144,34 @@ export interface IEventNamePropertyMapping { [EventName.ENVIRONMENT_REUSE]: { environmentType: 'venv' | 'conda'; }; + /** + * Telemetry event sent when a check for environment creation conditions is triggered. + */ + /* __GDPR__ + "environemt.check.trigger" : { + "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_CHECK_TRIGGER]: { + trigger: + | 'run-in-terminal' + | 'debug-in-terminal' + | 'run-selection' + | 'on-workspace-load' + | 'as-command' + | 'debug'; + }; + /** + * Telemetry event sent when a check for environment creation condition is computed. + */ + /* __GDPR__ + "environemt.check.result" : { + "result" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_CHECK_RESULT]: { + result: 'criteria-met' | 'criteria-not-met' | 'already-ran' | 'turned-off' | 'no-uri'; + }; /** * Telemetry event sent when a linter or formatter extension is already installed. */ diff --git a/extensions/positron-python/src/client/terminals/codeExecution/codeExecutionManager.ts b/extensions/positron-python/src/client/terminals/codeExecution/codeExecutionManager.ts index 05265a0e918f..37ddea169891 100644 --- a/extensions/positron-python/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/extensions/positron-python/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -22,6 +22,10 @@ import { traceError } from '../../logging'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } from '../../terminals/types'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../pythonEnvironments/creation/createEnvironmentTrigger'; @injectable() export class CodeExecutionManager implements ICodeExecutionManager { @@ -52,6 +56,10 @@ export class CodeExecutionManager implements ICodeExecutionManager { .then(noop, noop); return; } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { + trigger: 'run-in-terminal', + }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; await this.executeFileInTerminal(file, trigger, { newTerminalPerFile: cmd === Commands.Exec_In_Separate_Terminal, @@ -124,6 +132,8 @@ export class CodeExecutionManager implements ICodeExecutionManager { this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); return; } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'run-selection' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); await this.executeSelectionInTerminal().then(() => { if (this.shouldTerminalFocusOnStart(file)) this.commandManager.executeCommand('workbench.action.terminal.focus'); @@ -140,6 +150,8 @@ export class CodeExecutionManager implements ICodeExecutionManager { this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); return; } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'run-selection' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); await this.executeSelectionInDjangoShell().then(() => { if (this.shouldTerminalFocusOnStart(file)) this.commandManager.executeCommand('workbench.action.terminal.focus'); diff --git a/extensions/positron-python/src/client/testing/testController/common/resultResolver.ts b/extensions/positron-python/src/client/testing/testController/common/resultResolver.ts index 6e875473c836..79cee6452a8c 100644 --- a/extensions/positron-python/src/client/testing/testController/common/resultResolver.ts +++ b/extensions/positron-python/src/client/testing/testController/common/resultResolver.ts @@ -3,7 +3,7 @@ import { CancellationToken, TestController, TestItem, Uri, TestMessage, Location, TestRun } from 'vscode'; import * as util from 'util'; -import { DiscoveredTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; +import { DiscoveredTestPayload, EOTTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; import { TestProvider } from '../../types'; import { traceError, traceLog } from '../../../logging'; import { Testing } from '../../../common/utils/localize'; @@ -11,7 +11,8 @@ import { clearAllChildren, createErrorTestItem, getTestCaseNodes } from './testI import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { splitLines } from '../../../common/stringUtils'; -import { buildErrorNodeOptions, fixLogLines, populateTestTree } from './utils'; +import { buildErrorNodeOptions, fixLogLines, populateTestTree, splitTestNameWithRegex } from './utils'; +import { Deferred } from '../../../common/utils/async'; export class PythonResultResolver implements ITestResultResolver { testController: TestController; @@ -35,16 +36,30 @@ export class PythonResultResolver implements ITestResultResolver { this.vsIdToRunId = new Map(); } - public resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise { - const workspacePath = this.workspaceUri.fsPath; - traceLog('Using result resolver for discovery'); - - const rawTestData = payload; - if (!rawTestData) { + public resolveDiscovery( + payload: DiscoveredTestPayload | EOTTestPayload, + deferredTillEOT: Deferred, + token?: CancellationToken, + ): Promise { + if (!payload) { // No test data is available return Promise.resolve(); } + if ('eot' in payload) { + // the payload is an EOT payload, so resolve the deferred promise. + traceLog('ResultResolver EOT received for discovery.'); + const eotPayload = payload as EOTTestPayload; + if (eotPayload.eot === true) { + deferredTillEOT.resolve(); + return Promise.resolve(); + } + } + return this._resolveDiscovery(payload as DiscoveredTestPayload, token); + } + public _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise { + const workspacePath = this.workspaceUri.fsPath; + const rawTestData = payload as DiscoveredTestPayload; // Check if there were any errors in the discovery process. if (rawTestData.status === 'error') { const testingErrorConst = @@ -77,7 +92,12 @@ export class PythonResultResolver implements ITestResultResolver { populateTestTree(this.testController, rawTestData.tests, undefined, this, token); } else { // Delete everything from the test controller. + const errorNode = this.testController.items.get(`DiscoveryError:${workspacePath}`); this.testController.items.replace([]); + // Add back the error node if it exists. + if (errorNode !== undefined) { + this.testController.items.add(errorNode); + } } sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { @@ -87,8 +107,25 @@ export class PythonResultResolver implements ITestResultResolver { return Promise.resolve(); } - public resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise { - const rawTestExecData = payload; + public resolveExecution( + payload: ExecutionTestPayload | EOTTestPayload, + runInstance: TestRun, + deferredTillEOT: Deferred, + ): Promise { + if (payload !== undefined && 'eot' in payload) { + // the payload is an EOT payload, so resolve the deferred promise. + traceLog('ResultResolver EOT received for execution.'); + const eotPayload = payload as EOTTestPayload; + if (eotPayload.eot === true) { + deferredTillEOT.resolve(); + return Promise.resolve(); + } + } + return this._resolveExecution(payload as ExecutionTestPayload, runInstance); + } + + public _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise { + const rawTestExecData = payload as ExecutionTestPayload; if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { // Map which holds the subtest information for each test item. @@ -179,9 +216,8 @@ export class PythonResultResolver implements ITestResultResolver { }); } } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-failure') { - // split on " " since the subtest ID has the parent test ID in the first part of the ID. - const parentTestCaseId = keyTemp.split(' ')[0]; - const subtestId = keyTemp.split(' ')[1]; + // split on [] or () based on how the subtest is setup. + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); const data = rawTestExecData.result[keyTemp]; // find the subtest's parent test item @@ -190,7 +226,10 @@ export class PythonResultResolver implements ITestResultResolver { if (subtestStats) { subtestStats.failed += 1; } else { - this.subTestStats.set(parentTestCaseId, { failed: 1, passed: 0 }); + this.subTestStats.set(parentTestCaseId, { + failed: 1, + passed: 0, + }); runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); @@ -216,9 +255,8 @@ export class PythonResultResolver implements ITestResultResolver { throw new Error('Parent test item not found'); } } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { - // split on " " since the subtest ID has the parent test ID in the first part of the ID. - const parentTestCaseId = keyTemp.split(' ')[0]; - const subtestId = keyTemp.split(' ')[1]; + // split on [] or () based on how the subtest is setup. + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); // find the subtest's parent test item diff --git a/extensions/positron-python/src/client/testing/testController/common/server.ts b/extensions/positron-python/src/client/testing/testController/common/server.ts index 8797a861fb4a..46217eab0459 100644 --- a/extensions/positron-python/src/client/testing/testController/common/server.ts +++ b/extensions/positron-python/src/client/testing/testController/common/server.ts @@ -5,17 +5,23 @@ import * as net from 'net'; import * as crypto from 'crypto'; import { Disposable, Event, EventEmitter, TestRun } from 'vscode'; import * as path from 'path'; +import { ChildProcess } from 'child_process'; import { ExecutionFactoryCreateWithEnvironmentOptions, ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; -import { traceError, traceInfo, traceLog } from '../../../logging'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; -import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER } from './utils'; +import { + createDiscoveryErrorPayload, + createEOTPayload, + createExecutionErrorPayload, + extractJsonPayload, +} from './utils'; import { createDeferred } from '../../../common/utils/async'; export class PythonTestServer implements ITestServer, Disposable { @@ -35,56 +41,22 @@ export class PythonTestServer implements ITestServer, Disposable { this.server = net.createServer((socket: net.Socket) => { let buffer: Buffer = Buffer.alloc(0); // Buffer to accumulate received data socket.on('data', (data: Buffer) => { - try { - let rawData: string = data.toString(); - buffer = Buffer.concat([buffer, data]); - while (buffer.length > 0) { - const rpcHeaders = jsonRPCHeaders(buffer.toString()); - const uuid = rpcHeaders.headers.get(JSONRPC_UUID_HEADER); - const totalContentLength = rpcHeaders.headers.get('Content-Length'); - if (!uuid) { - traceError('On data received: Error occurred because payload UUID is undefined'); - this._onDataReceived.fire({ uuid: '', data: '' }); - return; - } - if (!this.uuids.includes(uuid)) { - traceError('On data received: Error occurred because the payload UUID is not recognized'); - this._onDataReceived.fire({ uuid: '', data: '' }); - return; - } - rawData = rpcHeaders.remainingRawData; - const rpcContent = jsonRPCContent(rpcHeaders.headers, rawData); - const extractedData = rpcContent.extractedJSON; - // do not send until we have the full content - if (extractedData.length === Number(totalContentLength)) { - // if the rawData includes tests then this is a discovery request - if (rawData.includes(`"tests":`)) { - this._onDiscoveryDataReceived.fire({ - uuid, - data: rpcContent.extractedJSON, - }); - // if the rawData includes result then this is a run request - } else if (rawData.includes(`"result":`)) { - this._onRunDataReceived.fire({ - uuid, - data: rpcContent.extractedJSON, - }); - } else { - traceLog( - `Error processing test server request: request is not recognized as discovery or run.`, - ); - this._onDataReceived.fire({ uuid: '', data: '' }); - return; - } - // this.uuids = this.uuids.filter((u) => u !== uuid); WHERE DOES THIS GO?? - buffer = Buffer.alloc(0); - } else { + buffer = Buffer.concat([buffer, data]); // get the new data and add it to the buffer + while (buffer.length > 0) { + try { + // try to resolve data, returned unresolved data + const remainingBuffer = this._resolveData(buffer); + if (remainingBuffer.length === buffer.length) { + // if the remaining buffer is exactly the same as the buffer before processing, + // then there is no more data to process so loop should be exited. break; } + buffer = remainingBuffer; + } catch (ex) { + traceError(`Error reading data from buffer: ${ex} observed.`); + buffer = Buffer.alloc(0); + this._onDataReceived.fire({ uuid: '', data: '' }); } - } catch (ex) { - traceError(`Error processing test server request: ${ex} observe`); - this._onDataReceived.fire({ uuid: '', data: '' }); } }); }); @@ -107,6 +79,47 @@ export class PythonTestServer implements ITestServer, Disposable { }); } + savedBuffer = ''; + + public _resolveData(buffer: Buffer): Buffer { + try { + const extractedJsonPayload = extractJsonPayload(buffer.toString(), this.uuids); + // what payload is so small it doesn't include the whole UUID think got this + if (extractedJsonPayload.uuid !== undefined && extractedJsonPayload.cleanedJsonData !== undefined) { + // if a full json was found in the buffer, fire the data received event then keep cycling with the remaining raw data. + traceVerbose(`Firing data received event, ${extractedJsonPayload.cleanedJsonData}`); + this._fireDataReceived(extractedJsonPayload.uuid, extractedJsonPayload.cleanedJsonData); + } + buffer = Buffer.from(extractedJsonPayload.remainingRawData); + if (buffer.length === 0) { + // if the buffer is empty, then there is no more data to process so buffer should be cleared. + buffer = Buffer.alloc(0); + } + } catch (ex) { + traceError(`Error attempting to resolve data: ${ex}`); + this._onDataReceived.fire({ uuid: '', data: '' }); + } + return buffer; + } + + private _fireDataReceived(uuid: string, extractedJSON: string): void { + if (extractedJSON.includes(`"tests":`) || extractedJSON.includes(`"command_type": "discovery"`)) { + this._onDiscoveryDataReceived.fire({ + uuid, + data: extractedJSON, + }); + // if the rawData includes result then this is a run request + } else if (extractedJSON.includes(`"result":`) || extractedJSON.includes(`"command_type": "execution"`)) { + this._onRunDataReceived.fire({ + uuid, + data: extractedJSON, + }); + } else { + traceError(`Error processing test server request: request is not recognized as discovery or run.`); + this._onDataReceived.fire({ uuid: '', data: '' }); + } + } + public serverReady(): Promise { return this.ready; } @@ -133,6 +146,14 @@ export class PythonTestServer implements ITestServer, Disposable { return this._onDiscoveryDataReceived.event; } + public triggerRunDataReceivedEvent(payload: DataReceivedEvent): void { + this._onRunDataReceived.fire(payload); + } + + public triggerDiscoveryDataReceivedEvent(payload: DataReceivedEvent): void { + this._onDiscoveryDataReceived.fire(payload); + } + public dispose(): void { this.server.close(); this._onDataReceived.dispose(); @@ -146,6 +167,7 @@ export class PythonTestServer implements ITestServer, Disposable { options: TestCommandOptions, runTestIdPort?: string, runInstance?: TestRun, + testIds?: string[], callback?: () => void, ): Promise { const { uuid } = options; @@ -157,7 +179,11 @@ export class PythonTestServer implements ITestServer, Disposable { cwd: options.cwd, throwOnStdErr: true, outputChannel: options.outChannel, - extraVariables: { PYTHONPATH: pythonPathCommand }, + extraVariables: { + PYTHONPATH: pythonPathCommand, + TEST_UUID: uuid.toString(), + TEST_PORT: this.getPort().toString(), + }, }; if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; @@ -169,12 +195,7 @@ export class PythonTestServer implements ITestServer, Disposable { }; const execService = await this.executionFactory.createActivatedEnvironment(creationOptions); - // Add the generated UUID to the data to be sent (expecting to receive it back). - // first check if we have testIds passed in (in case of execution) and - // insert appropriate flag and test id array - const args = [options.command.script, '--port', this.getPort().toString(), '--uuid', uuid].concat( - options.command.args, - ); + const args = [options.command.script].concat(options.command.args); if (options.outChannel) { options.outChannel.appendLine(`python ${args.join(' ')}`); @@ -202,14 +223,23 @@ export class PythonTestServer implements ITestServer, Disposable { // This means it is running discovery traceLog(`Discovering unittest tests with arguments: ${args}\r\n`); } - const deferred = createDeferred>(); + const deferredTillExecClose = createDeferred>(); - const result = execService.execObservable(args, spawnOptions); + let resultProc: ChildProcess | undefined; runInstance?.token.onCancellationRequested(() => { - result?.proc?.kill(); + traceInfo('Test run cancelled, killing unittest subprocess.'); + // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. + if (resultProc) { + resultProc?.kill(); + } else { + deferredTillExecClose?.resolve(); + } }); + const result = execService?.execObservable(args, spawnOptions); + resultProc = result?.proc; + // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. result?.proc?.stdout?.on('data', (data) => { @@ -218,14 +248,49 @@ export class PythonTestServer implements ITestServer, Disposable { result?.proc?.stderr?.on('data', (data) => { spawnOptions?.outputChannel?.append(data.toString()); }); - result?.proc?.on('exit', () => { - traceLog('Exec server closed.', uuid); - deferred.resolve({ stdout: '', stderr: '' }); - callback?.(); + result?.proc?.on('exit', (code, signal) => { + if (code !== 0) { + traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}`); + } + }); + + result?.proc?.on('exit', (code, signal) => { + // if the child has testIds then this is a run request + if (code !== 0 && testIds && testIds?.length !== 0) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, + ); + // if the child process exited with a non-zero exit code, then we need to send the error payload. + this._onRunDataReceived.fire({ + uuid, + data: JSON.stringify(createExecutionErrorPayload(code, signal, testIds, options.cwd)), + }); + // then send a EOT payload + this._onRunDataReceived.fire({ + uuid, + data: JSON.stringify(createEOTPayload(true)), + }); + } else if (code !== 0) { + // This occurs when we are running discovery + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, + ); + this._onDiscoveryDataReceived.fire({ + uuid, + data: JSON.stringify(createDiscoveryErrorPayload(code, signal, options.cwd)), + }); + // then send a EOT payload + this._onDiscoveryDataReceived.fire({ + uuid, + data: JSON.stringify(createEOTPayload(true)), + }); + } + deferredTillExecClose.resolve({ stdout: '', stderr: '' }); }); - await deferred.promise; + await deferredTillExecClose.promise; } } catch (ex) { + traceError(`Error while server attempting to run unittest command: ${ex}`); this.uuids = this.uuids.filter((u) => u !== uuid); this._onDataReceived.fire({ uuid, diff --git a/extensions/positron-python/src/client/testing/testController/common/types.ts b/extensions/positron-python/src/client/testing/testController/common/types.ts index 16c0bd0e3cee..32e0c4ba8cc6 100644 --- a/extensions/positron-python/src/client/testing/testController/common/types.ts +++ b/extensions/positron-python/src/client/testing/testController/common/types.ts @@ -14,6 +14,7 @@ import { } from 'vscode'; import { ITestDebugLauncher, TestDiscoveryOptions } from '../../common/types'; import { IPythonExecutionFactory } from '../../../common/process/types'; +import { Deferred } from '../../../common/utils/async'; export type TestRunInstanceOptions = TestRunOptions & { exclude?: readonly TestItem[]; @@ -178,19 +179,32 @@ export interface ITestServer { options: TestCommandOptions, runTestIdsPort?: string, runInstance?: TestRun, + testIds?: string[], callback?: () => void, ): Promise; serverReady(): Promise; getPort(): number; createUUID(cwd: string): string; deleteUUID(uuid: string): void; + triggerRunDataReceivedEvent(data: DataReceivedEvent): void; + triggerDiscoveryDataReceivedEvent(data: DataReceivedEvent): void; } export interface ITestResultResolver { runIdToVSid: Map; runIdToTestItem: Map; vsIdToRunId: Map; - resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise; - resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise; + resolveDiscovery( + payload: DiscoveredTestPayload | EOTTestPayload, + deferredTillEOT: Deferred, + token?: CancellationToken, + ): Promise; + resolveExecution( + payload: ExecutionTestPayload | EOTTestPayload, + runInstance: TestRun, + deferredTillEOT: Deferred, + ): Promise; + _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise; + _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise; } export interface ITestDiscoveryAdapter { // ** first line old method signature, second line new method signature @@ -239,6 +253,11 @@ export type DiscoveredTestPayload = { error?: string[]; }; +export type EOTTestPayload = { + commandType: 'discovery' | 'execution'; + eot: boolean; +}; + export type ExecutionTestPayload = { cwd: string; status: 'success' | 'error'; diff --git a/extensions/positron-python/src/client/testing/testController/common/utils.ts b/extensions/positron-python/src/client/testing/testController/common/utils.ts index f98550d3e72b..f5f416529c42 100644 --- a/extensions/positron-python/src/client/testing/testController/common/utils.ts +++ b/extensions/positron-python/src/client/testing/testController/common/utils.ts @@ -9,27 +9,101 @@ import { EnableTestAdapterRewrite } from '../../../common/experiments/groups'; import { IExperimentService } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { DebugTestTag, ErrorTestItemOptions, RunTestTag } from './testItemUtilities'; -import { DiscoveredTestItem, DiscoveredTestNode, ITestResultResolver } from './types'; +import { + DiscoveredTestItem, + DiscoveredTestNode, + DiscoveredTestPayload, + EOTTestPayload, + ExecutionTestPayload, + ITestResultResolver, +} from './types'; +import { Deferred, createDeferred } from '../../../common/utils/async'; export function fixLogLines(content: string): string { const lines = content.split(/\r?\n/g); return `${lines.join('\r\n')}\r\n`; } -export interface IJSONRPCContent { +export interface IJSONRPCData { extractedJSON: string; remainingRawData: string; } -export interface IJSONRPCHeaders { +export interface ParsedRPCHeadersAndData { headers: Map; remainingRawData: string; } +export interface ExtractOutput { + uuid: string | undefined; + cleanedJsonData: string | undefined; + remainingRawData: string; +} + export const JSONRPC_UUID_HEADER = 'Request-uuid'; export const JSONRPC_CONTENT_LENGTH_HEADER = 'Content-Length'; export const JSONRPC_CONTENT_TYPE_HEADER = 'Content-Type'; -export function jsonRPCHeaders(rawData: string): IJSONRPCHeaders { +export function createTestingDeferred(): Deferred { + return createDeferred(); +} + +export function extractJsonPayload(rawData: string, uuids: Array): ExtractOutput { + /** + * Extracts JSON-RPC payload from the provided raw data. + * @param {string} rawData - The raw string data from which the JSON payload will be extracted. + * @param {Array} uuids - The list of UUIDs that are active. + * @returns {string} The remaining raw data after the JSON payload is extracted. + */ + + const rpcHeaders: ParsedRPCHeadersAndData = parseJsonRPCHeadersAndData(rawData); + + // verify the RPC has a UUID and that it is recognized + let uuid = rpcHeaders.headers.get(JSONRPC_UUID_HEADER); + uuid = checkUuid(uuid, uuids); + + const payloadLength = rpcHeaders.headers.get('Content-Length'); + + // separate out the data within context length of the given payload from the remaining data in the buffer + const rpcContent: IJSONRPCData = ExtractJsonRPCData(payloadLength, rpcHeaders.remainingRawData); + const cleanedJsonData = rpcContent.extractedJSON; + const { remainingRawData } = rpcContent; + + // if the given payload has the complete json, process it otherwise wait for the rest in the buffer + if (cleanedJsonData.length === Number(payloadLength)) { + // call to process this data + // remove this data from the buffer + return { uuid, cleanedJsonData, remainingRawData }; + } + // wait for the remaining + return { uuid: undefined, cleanedJsonData: undefined, remainingRawData: rawData }; +} + +export function checkUuid(uuid: string | undefined, uuids: Array): string | undefined { + if (!uuid) { + // no UUID found, this could occurred if the payload is full yet so send back without erroring + return undefined; + } + if (!uuids.includes(uuid)) { + // no UUID found, this could occurred if the payload is full yet so send back without erroring + throw new Error('On data received: Error occurred because the payload UUID is not recognized'); + } + return uuid; +} + +export function parseJsonRPCHeadersAndData(rawData: string): ParsedRPCHeadersAndData { + /** + * Parses the provided raw data to extract JSON-RPC specific headers and remaining data. + * + * This function aims to extract specific JSON-RPC headers (like UUID, content length, + * and content type) from the provided raw string data. Headers are expected to be + * delimited by newlines and the format should be "key:value". The function stops parsing + * once it encounters an empty line, and the rest of the data after this line is treated + * as the remaining raw data. + * + * @param {string} rawData - The raw string containing headers and possibly other data. + * @returns {ParsedRPCHeadersAndData} An object containing the parsed headers as a map and the + * remaining raw data after the headers. + */ const lines = rawData.split('\n'); let remainingRawData = ''; const headerMap = new Map(); @@ -40,8 +114,10 @@ export function jsonRPCHeaders(rawData: string): IJSONRPCHeaders { break; } const [key, value] = line.split(':'); - if ([JSONRPC_UUID_HEADER, JSONRPC_CONTENT_LENGTH_HEADER, JSONRPC_CONTENT_TYPE_HEADER].includes(key)) { - headerMap.set(key.trim(), value.trim()); + if (value && value.trim()) { + if ([JSONRPC_UUID_HEADER, JSONRPC_CONTENT_LENGTH_HEADER, JSONRPC_CONTENT_TYPE_HEADER].includes(key)) { + headerMap.set(key.trim(), value.trim()); + } } } @@ -51,8 +127,21 @@ export function jsonRPCHeaders(rawData: string): IJSONRPCHeaders { }; } -export function jsonRPCContent(headers: Map, rawData: string): IJSONRPCContent { - const length = parseInt(headers.get('Content-Length') ?? '0', 10); +export function ExtractJsonRPCData(payloadLength: string | undefined, rawData: string): IJSONRPCData { + /** + * Extracts JSON-RPC content based on provided headers and raw data. + * + * This function uses the `Content-Length` header from the provided headers map + * to determine how much of the rawData string represents the actual JSON content. + * After extracting the expected content, it also returns any remaining data + * that comes after the extracted content as remaining raw data. + * + * @param {string | undefined} payloadLength - The value of the `Content-Length` header. + * @param {string} rawData - The raw string data from which the JSON content will be extracted. + * + * @returns {IJSONRPCContent} An object containing the extracted JSON content and any remaining raw data. + */ + const length = parseInt(payloadLength ?? '0', 10); const data = rawData.slice(0, length); const remainingRawData = rawData.slice(length); return { @@ -124,7 +213,7 @@ export async function startTestIdServer(testIds: string[]): Promise { } export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { - const labelText = testType === 'pytest' ? 'Pytest Discovery Error' : 'Unittest Discovery Error'; + const labelText = testType === 'pytest' ? 'pytest Discovery Error' : 'Unittest Discovery Error'; return { id: `DiscoveryError:${uri.fsPath}`, label: `${labelText} [${path.basename(uri.fsPath)}]`, @@ -188,3 +277,65 @@ export function populateTestTree( function isTestItem(test: DiscoveredTestNode | DiscoveredTestItem): test is DiscoveredTestItem { return test.type_ === 'test'; } + +export function createExecutionErrorPayload( + code: number | null, + signal: NodeJS.Signals | null, + testIds: string[], + cwd: string, +): ExecutionTestPayload { + const etp: ExecutionTestPayload = { + cwd, + status: 'error', + error: 'Test run failed, the python test process was terminated before it could exit on its own.', + result: {}, + }; + // add error result for each attempted test. + for (let i = 0; i < testIds.length; i = i + 1) { + const test = testIds[i]; + etp.result![test] = { + test, + outcome: 'error', + message: ` \n The python test process was terminated before it could exit on its own, the process errored with: Code: ${code}, Signal: ${signal}`, + }; + } + return etp; +} + +export function createDiscoveryErrorPayload( + code: number | null, + signal: NodeJS.Signals | null, + cwd: string, +): DiscoveredTestPayload { + return { + cwd, + status: 'error', + error: [ + ` \n The python test process was terminated before it could exit on its own, the process errored with: Code: ${code}, Signal: ${signal}`, + ], + }; +} + +export function createEOTPayload(executionBool: boolean): EOTTestPayload { + return { + commandType: executionBool ? 'execution' : 'discovery', + eot: true, + } as EOTTestPayload; +} + +/** + * Splits a test name into its parent test name and subtest unique section. + * + * @param testName The full test name string. + * @returns A tuple where the first item is the parent test name and the second item is the subtest section or `testName` if no subtest section exists. + */ +export function splitTestNameWithRegex(testName: string): [string, string] { + // If a match is found, return the parent test name and the subtest (whichever was captured between parenthesis or square brackets). + // Otherwise, return the entire testName for the parent and entire testName for the subtest. + const regex = /^(.*?) ([\[(].*[\])])$/; + const match = testName.match(regex); + if (match) { + return [match[1].trim(), match[2] || match[3] || testName]; + } + return [testName, testName]; +} diff --git a/extensions/positron-python/src/client/testing/testController/controller.ts b/extensions/positron-python/src/client/testing/testController/controller.ts index 1550323ff8f8..af77ab2b2525 100644 --- a/extensions/positron-python/src/client/testing/testController/controller.ts +++ b/extensions/positron-python/src/client/testing/testController/controller.ts @@ -371,6 +371,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc ); const dispose = token.onCancellationRequested(() => { + runInstance.appendOutput(`Run instance cancelled.\r\n`); runInstance.end(); }); @@ -461,7 +462,6 @@ export class PythonTestController implements ITestController, IExtensionSingleAc ); } } - if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { unconfiguredWorkspaces.push(workspace); } diff --git a/extensions/positron-python/src/client/testing/testController/pytest/pytestController.ts b/extensions/positron-python/src/client/testing/testController/pytest/pytestController.ts index 997e3e29b7ec..d23cac842cda 100644 --- a/extensions/positron-python/src/client/testing/testController/pytest/pytestController.ts +++ b/extensions/positron-python/src/client/testing/testController/pytest/pytestController.ts @@ -235,7 +235,7 @@ export class PytestController implements ITestFrameworkController { testController.items.add( createErrorTestItem(testController, { id: `DiscoveryError:${workspace.uri.fsPath}`, - label: `Pytest Discovery Error [${path.basename(workspace.uri.fsPath)}]`, + label: `pytest Discovery Error [${path.basename(workspace.uri.fsPath)}]`, error: util.format( `${cancel} discovering pytest tests (see Output > Python):\r\n`, message.length > 0 ? message : ex, diff --git a/extensions/positron-python/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/extensions/positron-python/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 450e2ef1edf2..c0e1a310ee4a 100644 --- a/extensions/positron-python/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/extensions/positron-python/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -4,14 +4,13 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { ExecutionFactoryCreateWithEnvironmentOptions, - ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred } from '../../../common/utils/async'; +import { Deferred, createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { traceVerbose } from '../../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; import { DataReceivedEvent, DiscoveredTestPayload, @@ -19,6 +18,7 @@ import { ITestResultResolver, ITestServer, } from '../common/types'; +import { createDiscoveryErrorPayload, createEOTPayload, createTestingDeferred } from '../common/utils'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied @@ -32,20 +32,21 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { ) {} async discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { - const settings = this.configSettings.getSettings(uri); const uuid = this.testServer.createUUID(uri.fsPath); - const { pytestArgs } = settings.testing; - traceVerbose(pytestArgs); - const dataReceivedDisposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { - this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); + const deferredTillEOT: Deferred = createDeferred(); + const dataReceivedDisposable = this.testServer.onDiscoveryDataReceived(async (e: DataReceivedEvent) => { + this.resultResolver?.resolveDiscovery(JSON.parse(e.data), deferredTillEOT); }); const disposeDataReceiver = function (testServer: ITestServer) { + traceInfo(`Disposing data receiver for ${uri.fsPath} and deleting UUID; pytest discovery.`); testServer.deleteUUID(uuid); dataReceivedDisposable.dispose(); }; try { await this.runPytestDiscovery(uri, uuid, executionFactory); } finally { + await deferredTillEOT.promise; + traceVerbose('deferredTill EOT resolved'); disposeDataReceiver(this.testServer); } // this is only a placeholder to handle function overloading until rewrite is finished @@ -54,7 +55,6 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { } async runPytestDiscovery(uri: Uri, uuid: string, executionFactory?: IPythonExecutionFactory): Promise { - const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); const settings = this.configSettings.getSettings(uri); @@ -82,8 +82,10 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; const execService = await executionFactory?.createActivatedEnvironment(creationOptions); // delete UUID following entire discovery finishing. - const deferredExec = createDeferred>(); const execArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); + traceVerbose(`Running pytest discovery with command: ${execArgs.join(' ')}`); + + const deferredTillExecClose: Deferred = createTestingDeferred(); const result = execService?.execObservable(execArgs, spawnOptions); // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. @@ -94,11 +96,31 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { result?.proc?.stderr?.on('data', (data) => { spawnOptions.outputChannel?.append(data.toString()); }); - result?.proc?.on('exit', () => { - deferredExec.resolve({ stdout: '', stderr: '' }); - deferred.resolve(); + result?.proc?.on('exit', (code, signal) => { + if (code !== 0) { + traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}.`); + } }); - - await deferredExec.promise; + result?.proc?.on('close', (code, signal) => { + if (code !== 0) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, + ); + // if the child process exited with a non-zero exit code, then we need to send the error payload. + this.testServer.triggerDiscoveryDataReceivedEvent({ + uuid, + data: JSON.stringify(createDiscoveryErrorPayload(code, signal, cwd)), + }); + // then send a EOT payload + this.testServer.triggerDiscoveryDataReceivedEvent({ + uuid, + data: JSON.stringify(createEOTPayload(true)), + }); + } + // deferredTillEOT is resolved when all data sent on stdout and stderr is received, close event is only called when this occurs + // due to the sync reading of the output. + deferredTillExecClose?.resolve(); + }); + await deferredTillExecClose.promise; } } diff --git a/extensions/positron-python/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/extensions/positron-python/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 96d53db22c1c..8020be17cf90 100644 --- a/extensions/positron-python/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/extensions/positron-python/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -3,9 +3,10 @@ import { TestRun, Uri } from 'vscode'; import * as path from 'path'; +import { ChildProcess } from 'child_process'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred } from '../../../common/utils/async'; -import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; +import { Deferred } from '../../../common/utils/async'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, @@ -15,7 +16,6 @@ import { } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, - ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -42,29 +42,43 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { debugLauncher?: ITestDebugLauncher, ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); - traceVerbose(uri, testIds, debugBool); + // deferredTillEOT is resolved when all data sent over payload is received + const deferredTillEOT: Deferred = utils.createTestingDeferred(); + const dataReceivedDisposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { - this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); + const eParsed = JSON.parse(e.data); + this.resultResolver?.resolveExecution(eParsed, runInstance, deferredTillEOT); + } else { + traceError('No run instance found, cannot resolve execution.'); } }); const disposeDataReceiver = function (testServer: ITestServer) { + traceInfo(`Disposing data receiver for ${uri.fsPath} and deleting UUID; pytest execution.`); testServer.deleteUUID(uuid); dataReceivedDisposable.dispose(); }; runInstance?.token.onCancellationRequested(() => { - disposeDataReceiver(this.testServer); + traceInfo("Test run cancelled, resolving 'till EOT' deferred."); + deferredTillEOT.resolve(); }); - await this.runTestsNew( - uri, - testIds, - uuid, - runInstance, - debugBool, - executionFactory, - debugLauncher, - disposeDataReceiver, - ); + + try { + await this.runTestsNew( + uri, + testIds, + uuid, + runInstance, + debugBool, + executionFactory, + debugLauncher, + deferredTillEOT, + ); + } finally { + await deferredTillEOT.promise; + traceVerbose('deferredTill EOT resolved'); + disposeDataReceiver(this.testServer); + } // placeholder until after the rewrite is adopted // TODO: remove after adoption. @@ -84,19 +98,16 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { debugBool?: boolean, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, - disposeDataReceiver?: (testServer: ITestServer) => void, + deferredTillEOT?: Deferred, ): Promise { - const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); - this.configSettings.isTestExecution(); const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); - const spawnOptions: SpawnOptions = { cwd, throwOnStdErr: true, @@ -116,7 +127,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { }; // need to check what will happen in the exec service is NOT defined and is null const execService = await executionFactory?.createActivatedEnvironment(creationOptions); - try { // Remove positional test folders and files, we will add as needed per node const testArgs = removePositionalFoldersAndFiles(pytestArgs); @@ -130,7 +140,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { if (debugBool && !testArgs.some((a) => a.startsWith('--capture') || a === '-s')) { testArgs.push('--capture', 'no'); } - traceLog(`Running PYTEST execution for the following test ids: ${testIds}`); const pytestRunTestIdsPort = await utils.startTestIdServer(testIds); if (spawnOptions.extraVariables) @@ -150,22 +159,31 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { }; traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions, () => { - deferred.resolve(); - this.testServer.deleteUUID(uuid); + deferredTillEOT?.resolve(); }); } else { + // deferredTillExecClose is resolved when all stdout and stderr is read + const deferredTillExecClose: Deferred = utils.createTestingDeferred(); // combine path to run script with run args const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); const runArgs = [scriptPath, ...testArgs]; - traceInfo(`Running pytests with arguments: ${runArgs.join(' ')}\r\n`); + traceInfo(`Running pytest with arguments: ${runArgs.join(' ')}\r\n`); - const deferredExec = createDeferred>(); - const result = execService?.execObservable(runArgs, spawnOptions); + let resultProc: ChildProcess | undefined; runInstance?.token.onCancellationRequested(() => { - result?.proc?.kill(); + traceInfo('Test run cancelled, killing pytest subprocess.'); + // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. + if (resultProc) { + resultProc?.kill(); + } else { + deferredTillExecClose?.resolve(); + } }); + const result = execService?.execObservable(runArgs, spawnOptions); + resultProc = result?.proc; + // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. result?.proc?.stdout?.on('data', (data) => { @@ -174,20 +192,46 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { result?.proc?.stderr?.on('data', (data) => { this.outputChannel?.append(data.toString()); }); + result?.proc?.on('exit', (code, signal) => { + if (code !== 0 && testIds) { + traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}.`); + } + }); - result?.proc?.on('exit', () => { - deferredExec.resolve({ stdout: '', stderr: '' }); - deferred.resolve(); - disposeDataReceiver?.(this.testServer); + result?.proc?.on('close', (code, signal) => { + traceVerbose('Test run finished, subprocess closed.'); + // if the child has testIds then this is a run request + // if the child process exited with a non-zero exit code, then we need to send the error payload. + if (code !== 0 && testIds) { + traceError( + `Subprocess closed unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, + ); + this.testServer.triggerRunDataReceivedEvent({ + uuid, + data: JSON.stringify(utils.createExecutionErrorPayload(code, signal, testIds, cwd)), + }); + // then send a EOT payload + this.testServer.triggerRunDataReceivedEvent({ + uuid, + data: JSON.stringify(utils.createEOTPayload(true)), + }); + } + // deferredTillEOT is resolved when all data sent on stdout and stderr is received, close event is only called when this occurs + // due to the sync reading of the output. + deferredTillExecClose?.resolve(); }); - await deferredExec.promise; + await deferredTillExecClose?.promise; } } catch (ex) { traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); return Promise.reject(ex); } - const executionPayload: ExecutionTestPayload = { cwd, status: 'success', error: '' }; + const executionPayload: ExecutionTestPayload = { + cwd, + status: 'success', + error: '', + }; return executionPayload; } } diff --git a/extensions/positron-python/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/extensions/positron-python/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 1cbad7ef65ef..440df4f94dc6 100644 --- a/extensions/positron-python/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/extensions/positron-python/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -14,6 +14,7 @@ import { TestCommandOptions, TestDiscoveryCommand, } from '../common/types'; +import { Deferred, createDeferred } from '../../../common/utils/async'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. @@ -34,7 +35,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const command = buildDiscoveryCommand(unittestArgs); const uuid = this.testServer.createUUID(uri.fsPath); - + const deferredTillEOT: Deferred = createDeferred(); const options: TestCommandOptions = { workspaceFolder: uri, command, @@ -44,7 +45,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; const dataReceivedDisposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { - this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); + this.resultResolver?.resolveDiscovery(JSON.parse(e.data), deferredTillEOT); }); const disposeDataReceiver = function (testServer: ITestServer) { testServer.deleteUUID(uuid); @@ -52,8 +53,10 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; await this.callSendCommand(options, () => { - disposeDataReceiver(this.testServer); + disposeDataReceiver?.(this.testServer); }); + await deferredTillEOT.promise; + disposeDataReceiver(this.testServer); // placeholder until after the rewrite is adopted // TODO: remove after adoption. const discoveryPayload: DiscoveredTestPayload = { @@ -64,7 +67,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { } private async callSendCommand(options: TestCommandOptions, callback: () => void): Promise { - await this.testServer.sendCommand(options, undefined, undefined, callback); + await this.testServer.sendCommand(options, undefined, undefined, [], callback); const discoveryPayload: DiscoveredTestPayload = { cwd: '', status: 'success' }; return discoveryPayload; } diff --git a/extensions/positron-python/src/client/testing/testController/unittest/testExecutionAdapter.ts b/extensions/positron-python/src/client/testing/testController/unittest/testExecutionAdapter.ts index 9af9e593c246..9da0872ef601 100644 --- a/extensions/positron-python/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/extensions/positron-python/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { TestRun, Uri } from 'vscode'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred } from '../../../common/utils/async'; +import { Deferred, createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { DataReceivedEvent, @@ -15,7 +15,7 @@ import { TestCommandOptions, TestExecutionCommand, } from '../common/types'; -import { traceLog } from '../../../logging'; +import { traceError, traceInfo, traceLog } from '../../../logging'; import { startTestIdServer } from '../common/utils'; /** @@ -37,19 +37,30 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runInstance?: TestRun, ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); + const deferredTillEOT: Deferred = createDeferred(); const disposedDataReceived = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { - this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); + this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance, deferredTillEOT); + } else { + traceError('No run instance found, cannot resolve execution.'); } }); const disposeDataReceiver = function (testServer: ITestServer) { + traceInfo(`Disposing data receiver for ${uri.fsPath} and deleting UUID; unittest execution.`); testServer.deleteUUID(uuid); disposedDataReceived.dispose(); }; runInstance?.token.onCancellationRequested(() => { - disposeDataReceiver(this.testServer); + traceInfo("Test run cancelled, resolving 'till EOT' deferred."); + deferredTillEOT.resolve(); }); - await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, disposeDataReceiver); + try { + await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, deferredTillEOT); + await deferredTillEOT.promise; + disposeDataReceiver(this.testServer); + } catch (error) { + traceError(`Error in running unittest tests: ${error}`); + } const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; return executionPayload; } @@ -60,7 +71,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { uuid: string, runInstance?: TestRun, debugBool?: boolean, - disposeDataReceiver?: (testServer: ITestServer) => void, + deferredTillEOT?: Deferred, ): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; @@ -77,15 +88,12 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { testIds, outChannel: this.outputChannel, }; - - const deferred = createDeferred(); traceLog(`Running UNITTEST execution for the following test ids: ${testIds}`); const runTestIdsPort = await startTestIdServer(testIds); - await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, () => { - deferred.resolve(); - disposeDataReceiver?.(this.testServer); + await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, testIds, () => { + deferredTillEOT?.resolve(); }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. diff --git a/extensions/positron-python/src/test/common/installer.test.ts b/extensions/positron-python/src/test/common/installer.test.ts index 7ff0ee81c27f..5c1842a2c97c 100644 --- a/extensions/positron-python/src/test/common/installer.test.ts +++ b/extensions/positron-python/src/test/common/installer.test.ts @@ -87,7 +87,6 @@ import { ProductType, } from '../../client/common/types'; import { createDeferred } from '../../client/common/utils/async'; -import { getNamesAndValues } from '../../client/common/utils/enum'; import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; import { Random } from '../../client/common/utils/random'; import { ImportTracker } from '../../client/telemetry/importTracker'; @@ -105,6 +104,7 @@ import { } from '../../client/interpreter/configuration/types'; import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; +import { getProductsForInstallerTests } from './productsToTest'; suite('Installer', () => { let ioc: UnitTestIocContainer; @@ -276,7 +276,8 @@ suite('Installer', () => { await installer.isInstalled(product, resource); await checkInstalledDef.promise; } - getNamesAndValues(Product).forEach((prod) => { + + getProductsForInstallerTests().forEach((prod) => { test(`Ensure isInstalled for Product: '${prod.name}' executes the right command`, async function () { if ( new ProductService().getProductType(prod.value) === ProductType.DataScience || @@ -293,7 +294,7 @@ suite('Installer', () => { new MockModuleInstaller('two', true), ); ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.unittest || prod.value === Product.isort) { + if (prod.value === Product.unittest) { return undefined; } await testCheckingIfProductIsInstalled(prod.value); @@ -316,7 +317,8 @@ suite('Installer', () => { await installer.install(product); await checkInstalledDef.promise; } - getNamesAndValues(Product).forEach((prod) => { + + getProductsForInstallerTests().forEach((prod) => { test(`Ensure install for Product: '${prod.name}' executes the right command in IModuleInstaller`, async function () { const productType = new ProductService().getProductType(prod.value); if (productType === ProductType.DataScience || productType === ProductType.Python) { @@ -331,7 +333,7 @@ suite('Installer', () => { new MockModuleInstaller('two', true), ); ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.unittest || prod.value === Product.isort) { + if (prod.value === Product.unittest) { return undefined; } await testInstallingProduct(prod.value); diff --git a/extensions/positron-python/src/test/common/installer/installer.invalidPath.unit.test.ts b/extensions/positron-python/src/test/common/installer/installer.invalidPath.unit.test.ts index 7e8392204600..b6738759f0d7 100644 --- a/extensions/positron-python/src/test/common/installer/installer.invalidPath.unit.test.ts +++ b/extensions/positron-python/src/test/common/installer/installer.invalidPath.unit.test.ts @@ -14,10 +14,10 @@ import { ProductInstaller } from '../../../client/common/installer/productInstal import { ProductService } from '../../../client/common/installer/productService'; import { IProductPathService, IProductService } from '../../../client/common/installer/types'; import { IPersistentState, IPersistentStateFactory, Product, ProductType } from '../../../client/common/types'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../client/ioc/types'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { getProductsForInstallerTests } from '../productsToTest'; use(chaiAsPromised); @@ -26,7 +26,7 @@ suite('Module Installer - Invalid Paths', () => { ['moduleName', path.join('users', 'dev', 'tool', 'executable')].forEach((pathToExecutable) => { const isExecutableAModule = path.basename(pathToExecutable) === pathToExecutable; - getNamesAndValues(Product).forEach((product) => { + getProductsForInstallerTests().forEach((product) => { let installer: ProductInstaller; let serviceContainer: TypeMoq.IMock; let app: TypeMoq.IMock; @@ -78,7 +78,6 @@ suite('Module Installer - Invalid Paths', () => { }); switch (product.value) { - case Product.isort: case Product.unittest: { return; } diff --git a/extensions/positron-python/src/test/common/installer/installer.unit.test.ts b/extensions/positron-python/src/test/common/installer/installer.unit.test.ts index 38b9d9174472..69a5f3678f69 100644 --- a/extensions/positron-python/src/test/common/installer/installer.unit.test.ts +++ b/extensions/positron-python/src/test/common/installer/installer.unit.test.ts @@ -6,24 +6,11 @@ import { assert, expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as sinon from 'sinon'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Disposable, Uri, WorkspaceFolder } from 'vscode'; -import { ApplicationShell } from '../../../client/common/application/applicationShell'; -import { CommandManager } from '../../../client/common/application/commandManager'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../client/common/application/types'; -import { WorkspaceService } from '../../../client/common/application/workspace'; -import { ConfigurationService } from '../../../client/common/configuration/service'; -import { Commands } from '../../../client/common/constants'; -import { ExperimentService } from '../../../client/common/experiments/service'; +import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; import '../../../client/common/extensions'; -import { - FormatterInstaller, - LinterInstaller, - ProductInstaller, -} from '../../../client/common/installer/productInstaller'; -import { ProductNames } from '../../../client/common/installer/productNames'; -import { LinterProductPathService } from '../../../client/common/installer/productPath'; +import { ProductInstaller } from '../../../client/common/installer/productInstaller'; import { ProductService } from '../../../client/common/installer/productService'; import { IInstallationChannelManager, @@ -39,9 +26,7 @@ import { IPythonExecutionService, } from '../../../client/common/process/types'; import { - IConfigurationService, IDisposableRegistry, - IExperimentService, InstallerResponse, IPersistentState, IPersistentStateFactory, @@ -49,20 +34,17 @@ import { ProductType, } from '../../../client/common/types'; import { createDeferred, Deferred } from '../../../client/common/utils/async'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { ServiceContainer } from '../../../client/ioc/container'; import { IServiceContainer } from '../../../client/ioc/types'; -import { LinterManager } from '../../../client/linters/linterManager'; -import { ILinterManager } from '../../../client/linters/types'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; import { sleep } from '../../common'; +import { getProductsForInstallerTests } from '../productsToTest'; use(chaiAsPromised); suite('Module Installer only', () => { [undefined, Uri.file('resource')].forEach((resource) => { - getNamesAndValues(Product) + getProductsForInstallerTests() .concat([{ name: 'Unknown product', value: 404 }]) .forEach((product) => { @@ -183,9 +165,6 @@ suite('Module Installer only', () => { }); return; } - case Product.isort: { - return; - } case Product.unittest: { test(`Ensure resource info is passed into the module installer ${product.name} (${ resource ? 'With a resource' : 'without a resource' @@ -638,273 +617,5 @@ suite('Module Installer only', () => { workspaceService.verifyAll(); }); }); - - suite('Test FormatterInstaller.promptToInstallImplementation', () => { - class FormatterInstallerTest extends FormatterInstaller { - public async promptToInstallImplementation(product: Product, uri?: Uri): Promise { - return super.promptToInstallImplementation(product, uri); - } - - // eslint-disable-next-line class-methods-use-this - protected getStoredResponse(_key: string) { - return false; - } - - // eslint-disable-next-line class-methods-use-this - protected isExecutableAModule(_product: Product, _resource?: Uri) { - return true; - } - } - let installer: FormatterInstallerTest; - let appShell: IApplicationShell; - let configService: IConfigurationService; - let workspaceService: IWorkspaceService; - let productService: IProductService; - let cmdManager: ICommandManager; - setup(() => { - const serviceContainer = mock(ServiceContainer); - appShell = mock(ApplicationShell); - configService = mock(ConfigurationService); - workspaceService = mock(WorkspaceService); - productService = mock(ProductService); - cmdManager = mock(CommandManager); - - when(serviceContainer.get(IApplicationShell)).thenReturn(instance(appShell)); - when(serviceContainer.get(IConfigurationService)).thenReturn( - instance(configService), - ); - when(serviceContainer.get(IWorkspaceService)).thenReturn(instance(workspaceService)); - when(serviceContainer.get(IProductService)).thenReturn(instance(productService)); - when(serviceContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - - installer = new FormatterInstallerTest(instance(serviceContainer)); - }); - - teardown(() => { - sinon.restore(); - }); - - test('If nothing is selected, return Ignore as response', async () => { - const product = Product.autopep8; - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn((undefined as unknown) as Thenable); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Ignore); - }); - - test('If `Yes` is selected, install product', async () => { - const product = Product.autopep8; - const install = sinon.stub(FormatterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn(('Yes' as unknown) as Thenable); - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Installed); - assert.ok(install.calledOnceWith(product, resource, undefined)); - }); - - test('If `Use black` is selected, install black formatter', async () => { - const product = Product.autopep8; - const install = sinon.stub(FormatterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn(('Use black' as unknown) as Thenable); - when(configService.updateSetting('formatting.provider', 'black', resource)).thenResolve(); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Installed); - verify(configService.updateSetting('formatting.provider', 'black', resource)).once(); - assert.ok(install.calledOnceWith(Product.black, resource, undefined)); - }); - - test('If `Use yapf` is selected, install black formatter', async () => { - const product = Product.autopep8; - const install = sinon.stub(FormatterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn(('Use yapf' as unknown) as Thenable); - when(configService.updateSetting('formatting.provider', 'yapf', resource)).thenResolve(); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Installed); - verify(configService.updateSetting('formatting.provider', 'yapf', resource)).once(); - assert.ok(install.calledOnceWith(Product.yapf, resource, undefined)); - }); - }); - }); -}); - -[undefined, Uri.file('resource')].forEach((resource) => { - suite(`Test LinterInstaller with resource: ${resource}`, () => { - class LinterInstallerTest extends LinterInstaller { - public isModuleExecutable = true; - - public async promptToInstallImplementation(product: Product, uri?: Uri): Promise { - return super.promptToInstallImplementation(product, uri); - } - - // eslint-disable-next-line class-methods-use-this - protected getStoredResponse(_key: string) { - return false; - } - - protected isExecutableAModule(_product: Product, _resource?: Uri) { - return this.isModuleExecutable; - } - } - - let installer: LinterInstallerTest; - let appShell: IApplicationShell; - let configService: IConfigurationService; - let workspaceService: IWorkspaceService; - let productService: IProductService; - let cmdManager: ICommandManager; - let experimentsService: IExperimentService; - let linterManager: ILinterManager; - let serviceContainer: IServiceContainer; - let productPathService: IProductPathService; - setup(() => { - serviceContainer = mock(ServiceContainer); - appShell = mock(ApplicationShell); - configService = mock(ConfigurationService); - workspaceService = mock(WorkspaceService); - productService = mock(ProductService); - cmdManager = mock(CommandManager); - experimentsService = mock(ExperimentService); - linterManager = mock(LinterManager); - productPathService = mock(LinterProductPathService); - - when(serviceContainer.get(IApplicationShell)).thenReturn(instance(appShell)); - when(serviceContainer.get(IConfigurationService)).thenReturn( - instance(configService), - ); - when(serviceContainer.get(IWorkspaceService)).thenReturn(instance(workspaceService)); - when(serviceContainer.get(IProductService)).thenReturn(instance(productService)); - when(serviceContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - - const exp = instance(experimentsService); - when(serviceContainer.get(IExperimentService)).thenReturn(exp); - when(experimentsService.inExperiment(anything())).thenResolve(false); - - when(serviceContainer.get(ILinterManager)).thenReturn(instance(linterManager)); - when(serviceContainer.get(IProductPathService, ProductType.Linter)).thenReturn( - instance(productPathService), - ); - - installer = new LinterInstallerTest(instance(serviceContainer)); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Ensure 3 options for pylint', async () => { - const product = Product.pylint; - const options = ['Select Linter', "Don't show again"]; - const productName = ProductNames.get(product)!; - - await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).once(); - }); - test('Ensure select linter command is invoked', async () => { - const product = Product.pylint; - const options = ['Select Linter', "Don't show again"]; - const productName = ProductNames.get(product)!; - when( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).thenResolve(('Select Linter' as unknown) as void); - when(cmdManager.executeCommand(Commands.Set_Linter)).thenResolve(undefined); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).once(); - verify(cmdManager.executeCommand(Commands.Set_Linter)).once(); - expect(response).to.be.equal(InstallerResponse.Ignore); - }); - test('If install button is selected, install linter and return response', async () => { - const product = Product.pylint; - const options = ['Select Linter', "Don't show again"]; - const productName = ProductNames.get(product)!; - when( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).thenResolve(('Install' as unknown) as void); - when(cmdManager.executeCommand(Commands.Set_Linter)).thenResolve(undefined); - const install = sinon.stub(LinterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - const response = await installer.promptToInstallImplementation(product, resource); - - expect(response).to.be.equal(InstallerResponse.Installed); - assert.ok(install.calledOnceWith(product, resource, undefined)); - }); }); }); diff --git a/extensions/positron-python/src/test/common/installer/productInstaller.unit.test.ts b/extensions/positron-python/src/test/common/installer/productInstaller.unit.test.ts index a63ce23aa438..7e21a241ff31 100644 --- a/extensions/positron-python/src/test/common/installer/productInstaller.unit.test.ts +++ b/extensions/positron-python/src/test/common/installer/productInstaller.unit.test.ts @@ -3,26 +3,15 @@ 'use strict'; -import * as assert from 'assert'; import { expect } from 'chai'; -import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; import { IApplicationShell } from '../../../client/common/application/types'; -import { DataScienceInstaller, FormatterInstaller } from '../../../client/common/installer/productInstaller'; -import { ProductNames } from '../../../client/common/installer/productNames'; -import { - IInstallationChannelManager, - IModuleInstaller, - InterpreterUri, - IProductPathService, - IProductService, -} from '../../../client/common/installer/types'; -import { InstallerResponse, IPersistentStateFactory, Product, ProductType } from '../../../client/common/types'; -import { Common } from '../../../client/common/utils/localize'; +import { DataScienceInstaller } from '../../../client/common/installer/productInstaller'; +import { IInstallationChannelManager, IModuleInstaller, InterpreterUri } from '../../../client/common/installer/types'; +import { InstallerResponse, Product } from '../../../client/common/types'; import { Architecture } from '../../../client/common/utils/platform'; import { IServiceContainer } from '../../../client/ioc/types'; import { EnvironmentType, ModuleInstallerType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { MockMemento } from '../../mocks/mementos'; class AlwaysInstalledDataScienceInstaller extends DataScienceInstaller { // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this @@ -213,250 +202,3 @@ suite('DataScienceInstaller install', async () => { }); }); - -suite('Formatter installer', async () => { - let serviceContainer: TypeMoq.IMock; - // let outputChannel: TypeMoq.IMock; - let appShell: TypeMoq.IMock; - let persistentStateFactory: TypeMoq.IMock; - let productPathService: TypeMoq.IMock; - // let isExecutableAsModuleStub: sinon.SinonStub; - - // constructor(protected serviceContainer: IServiceContainer, protected outputChannel: OutputChannel) { - // this.appShell = serviceContainer.get(IApplicationShell); - // this.configService = serviceContainer.get(IConfigurationService); - // this.workspaceService = serviceContainer.get(IWorkspaceService); - // this.productService = serviceContainer.get(IProductService); - // this.persistentStateFactory = serviceContainer.get(IPersistentStateFactory); - // } - - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - // outputChannel = TypeMoq.Mock.ofType(); - appShell = TypeMoq.Mock.ofType(); - persistentStateFactory = TypeMoq.Mock.ofType(); - productPathService = TypeMoq.Mock.ofType(); - - const installStub = sinon.stub(FormatterInstaller.prototype, 'install'); - installStub.returns(Promise.resolve(InstallerResponse.Installed)); - - const productService = TypeMoq.Mock.ofType(); - productService.setup((p) => p.getProductType(TypeMoq.It.isAny())).returns(() => ProductType.Formatter); - - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPersistentStateFactory))) - .returns(() => persistentStateFactory.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IProductService))).returns(() => productService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductPathService), ProductType.Formatter)) - .returns(() => productPathService.object); - }); - - teardown(() => { - sinon.restore(); - }); - - // - if black not installed, offer autopep8 and yapf options - // - if autopep8 not installed, offer black and yapf options - // - if yapf not installed, offer black and autopep8 options - // - if not executable as a module, display error message - // - if never show again was set to true earlier, ignore - // if never show again is selected, ignore - - test('If black is not installed, offer autopep8 and yapf as options', async () => { - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Installed); - }); - - test('If autopep8 is not installed, offer black and yapf as options', async () => { - const messageOptions = [ - Common.bannerLabelYes, - - 'Use {0}'.format(ProductNames.get(Product.black)!), - 'Use {0}'.format(ProductNames.get(Product.yapf)!), - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.autopep8); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Installed); - }); - - test('If yapf is not installed, offer autopep8 and black as options', async () => { - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.black)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.yapf); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Installed); - }); - - test('If the formatter is not executable as a module, display an error message', async () => { - const messageOptions = [ - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => false) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.getExecutableNameFromSettings(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => 'foo'); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - productPathService.verifyAll(); - }); - - test('If "Do not show again" has been selected earlier, do not display the prompt', async () => { - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.never()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: true, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - assert.strictEqual(result, InstallerResponse.Ignore); - }); - - test('If "Do not show again" is selected, do not install the formatter and do not show the prompt again', async () => { - let value = false; - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.doNotShowAgain)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value, - updateValue: (newValue) => { - value = newValue; - return Promise.resolve(); - }, - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.black); - const resultTwo = await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Ignore); - assert.strictEqual(resultTwo, InstallerResponse.Ignore); - }); -}); diff --git a/extensions/positron-python/src/test/common/installer/productPath.unit.test.ts b/extensions/positron-python/src/test/common/installer/productPath.unit.test.ts index 1e64ca63e117..0f627289da70 100644 --- a/extensions/positron-python/src/test/common/installer/productPath.unit.test.ts +++ b/extensions/positron-python/src/test/common/installer/productPath.unit.test.ts @@ -26,18 +26,18 @@ import { Product, ProductType, } from '../../../client/common/types'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; import { IFormatterHelper } from '../../../client/formatters/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { ILinterInfo, ILinterManager } from '../../../client/linters/types'; import { ITestsHelper } from '../../../client/testing/common/types'; import { ITestingSettings } from '../../../client/testing/configuration/types'; +import { getProductsForInstallerTests } from '../productsToTest'; use(chaiAsPromised); suite('Product Path', () => { [undefined, Uri.file('resource')].forEach((resource) => { - getNamesAndValues(Product).forEach((product) => { + getProductsForInstallerTests().forEach((product) => { class TestBaseProductPathsService extends BaseProductPathsService { public getExecutableNameFromSettings(_: Product, _resource?: Uri): string { return ''; @@ -75,9 +75,6 @@ suite('Product Path', () => { .returns(() => new ProductService()); }); - if (product.value === Product.isort) { - return; - } suite('Method isExecutableAModule()', () => { test('Returns true if User has customized the executable name', () => { productInstaller.translateProductToModuleName = () => 'moduleName'; diff --git a/extensions/positron-python/src/test/common/productsToTest.ts b/extensions/positron-python/src/test/common/productsToTest.ts new file mode 100644 index 000000000000..7fc06863f67c --- /dev/null +++ b/extensions/positron-python/src/test/common/productsToTest.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Product } from '../../client/common/types'; +import { getNamesAndValues } from '../../client/common/utils/enum'; + +export function getProductsForInstallerTests(): { name: string; value: Product }[] { + return getNamesAndValues(Product).filter( + (p) => + ![ + 'pylint', + 'flake8', + 'pycodestyle', + 'pylama', + 'prospector', + 'pydocstyle', + 'yapf', + 'autopep8', + 'mypy', + 'isort', + 'black', + 'bandit', + ].includes(p.name), + ); +} diff --git a/extensions/positron-python/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts b/extensions/positron-python/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts index 7059fb7ab26f..f177db5c2a32 100644 --- a/extensions/positron-python/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts +++ b/extensions/positron-python/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts @@ -31,6 +31,7 @@ import { import { EnvGroups, InterpreterStateArgs, + QuickPickType, SetInterpreterCommand, } from '../../../../client/interpreter/configuration/interpreterSelector/commands/setInterpreter'; import { @@ -265,8 +266,14 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; - const activeItem = await actualParameters!.activeItem; - assert.deepStrictEqual(activeItem, recommended); + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, recommended); + } else { + assert(false, 'Not a function'); + } delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); }); @@ -308,8 +315,14 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; - const activeItem = await actualParameters!.activeItem; - assert.deepStrictEqual(activeItem, noPythonInstalled); + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, noPythonInstalled); + } else { + assert(false, 'Not a function'); + } delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); }); @@ -666,8 +679,14 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; - const activeItem = await actualParameters!.activeItem; - assert.deepStrictEqual(activeItem, recommended); + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, recommended); + } else { + assert(false, 'Not a function'); + } delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts index c773e1cbd5bc..77077ad945fb 100644 --- a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts @@ -45,7 +45,7 @@ suite('Interpreter Path Command', () => { test('If `workspaceFolder` property exists in `args`, it is used to retrieve setting from config', async () => { const args = { workspaceFolder: 'folderPath' }; when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { - assert.deepEqual(arg, Uri.parse('folderPath')); + assert.deepEqual(arg, Uri.file('folderPath')); return Promise.resolve({ path: 'settingValue' }) as unknown; }); @@ -56,7 +56,7 @@ suite('Interpreter Path Command', () => { test('If `args[1]` is defined, it is used to retrieve setting from config', async () => { const args = ['command', 'folderPath']; when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { - assert.deepEqual(arg, Uri.parse('folderPath')); + assert.deepEqual(arg, Uri.file('folderPath')); return Promise.resolve({ path: 'settingValue' }) as unknown; }); @@ -73,14 +73,4 @@ suite('Interpreter Path Command', () => { const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); expect(setting).to.equal('settingValue'); }); - - test('If `args[1]` is not a valid uri', async () => { - const args = ['command', '${input:some_input}']; - when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { - assert.deepEqual(arg, undefined); - return Promise.resolve({ path: 'settingValue' }) as unknown; - }); - const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); - expect(setting).to.equal('settingValue'); - }); }); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts index 2aec3dcfd041..59f61f81cd85 100644 --- a/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts @@ -22,6 +22,7 @@ import * as platform from '../../../../../client/common/utils/platform'; import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; import { IEnvironmentActivationService } from '../../../../../client/interpreter/activation/types'; +import * as triggerApis from '../../../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; getInfoPerOS().forEach(([osName, osType, path]) => { if (osType === platform.OSType.Unknown) { @@ -42,12 +43,18 @@ getInfoPerOS().forEach(([osName, osType, path]) => { let getActiveTextEditorStub: sinon.SinonStub; let getOSTypeStub: sinon.SinonStub; let getWorkspaceFolderStub: sinon.SinonStub; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; setup(() => { getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); getOSTypeStub = sinon.stub(platform, 'getOSType'); getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); getOSTypeStub.returns(osType); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); }); teardown(() => { diff --git a/extensions/positron-python/src/test/debugger/extension/debugCommands.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/debugCommands.unit.test.ts index 3c023f3f1450..7d2463072f06 100644 --- a/extensions/positron-python/src/test/debugger/extension/debugCommands.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/debugCommands.unit.test.ts @@ -14,6 +14,7 @@ import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; import * as telemetry from '../../../client/telemetry'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as triggerApis from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; suite('Debugging - commands', () => { let commandManager: typemoq.IMock; @@ -21,6 +22,7 @@ suite('Debugging - commands', () => { let disposables: typemoq.IMock; let interpreterService: typemoq.IMock; let debugCommands: IExtensionSingleActivationService; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; setup(() => { commandManager = typemoq.Mock.ofType(); @@ -36,6 +38,11 @@ suite('Debugging - commands', () => { sinon.stub(telemetry, 'sendTelemetryEvent').callsFake(() => { /** noop */ }); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); }); teardown(() => { sinon.restore(); diff --git a/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 1513be676ee4..e41d6ce4d53c 100644 --- a/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -191,21 +191,28 @@ suite('Terminal Environment Variable Collection Service', () => { verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); }); - test('If activated variables contain PS1, prefix it using shell integration', async () => { - const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env, PS1: '(prompt)' }; + // eslint-disable-next-line consistent-return + test('If activated variables contain PS1, prefix it using shell integration', async function () { + if (getOSType() === OSType.Windows) { + return this.skip(); + } + const envVars: NodeJS.ProcessEnv = { + CONDA_PREFIX: 'prefix/to/conda', + ...process.env, + PS1: '(envName) extra prompt', // Should not use this + }; when( - environmentActivationService.getActivatedEnvironmentVariables( - anything(), - undefined, - undefined, - customShell, - ), + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + envName: 'envName', + } as unknown) as PythonEnvironment); + when(collection.replace(anything(), anything(), anything())).thenResolve(); when(collection.delete(anything())).thenResolve(); let opts: EnvironmentVariableMutatorOptions | undefined; - when(collection.prepend('PS1', '(prompt)', anything())).thenCall((_, _v, o) => { + when(collection.prepend('PS1', '(envName) ', anything())).thenCall((_, _v, o) => { opts = o; }); @@ -216,6 +223,85 @@ suite('Terminal Environment Variable Collection Service', () => { assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); }); + test('Respect VIRTUAL_ENV_DISABLE_PROMPT when setting PS1 for venv', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + VIRTUAL_BIN: 'prefix/to/conda', + ...process.env, + VIRTUAL_ENV_DISABLE_PROMPT: '1', + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + when(collection.prepend('PS1', anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.prepend('PS1', anything(), anything())).never(); + }); + + test('Otherwise set PS1 for venv even if PS1 is not returned', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + VIRTUAL_BIN: 'prefix/to/conda', + ...process.env, + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + when(collection.prepend('PS1', '(envName) ', anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.prepend('PS1', '(envName) ', anything())).once(); + }); + + test('Respect CONDA_PROMPT_MODIFIER when setting PS1 for conda', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + CONDA_PREFIX: 'prefix/to/conda', + ...process.env, + CONDA_PROMPT_MODIFIER: '(envName)', + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Conda, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PS1', '(envName) ', anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); + }); + test('Prepend only "prepend portion of PATH" where applicable', async () => { const processEnv = { PATH: 'hello/1/2/3' }; reset(environmentActivationService); @@ -248,12 +334,13 @@ suite('Terminal Environment Variable Collection Service', () => { assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); }); - test('Prepend full PATH otherwise', async () => { + test('Prepend full PATH with separator otherwise', async () => { const processEnv = { PATH: 'hello/1/2/3' }; reset(environmentActivationService); when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( processEnv, ); + const separator = getOSType() === OSType.Windows ? ';' : ':'; const finalPath = 'hello/3/2/1'; const envVars: NodeJS.ProcessEnv = { PATH: finalPath }; when( @@ -275,7 +362,7 @@ suite('Terminal Environment Variable Collection Service', () => { await terminalEnvVarCollectionService._applyCollection(undefined, customShell); verify(collection.clear()).once(); - verify(collection.prepend('PATH', finalPath, anything())).once(); + verify(collection.prepend('PATH', `${finalPath}${separator}`, anything())).once(); verify(collection.replace('PATH', anything(), anything())).never(); assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); }); @@ -415,7 +502,11 @@ suite('Terminal Environment Variable Collection Service', () => { test('Correct track that prompt was not set for non-Windows where PS1 is not set but env name is base', async () => { when(platform.osType).thenReturn(OSType.Linux); - const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + const envVars: NodeJS.ProcessEnv = { + CONDA_PREFIX: 'prefix/to/conda', + ...process.env, + CONDA_PROMPT_MODIFIER: '(base)', + }; const ps1Shell = 'zsh'; const resource = Uri.file('a'); const workspaceFolder: WorkspaceFolder = { @@ -570,7 +661,7 @@ suite('Terminal Environment Variable Collection Service', () => { await terminalEnvVarCollectionService._applyCollection(undefined, customShell); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(collection.clear()).twice(); + verify(collection.clear()).once(); }); test('If no activated variables are returned for default shell, clear collection', async () => { diff --git a/extensions/positron-python/src/test/linters/lint.multilinter.test.ts b/extensions/positron-python/src/test/linters/lint.multilinter.test.ts deleted file mode 100644 index dba263e78479..000000000000 --- a/extensions/positron-python/src/test/linters/lint.multilinter.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as path from 'path'; -import { ConfigurationTarget, DiagnosticCollection, Uri, window, workspace } from 'vscode'; -import { LanguageServerType } from '../../client/activation/types'; -import { ICommandManager } from '../../client/common/application/types'; -import { Product } from '../../client/common/installer/productInstaller'; -import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; -import { ExecutionResult, IPythonToolExecutionService, SpawnOptions } from '../../client/common/process/types'; -import { ExecutionInfo, IConfigurationService } from '../../client/common/types'; -import { ILinterManager } from '../../client/linters/types'; -import { deleteFile, IExtensionTestApi, PythonSettingKeys, rootWorkspaceUri } from '../common'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; - -const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); -const pythonFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'linting'); - -// Mocked out python tool execution (all we need is mocked linter return values). -class MockPythonToolExecService extends PythonToolExecutionService { - // Mocked samples of linter messages from flake8 and pylint: - public flake8Msg = - '1,1,W,W391:blank line at end of file\ns:142:13), :1\n1,7,E,E999:SyntaxError: invalid syntax\n'; - - public pylintMsg = `[ - { - "type": "error", - "module": "print", - "obj": "", - "line": 1, - "column": 0, - "path": "print.py", - "symbol": "syntax-error", - "message": "Missing parentheses in call to 'print'. Did you mean print(x)? (, line 1)", - "message-id": "E0001" - } -]`; - - // Depending on moduleName being exec'd, return the appropriate sample. - public async execForLinter( - executionInfo: ExecutionInfo, - _options: SpawnOptions, - _resource: Uri, - ): Promise> { - let msg = this.flake8Msg; - if (executionInfo.moduleName === 'pylint') { - msg = this.pylintMsg; - } - return { stdout: msg }; - } -} - -suite('Linting - Multiple Linters Enabled Test', () => { - let api: IExtensionTestApi; - let configService: IConfigurationService; - let linterManager: ILinterManager; - - suiteSetup(async () => { - api = await initialize(); - configService = api.serviceContainer.get(IConfigurationService); - linterManager = api.serviceContainer.get(ILinterManager); - }); - setup(async () => { - await initializeTest(); - await resetSettings(); - - // We only want to return some valid strings from linters, we don't care if they - // are being returned by actual linters (we aren't testing linters here, only how - // our code responds to those linters). - api.serviceManager.rebind(IPythonToolExecutionService, MockPythonToolExecService); - }); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await closeActiveWindows(); - await resetSettings(); - await deleteFile(path.join(workspaceUri.fsPath, '.pylintrc')); - await deleteFile(path.join(workspaceUri.fsPath, '.pydocstyle')); - - // Restore the execution service as it was... - api.serviceManager.rebind(IPythonToolExecutionService, PythonToolExecutionService); - }); - - async function resetSettings() { - // Don't run these updates in parallel, as they are updating the same file. - const target = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; - - await configService.updateSetting('linting.enabled', true, rootWorkspaceUri, target); - await configService.updateSetting('linting.lintOnSave', false, rootWorkspaceUri, target); - - linterManager.getAllLinterInfos().forEach(async (x) => { - await configService.updateSetting(makeSettingKey(x.product), false, rootWorkspaceUri, target); - }); - } - - function makeSettingKey(product: Product): PythonSettingKeys { - return `linting.${linterManager.getLinterInfo(product).enabledSettingName}` as PythonSettingKeys; - } - - test('Multiple linters', async () => { - await closeActiveWindows(); - const document = await workspace.openTextDocument(path.join(pythonFilesPath, 'print.py')); - await window.showTextDocument(document); - await configService.updateSetting( - 'languageServer', - LanguageServerType.Jedi, - undefined, - ConfigurationTarget.Workspace, - ); - await configService.updateSetting('linting.enabled', true, workspaceUri); - await configService.updateSetting('linting.pylintEnabled', true, workspaceUri); - await configService.updateSetting('linting.flake8Enabled', true, workspaceUri); - - const commands = api.serviceContainer.get(ICommandManager); - - const collection = (await commands.executeCommand('python.runLinting')) as DiagnosticCollection; - assert.notStrictEqual(collection, undefined, 'python.runLinting did not return valid diagnostics collection.'); - - const messages = collection!.get(document.uri); - assert.notStrictEqual(messages!.length, 0, 'No diagnostic messages.'); - assert.notStrictEqual(messages!.filter((x) => x.source === 'pylint').length, 0, 'No pylint messages.'); - assert.notStrictEqual(messages!.filter((x) => x.source === 'flake8').length, 0, 'No flake8 messages.'); - }); -}); diff --git a/extensions/positron-python/src/test/linters/linterCommands.unit.test.ts b/extensions/positron-python/src/test/linters/linterCommands.unit.test.ts deleted file mode 100644 index b3d5c4693832..000000000000 --- a/extensions/positron-python/src/test/linters/linterCommands.unit.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { anything, capture, deepEqual, instance, mock, verify, when } from 'ts-mockito'; -import { DiagnosticCollection } from 'vscode'; -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { CommandManager } from '../../client/common/application/commandManager'; -import { DocumentManager } from '../../client/common/application/documentManager'; -import { IApplicationShell, ICommandManager, IDocumentManager } from '../../client/common/application/types'; -import { Commands } from '../../client/common/constants'; -import { Product } from '../../client/common/types'; -import { ServiceContainer } from '../../client/ioc/container'; -import { LinterCommands } from '../../client/linters/linterCommands'; -import { LinterManager } from '../../client/linters/linterManager'; -import { LintingEngine } from '../../client/linters/lintingEngine'; -import { ILinterInfo, ILinterManager, ILintingEngine } from '../../client/linters/types'; - -suite('Linting - Linter Commands', () => { - let linterCommands: LinterCommands; - let manager: ILinterManager; - let shell: IApplicationShell; - let docManager: IDocumentManager; - let cmdManager: ICommandManager; - let lintingEngine: ILintingEngine; - setup(() => { - const svcContainer = mock(ServiceContainer); - manager = mock(LinterManager); - shell = mock(ApplicationShell); - docManager = mock(DocumentManager); - cmdManager = mock(CommandManager); - lintingEngine = mock(LintingEngine); - when(svcContainer.get(ILinterManager)).thenReturn(instance(manager)); - when(svcContainer.get(IApplicationShell)).thenReturn(instance(shell)); - when(svcContainer.get(IDocumentManager)).thenReturn(instance(docManager)); - when(svcContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - when(svcContainer.get(ILintingEngine)).thenReturn(instance(lintingEngine)); - linterCommands = new LinterCommands(instance(svcContainer)); - }); - - test('Commands are registered', () => { - verify(cmdManager.registerCommand(Commands.Set_Linter, anything())).once(); - verify(cmdManager.registerCommand(Commands.Enable_Linter, anything())).once(); - verify(cmdManager.registerCommand(Commands.Run_Linter, anything())).once(); - }); - - test('Run Linting method will lint all open files', async () => { - when(lintingEngine.lintOpenPythonFiles('manual')).thenResolve(('Hello' as unknown) as DiagnosticCollection); - - const result = await linterCommands.runLinting(); - - expect(result).to.be.equal('Hello'); - }); - - async function testEnableLintingWithCurrentState( - currentState: boolean, - selectedState: 'Enable' | 'Disable' | undefined, - ) { - when(manager.isLintingEnabled(anything())).thenResolve(currentState); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${currentState ? 'Enable' : 'Disable'}`, - }; - when(shell.showQuickPick(anything(), anything())).thenReturn(Promise.resolve(selectedState)); - - await linterCommands.enableLintingAsync(); - - verify(shell.showQuickPick(anything(), anything())).once(); - const options = capture(shell.showQuickPick).last()[0]; - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(options).to.deep.equal(['Enable', 'Disable']); - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - - if (selectedState) { - verify(manager.enableLintingAsync(selectedState === 'Enable', anything())).once(); - } else { - verify(manager.enableLintingAsync(anything(), anything())).never(); - } - } - test("Enable linting should check if linting is enabled, and display current state of 'Enable' and select nothing", async () => { - await testEnableLintingWithCurrentState(true, undefined); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Enable' and select 'Enable'", async () => { - await testEnableLintingWithCurrentState(true, 'Enable'); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Enable' and select 'Disable'", async () => { - await testEnableLintingWithCurrentState(true, 'Disable'); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Disable' and select 'Enable'", async () => { - await testEnableLintingWithCurrentState(true, 'Enable'); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Disable' and select 'Disable'", async () => { - await testEnableLintingWithCurrentState(true, 'Disable'); - }); - - test('Set Linter should display a quickpick', async () => { - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(anything())).thenResolve([]); - when(shell.showQuickPick(anything(), anything())).thenResolve(); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: 'current: none', - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - }); - - test('Set Linter should display a quickpick and currently active linter when only one is enabled', async () => { - const linterId = 'Hello World'; - const activeLinters: ILinterInfo[] = [({ id: linterId } as unknown) as ILinterInfo]; - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(anything())).thenResolve(activeLinters); - when(shell.showQuickPick(anything(), anything())).thenResolve(); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${linterId}`, - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())).once(); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - }); - - test('Set Linter should display a quickpick and with message about multiple linters being enabled', async () => { - const activeLinters: ILinterInfo[] = ([{ id: 'linterId' }, { id: 'linterId2' }] as unknown) as ILinterInfo[]; - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(anything())).thenResolve(activeLinters); - when(shell.showQuickPick(anything(), anything())).thenResolve(); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: 'current: multiple selected', - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - }); - - test('Selecting a linter should display warning message about multiple linters', async () => { - const linters: ILinterInfo[] = ([ - { id: '1' }, - { id: '2' }, - { id: '3', product: 'Three' }, - ] as unknown) as ILinterInfo[]; - const activeLinters: ILinterInfo[] = ([{ id: '1' }, { id: '3' }] as unknown) as ILinterInfo[]; - when(manager.getAllLinterInfos()).thenReturn(linters); - when(manager.getActiveLinters(anything())).thenResolve(activeLinters); - when(shell.showQuickPick(anything(), anything())).thenReturn(Promise.resolve('3')); - when(shell.showWarningMessage(anything(), 'Yes', 'No')).thenReturn(Promise.resolve('Yes')); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: 'current: multiple selected', - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())).once(); - verify(shell.showWarningMessage(anything(), 'Yes', 'No')).once(); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - verify(manager.setActiveLintersAsync(deepEqual([('Three' as unknown) as Product]), anything())).once(); - }); -}); diff --git a/extensions/positron-python/src/test/mocks/mockChildProcess.ts b/extensions/positron-python/src/test/mocks/mockChildProcess.ts index a46d66d79ca0..c0a24b1c955f 100644 --- a/extensions/positron-python/src/test/mocks/mockChildProcess.ts +++ b/extensions/positron-python/src/test/mocks/mockChildProcess.ts @@ -133,9 +133,9 @@ export class MockChildProcess extends EventEmitter { emit(event: string | symbol, ...args: unknown[]): boolean { if (this.eventMap.has(event.toString())) { - this.eventMap.get(event.toString()).forEach((listener: (arg0: unknown) => void) => { - const argsArray = Array.isArray(args) ? args : [args]; - listener(argsArray); + this.eventMap.get(event.toString()).forEach((listener: (...arg0: unknown[]) => void) => { + const argsArray: unknown[] = Array.isArray(args) ? args : [args]; + listener(...argsArray); }); } return true; diff --git a/extensions/positron-python/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts index ca0e24d5f3d3..1e9de68ad77a 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts @@ -156,6 +156,7 @@ suite('Conda and its environments are located correctly', () => { const isFile = typeof dir[name] === 'string'; return { name, + path: dir.name?.toString() ?? '', isFile: () => isFile, isDirectory: () => !isFile, isBlockDevice: () => false, diff --git a/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName b/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName index b723d0199f86..b5846df18ca8 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName +++ b/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile b/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile index b723d0199f86..b5846df18ca8 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile +++ b/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile b/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile index b723d0199f86..b5846df18ca8 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile +++ b/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile b/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile index b723d0199f86..b5846df18ca8 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile +++ b/extensions/positron-python/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/extensions/positron-python/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts new file mode 100644 index 000000000000..f751d270219e --- /dev/null +++ b/extensions/positron-python/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts @@ -0,0 +1,285 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as triggerUtils from '../../../client/pythonEnvironments/creation/common/createEnvTriggerUtils'; +import * as commonUtils from '../../../client/pythonEnvironments/creation/common/commonUtils'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheck, +} from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import * as commandApis from '../../../client/common/vscodeApis/commandApis'; +import { Commands } from '../../../client/common/constants'; +import { CreateEnv } from '../../../client/common/utils/localize'; + +suite('Create Environment Trigger', () => { + let shouldPromptToCreateEnvStub: sinon.SinonStub; + let hasVenvStub: sinon.SinonStub; + let hasPrefixCondaEnvStub: sinon.SinonStub; + let hasRequirementFilesStub: sinon.SinonStub; + let hasKnownFilesStub: sinon.SinonStub; + let isGlobalPythonSelectedStub: sinon.SinonStub; + let showInformationMessageStub: sinon.SinonStub; + let isCreateEnvWorkspaceCheckNotRunStub: sinon.SinonStub; + let getWorkspaceFolderStub: sinon.SinonStub; + let executeCommandStub: sinon.SinonStub; + let disableCreateEnvironmentTriggerStub: sinon.SinonStub; + let disableWorkspaceCreateEnvironmentTriggerStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + shouldPromptToCreateEnvStub = sinon.stub(triggerUtils, 'shouldPromptToCreateEnv'); + hasVenvStub = sinon.stub(commonUtils, 'hasVenv'); + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + hasRequirementFilesStub = sinon.stub(triggerUtils, 'hasRequirementFiles'); + hasKnownFilesStub = sinon.stub(triggerUtils, 'hasKnownFiles'); + isGlobalPythonSelectedStub = sinon.stub(triggerUtils, 'isGlobalPythonSelected'); + showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); + + isCreateEnvWorkspaceCheckNotRunStub = sinon.stub(triggerUtils, 'isCreateEnvWorkspaceCheckNotRun'); + isCreateEnvWorkspaceCheckNotRunStub.returns(true); + + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); + getWorkspaceFolderStub.returns(workspace1); + + executeCommandStub = sinon.stub(commandApis, 'executeCommand'); + disableCreateEnvironmentTriggerStub = sinon.stub(triggerUtils, 'disableCreateEnvironmentTrigger'); + disableWorkspaceCreateEnvironmentTriggerStub = sinon.stub( + triggerUtils, + 'disableWorkspaceCreateEnvironmentTrigger', + ); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No Uri', async () => { + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, undefined); + sinon.assert.notCalled(shouldPromptToCreateEnvStub); + }); + + test('Should not perform checks if user set trigger to "off"', async () => { + shouldPromptToCreateEnvStub.returns(false); + + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.notCalled(hasVenvStub); + sinon.assert.notCalled(hasPrefixCondaEnvStub); + sinon.assert.notCalled(hasRequirementFilesStub); + sinon.assert.notCalled(hasKnownFilesStub); + sinon.assert.notCalled(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not perform checks even if force is true, if user set trigger to "off"', async () => { + shouldPromptToCreateEnvStub.returns(false); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri, { + force: true, + }); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.notCalled(hasVenvStub); + sinon.assert.notCalled(hasPrefixCondaEnvStub); + sinon.assert.notCalled(hasRequirementFilesStub); + sinon.assert.notCalled(hasKnownFilesStub); + sinon.assert.notCalled(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there is a ".venv"', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(true); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there is a ".conda"', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(true); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there are no requirements', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(false); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there are known files', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(false); + hasKnownFilesStub.resolves(true); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if selected python is not global', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(false); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should show prompt if all conditions met: User closes prompt', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + showInformationMessageStub.resolves(undefined); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.notCalled(executeCommandStub); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + sinon.assert.notCalled(disableWorkspaceCreateEnvironmentTriggerStub); + }); + + test('Should show prompt if all conditions met: User clicks create', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + + showInformationMessageStub.resolves(CreateEnv.Trigger.createEnvironment); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.calledOnceWithExactly(executeCommandStub, Commands.Create_Environment); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + sinon.assert.notCalled(disableWorkspaceCreateEnvironmentTriggerStub); + }); + + test('Should show prompt if all conditions met: User clicks disable global', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + + showInformationMessageStub.resolves(CreateEnv.Trigger.disableCheck); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.notCalled(executeCommandStub); + sinon.assert.calledOnce(disableCreateEnvironmentTriggerStub); + sinon.assert.notCalled(disableWorkspaceCreateEnvironmentTriggerStub); + }); + + test('Should show prompt if all conditions met: User clicks disable workspace', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + + showInformationMessageStub.resolves(CreateEnv.Trigger.disableCheckWorkspace); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.notCalled(executeCommandStub); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + sinon.assert.calledOnce(disableWorkspaceCreateEnvironmentTriggerStub); + }); +}); diff --git a/extensions/positron-python/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts index 10fe06bba442..4addb5687085 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts @@ -62,7 +62,7 @@ function getPyProjectTomlFile(): typemoq.IMock { .setup((p) => p.getText(typemoq.It.isAny())) .returns( () => - '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[project]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.7"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[project]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.8"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', ); return someFile; } @@ -76,7 +76,7 @@ function getSomeTomlFile(): typemoq.IMock { .setup((p) => p.getText(typemoq.It.isAny())) .returns( () => - '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[something]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.7"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[something]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.8"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', ); return someFile; } diff --git a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts index e1ac1bafe6ac..e1344dc5f3ad 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts @@ -35,6 +35,8 @@ suite('Conda Creation provider tests', () => { let execObservableStub: sinon.SinonStub; let withProgressStub: sinon.SinonStub; let showErrorMessageWithLogsStub: sinon.SinonStub; + let pickExistingCondaActionStub: sinon.SinonStub; + let getPrefixCondaEnvPathStub: sinon.SinonStub; setup(() => { pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); @@ -46,6 +48,11 @@ suite('Conda Creation provider tests', () => { showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); showErrorMessageWithLogsStub.resolves(); + pickExistingCondaActionStub = sinon.stub(condaUtils, 'pickExistingCondaAction'); + pickExistingCondaActionStub.resolves(condaUtils.ExistingCondaAction.Create); + + getPrefixCondaEnvPathStub = sinon.stub(commonUtils, 'getPrefixCondaEnvPath'); + progressMock = typemoq.Mock.ofType(); condaProvider = condaCreationProvider(); }); @@ -77,6 +84,7 @@ suite('Conda Creation provider tests', () => { pickPythonVersionStub.resolves(undefined); await assert.isRejected(condaProvider.createEnvironment()); + assert.isTrue(pickExistingCondaActionStub.calledOnce); }); test('Create conda environment', async () => { @@ -136,6 +144,7 @@ suite('Conda Creation provider tests', () => { workspaceFolder: workspace1, }); assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(pickExistingCondaActionStub.calledOnce); }); test('Create conda environment failed', async () => { @@ -188,6 +197,7 @@ suite('Conda Creation provider tests', () => { const result = await promise; assert.ok(result?.error); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(pickExistingCondaActionStub.calledOnce); }); test('Create conda environment failed (non-zero exit code)', async () => { @@ -245,5 +255,26 @@ suite('Conda Creation provider tests', () => { const result = await promise; assert.ok(result?.error); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(pickExistingCondaActionStub.calledOnce); + }); + + test('Use existing conda environment', async () => { + getCondaBaseEnvStub.resolves('/usr/bin/conda'); + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + pickWorkspaceFolderStub.resolves(workspace1); + pickExistingCondaActionStub.resolves(condaUtils.ExistingCondaAction.UseExisting); + getPrefixCondaEnvPathStub.returns('existing_environment'); + + const result = await condaProvider.createEnvironment(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(pickPythonVersionStub.notCalled); + assert.isTrue(execObservableStub.notCalled); + assert.isTrue(withProgressStub.notCalled); + + assert.deepStrictEqual(result, { path: 'existing_environment', workspaceFolder: workspace1 }); }); }); diff --git a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts new file mode 100644 index 000000000000..b1acd0678714 --- /dev/null +++ b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { deleteCondaEnvironment } from '../../../../client/pythonEnvironments/creation/provider/condaDeleteUtils'; + +suite('Conda Delete test', () => { + let plainExecStub: sinon.SinonStub; + let getPrefixCondaEnvPathStub: sinon.SinonStub; + let hasPrefixCondaEnvStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + plainExecStub = sinon.stub(rawProcessApis, 'plainExec'); + getPrefixCondaEnvPathStub = sinon.stub(commonUtils, 'getPrefixCondaEnvPath'); + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Delete conda env ', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(false); + plainExecStub.resolves({ stdout: 'stdout' }); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isTrue(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.calledOnce); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete conda env with error', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(true); + plainExecStub.resolves({ stdout: 'stdout' }); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isFalse(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.calledOnce); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + }); + + test('Delete conda env with exception', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(false); + plainExecStub.rejects(new Error('error')); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isFalse(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.notCalled); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + }); +}); diff --git a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts index 3f115f9f58ed..a3f4a1abe905 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts @@ -3,9 +3,17 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; -import { CancellationTokenSource } from 'vscode'; +import * as path from 'path'; +import { CancellationTokenSource, Uri } from 'vscode'; import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; -import { pickPythonVersion } from '../../../../client/pythonEnvironments/creation/provider/condaUtils'; +import { + ExistingCondaAction, + pickExistingCondaAction, + pickPythonVersion, +} from '../../../../client/pythonEnvironments/creation/provider/condaUtils'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { CreateEnv } from '../../../../client/common/utils/localize'; suite('Conda Utils test', () => { let showQuickPickWithBackStub: sinon.SinonStub; @@ -43,3 +51,60 @@ suite('Conda Utils test', () => { assert.isUndefined(actual); }); }); + +suite('Existing .conda env test', () => { + let hasPrefixCondaEnvStub: sinon.SinonStub; + let showQuickPickWithBackStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No .conda found', async () => { + hasPrefixCondaEnvStub.resolves(false); + showQuickPickWithBackStub.resolves(undefined); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.Create); + assert.isTrue(showQuickPickWithBackStub.notCalled); + }); + + test('User presses escape', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves(undefined); + await assert.isRejected(pickExistingCondaAction(workspace1)); + }); + + test('.conda found and user selected to re-create', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Conda.recreate, + description: CreateEnv.Conda.recreateDescription, + }); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.Recreate); + }); + + test('.conda found and user selected to re-use', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Conda.useExisting, + description: CreateEnv.Conda.useExistingDescription, + }); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.UseExisting); + }); +}); diff --git a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts index de65887b7edc..72914b9118e2 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -15,7 +15,7 @@ import * as rawProcessApis from '../../../../client/common/process/rawProcessApi import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; import { createDeferred } from '../../../../client/common/utils/async'; -import { Output } from '../../../../client/common/process/types'; +import { Output, SpawnOptions } from '../../../../client/common/process/types'; import { VENV_CREATED_MARKER } from '../../../../client/pythonEnvironments/creation/provider/venvProgressAndTelemetry'; import { CreateEnv } from '../../../../client/common/utils/localize'; import * as venvUtils from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; @@ -394,4 +394,157 @@ suite('venv Creation provider tests', () => { assert.isTrue(showErrorMessageWithLogsStub.notCalled); assert.isTrue(deleteEnvironmentStub.notCalled); }); + + test('Create venv with 1000 requirement files', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + const requirements = Array.from({ length: 1000 }, (_, i) => ({ + installType: 'requirements', + installItem: `requirements${i}.txt`, + })); + pickPackagesToInstallStub.resolves(requirements); + const expected = JSON.stringify({ requirements: requirements.map((r) => r.installItem) }); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + let stdin: undefined | string; + let hasStdinArg = false; + execObservableStub.callsFake((_c, argv: string[], options) => { + stdin = options?.stdinStr; + hasStdinArg = argv.includes('--stdin'); + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + assert.strictEqual(stdin, expected); + assert.isTrue(hasStdinArg); + }); + + test('Create venv with 5 requirement files', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + const requirements = Array.from({ length: 5 }, (_, i) => ({ + installType: 'requirements', + installItem: `requirements${i}.txt`, + })); + pickPackagesToInstallStub.resolves(requirements); + const expectedRequirements = requirements.map((r) => r.installItem).sort(); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + let stdin: undefined | string; + let hasStdinArg = false; + let actualRequirements: string[] = []; + execObservableStub.callsFake((_c, argv: string[], options: SpawnOptions) => { + stdin = options?.stdinStr; + actualRequirements = argv.filter((arg) => arg.startsWith('requirements')).sort(); + hasStdinArg = argv.includes('--stdin'); + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + assert.isUndefined(stdin); + assert.deepStrictEqual(actualRequirements, expectedRequirements); + assert.isFalse(hasStdinArg); + }); }); diff --git a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts index ae4f43a0296c..1671026d5dd4 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts @@ -10,11 +10,13 @@ import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; import { ExistingVenvAction, + OPEN_REQUIREMENTS_BUTTON, pickExistingVenvAction, pickPackagesToInstall, } from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; import { CreateEnv } from '../../../../client/common/utils/localize'; +import { createDeferred } from '../../../../client/common/utils/async'; chaiUse(chaiAsPromised); @@ -23,6 +25,7 @@ suite('Venv Utils test', () => { let showQuickPickWithBackStub: sinon.SinonStub; let pathExistsStub: sinon.SinonStub; let readFileStub: sinon.SinonStub; + let showTextDocumentStub: sinon.SinonStub; const workspace1 = { uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), @@ -35,6 +38,7 @@ suite('Venv Utils test', () => { showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); pathExistsStub = sinon.stub(fs, 'pathExists'); readFileStub = sinon.stub(fs, 'readFile'); + showTextDocumentStub = sinon.stub(windowApis, 'showTextDocument'); }); teardown(() => { @@ -224,13 +228,18 @@ suite('Venv Utils test', () => { await assert.isRejected(pickPackagesToInstall(workspace1)); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], { placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, ignoreFocusOut: true, canPickMany: true, }, undefined, + sinon.match.func, ), ); assert.isTrue(readFileStub.calledOnce); @@ -257,13 +266,18 @@ suite('Venv Utils test', () => { const actual = await pickPackagesToInstall(workspace1); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], { placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, ignoreFocusOut: true, canPickMany: true, }, undefined, + sinon.match.func, ), ); assert.deepStrictEqual(actual, []); @@ -290,13 +304,18 @@ suite('Venv Utils test', () => { const actual = await pickPackagesToInstall(workspace1); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], { placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, ignoreFocusOut: true, canPickMany: true, }, undefined, + sinon.match.func, ), ); assert.deepStrictEqual(actual, [ @@ -328,13 +347,18 @@ suite('Venv Utils test', () => { const actual = await pickPackagesToInstall(workspace1); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], { placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, ignoreFocusOut: true, canPickMany: true, }, undefined, + sinon.match.func, ), ); assert.deepStrictEqual(actual, [ @@ -349,6 +373,45 @@ suite('Venv Utils test', () => { ]); assert.isTrue(readFileStub.notCalled); }); + + test('User clicks button to open requirements.txt', async () => { + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + pathExistsStub.resolves(false); + + const deferred = createDeferred(); + showQuickPickWithBackStub.callsFake(async (_items, _options, _token, callback) => { + callback({ + button: OPEN_REQUIREMENTS_BUTTON, + item: { label: 'requirements.txt' }, + }); + await deferred.promise; + return [{ label: 'requirements.txt' }]; + }); + + let uri: Uri | undefined; + showTextDocumentStub.callsFake((arg: Uri) => { + uri = arg; + deferred.resolve(); + return Promise.resolve(); + }); + + await pickPackagesToInstall(workspace1); + assert.deepStrictEqual( + uri?.toString(), + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')).toString(), + ); + }); }); suite('Test pick existing venv action', () => { diff --git a/extensions/positron-python/src/test/standardTest.ts b/extensions/positron-python/src/test/standardTest.ts index 0562d1adf431..0fe53437cf3d 100644 --- a/extensions/positron-python/src/test/standardTest.ts +++ b/extensions/positron-python/src/test/standardTest.ts @@ -6,6 +6,7 @@ import { downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath, runTest import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../client/common/constants'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; import { getChannel } from './utils/vscode'; +import { TestOptions } from '@vscode/test-electron/out/runTest'; // If running smoke tests, we don't have access to this. if (process.env.TEST_FILES_SUFFIX !== 'smoke.test') { @@ -85,18 +86,20 @@ async function start() { : ['--disable-extensions']; await installJupyterExtension(vscodeExecutablePath); await installPylanceExtension(vscodeExecutablePath); + console.log('VS Code executable', vscodeExecutablePath); const launchArgs = baseLaunchArgs .concat([workspacePath]) .concat(channel === 'insiders' ? ['--enable-proposed-api'] : []) .concat(['--timeout', '5000']); console.log(`Starting vscode ${channel} with args ${launchArgs.join(' ')}`); - await runTests({ + const options: TestOptions = { extensionDevelopmentPath: extensionDevelopmentPath, extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test'), launchArgs, version: channel, extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, - }); + }; + await runTests(options); } start().catch((ex) => { console.error('End Standard tests (with errors)', ex); diff --git a/extensions/positron-python/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts b/extensions/positron-python/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts index 30f95c94d217..29c310f6c724 100644 --- a/extensions/positron-python/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts +++ b/extensions/positron-python/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; import { Disposable, TextDocument, TextEditor, Uri } from 'vscode'; import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; @@ -13,6 +14,7 @@ import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } fr import { IConfigurationService } from '../../../client/common/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as triggerApis from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; suite('Terminal - Code Execution Manager', () => { let executionManager: ICodeExecutionManager; @@ -24,6 +26,7 @@ suite('Terminal - Code Execution Manager', () => { let configService: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; let interpreterService: TypeMoq.IMock; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; setup(() => { fileSystem = TypeMoq.Mock.ofType(); fileSystem.setup((f) => f.readFile(TypeMoq.It.isAny())).returns(() => Promise.resolve('')); @@ -52,8 +55,14 @@ suite('Terminal - Code Execution Manager', () => { configService.object, serviceContainer.object, ); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); }); teardown(() => { + sinon.restore(); disposables.forEach((disposable) => { if (disposable) { disposable.dispose(); diff --git a/extensions/positron-python/src/test/testing/common/testingAdapter.test.ts b/extensions/positron-python/src/test/testing/common/testingAdapter.test.ts index 1334085e4cea..4f46f1cf738c 100644 --- a/extensions/positron-python/src/test/testing/common/testingAdapter.test.ts +++ b/extensions/positron-python/src/test/testing/common/testingAdapter.test.ts @@ -1,32 +1,38 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { TestRun, Uri } from 'vscode'; +import { TestController, TestRun, Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as path from 'path'; import * as assert from 'assert'; import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; -import { ITestResultResolver, ITestServer } from '../../../client/testing/testController/common/types'; +import { ITestController, ITestResultResolver } from '../../../client/testing/testController/common/types'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; import { IPythonExecutionFactory } from '../../../client/common/process/types'; import { ITestDebugLauncher } from '../../../client/testing/common/types'; import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize'; -import { traceError, traceLog } from '../../../client/logging'; +import { traceLog } from '../../../client/logging'; import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; +import { PythonResultResolver } from '../../../client/testing/testController/common/resultResolver'; +import { TestProvider } from '../../../client/testing/types'; +import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../../client/testing/common/constants'; suite('End to End Tests: test adapters', () => { - let resultResolver: typeMoq.IMock; - let pythonTestServer: ITestServer; + let resultResolver: ITestResultResolver; + let pythonTestServer: PythonTestServer; let pythonExecFactory: IPythonExecutionFactory; let debugLauncher: ITestDebugLauncher; let configService: IConfigurationService; - let testOutputChannel: ITestOutputChannel; let serviceContainer: IServiceContainer; let workspaceUri: Uri; + let testOutputChannel: typeMoq.IMock; + let testController: TestController; + const unittestProvider: TestProvider = UNITTEST_PROVIDER; + const pytestProvider: TestProvider = PYTEST_PROVIDER; const rootPathSmallWorkspace = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, 'src', @@ -39,6 +45,18 @@ suite('End to End Tests: test adapters', () => { 'testTestingRootWkspc', 'largeWorkspace', ); + const rootPathErrorWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'errorWorkspace', + ); + const rootPathDiscoveryErrorWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'discoveryErrorWorkspace', + ); suiteSetup(async () => { serviceContainer = (await initialize()).serviceContainer; }); @@ -48,72 +66,97 @@ suite('End to End Tests: test adapters', () => { configService = serviceContainer.get(IConfigurationService); pythonExecFactory = serviceContainer.get(IPythonExecutionFactory); debugLauncher = serviceContainer.get(ITestDebugLauncher); - testOutputChannel = serviceContainer.get(ITestOutputChannel); - - // create mock resultResolver object - resultResolver = typeMoq.Mock.ofType(); + testController = serviceContainer.get(ITestController); // create objects that were not injected pythonTestServer = new PythonTestServer(pythonExecFactory, debugLauncher); await pythonTestServer.serverReady(); + + testOutputChannel = typeMoq.Mock.ofType(); + testOutputChannel + .setup((x) => x.append(typeMoq.It.isAny())) + .callback((appendVal: any) => { + traceLog('output channel - ', appendVal.toString()); + }) + .returns(() => { + // Whatever you need to return + }); + testOutputChannel + .setup((x) => x.appendLine(typeMoq.It.isAny())) + .callback((appendVal: any) => { + traceLog('output channel ', appendVal.toString()); + }) + .returns(() => { + // Whatever you need to return + }); + }); + teardown(async () => { + pythonTestServer.dispose(); }); test('unittest discovery adapter small workspace', async () => { // result resolver and saved data for assertions let actualData: { - status: unknown; - error: string | any[]; - tests: unknown; + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + workspaceUri = Uri.parse(rootPathSmallWorkspace); + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + resultResolver._resolveDiscovery = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveDiscovery ${data}`); - actualData = data; - return Promise.resolve(); - }); // set workspace to test workspace folder and set up settings - workspaceUri = Uri.parse(rootPathSmallWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; // run unittest discovery const discoveryAdapter = new UnittestTestDiscoveryAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); await discoveryAdapter.discoverTests(workspaceUri).finally(() => { // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); // 3. Confirm tests are found assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); }); }); test('unittest discovery adapter large workspace', async () => { // result resolver and saved data for assertions let actualData: { - status: unknown; - error: string | any[]; - tests: unknown; + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + resultResolver._resolveDiscovery = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveDiscovery ${data}`); - actualData = data; - return Promise.resolve(); - }); // set settings to work for the given workspace workspaceUri = Uri.parse(rootPathLargeWorkspace); @@ -122,85 +165,89 @@ suite('End to End Tests: test adapters', () => { const discoveryAdapter = new UnittestTestDiscoveryAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); await discoveryAdapter.discoverTests(workspaceUri).finally(() => { - // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); // 3. Confirm tests are found assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); }); }); test('pytest discovery adapter small workspace', async () => { // result resolver and saved data for assertions let actualData: { - status: unknown; - error: string | any[]; - tests: unknown; + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver._resolveDiscovery = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveDiscovery ${data}`); - actualData = data; - return Promise.resolve(); - }); // run pytest discovery const discoveryAdapter = new PytestTestDiscoveryAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathSmallWorkspace); - await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(actualData.error.length, 0, "Expected no errors in 'error' field"); + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); // 3. Confirm tests are found assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); }); }); test('pytest discovery adapter large workspace', async () => { // result resolver and saved data for assertions let actualData: { - status: unknown; - error: string | any[]; - tests: unknown; + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver._resolveDiscovery = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveDiscovery ${data}`); - actualData = data; - return Promise.resolve(); - }); // run pytest discovery const discoveryAdapter = new PytestTestDiscoveryAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); // set workspace to test workspace folder @@ -208,33 +255,42 @@ suite('End to End Tests: test adapters', () => { await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(actualData.error.length, 0, "Expected no errors in 'error' field"); + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); // 3. Confirm tests are found assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); }); }); test('unittest execution adapter small workspace', async () => { // result resolver and saved data for assertions - let actualData: { - status: unknown; - error: string | any[]; - result: unknown; + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveExecution ${data}`); - actualData = data; - return Promise.resolve(); - }); // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathSmallWorkspace); @@ -243,8 +299,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new UnittestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -258,35 +314,34 @@ suite('End to End Tests: test adapters', () => { await executionAdapter .runTests(workspaceUri, ['test_simple.SimpleClass.test_simple_unit'], false, testRun.object) .finally(() => { - // verification after execution is complete - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); - - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm tests are found - assert.ok(actualData.result, 'Expected results to be present'); + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); test('unittest execution adapter large workspace', async () => { // result resolver and saved data for assertions - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceError(`resolveExecution ${data}`); - traceLog(`resolveExecution ${data}`); - // do the following asserts for each time resolveExecution is called, should be called once per test. - // 1. Check the status, can be subtest success or failure - assert( - data.status === 'subtest-success' || data.status === 'subtest-failure', - "Expected status to be 'subtest-success' or 'subtest-failure'", + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + const validStatuses = ['subtest-success', 'subtest-failure']; + assert.ok( + validStatuses.includes(payload.status), + `Expected status to be one of ${validStatuses.join(', ')}, but instead status is ${payload.status}`, ); - // 2. Confirm tests are found - assert.ok(data.result, 'Expected results to be present'); - return Promise.resolve(); - }); + assert.ok(payload.result, 'Expected results to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathLargeWorkspace); @@ -296,8 +351,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new UnittestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -310,29 +365,35 @@ suite('End to End Tests: test adapters', () => { ); await executionAdapter .runTests(workspaceUri, ['test_parameterized_subtest.NumbersTest.test_even'], false, testRun.object) - .finally(() => { - // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.atLeastOnce(), - ); + .then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 2000, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); test('pytest execution adapter small workspace', async () => { // result resolver and saved data for assertions - let actualData: { - status: unknown; - error: string | any[]; - result: unknown; + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveExecution ${data}`); - actualData = data; - return Promise.resolve(); - }); - // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathSmallWorkspace); @@ -340,8 +401,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new PytestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -360,42 +421,42 @@ suite('End to End Tests: test adapters', () => { testRun.object, pythonExecFactory, ) - .finally(() => { - // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); - - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(actualData.error, null, "Expected no errors in 'error' field"); - // 3. Confirm tests are found - assert.ok(actualData.result, 'Expected results to be present'); + .then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); test('pytest execution adapter large workspace', async () => { - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveExecution ${data}`); - // do the following asserts for each time resolveExecution is called, should be called once per test. - // 1. Check the status is "success" - assert.strictEqual(data.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(data.error, null, "Expected no errors in 'error' field"); - // 3. Confirm tests are found - assert.ok(data.result, 'Expected results to be present'); - return Promise.resolve(); - }); + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathLargeWorkspace); // generate list of test_ids const testIds: string[] = []; - for (let i = 0; i < 200; i = i + 1) { + for (let i = 0; i < 2000; i = i + 1) { const testId = `${rootPathLargeWorkspace}/test_parameterized_subtest.py::test_odd_even[${i}]`; testIds.push(testId); } @@ -404,8 +465,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new PytestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -416,12 +477,243 @@ suite('End to End Tests: test adapters', () => { onCancellationRequested: () => undefined, } as any), ); - await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object, pythonExecFactory).finally(() => { - // resolve execution should be called 200 times since there are 200 tests run. - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.exactly(200), + await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object, pythonExecFactory).then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 2000, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); + test('unittest discovery adapter seg fault error handling', async () => { + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveDiscovery = async (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + traceLog(`unittest discovery adapter seg fault error handling \n ${JSON.stringify(data)}`); + try { + if (data.status === 'error') { + if (data.error === undefined) { + // Dereference a NULL pointer + const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); + assert.notDeepEqual(indexOfTest, -1, 'Expected test to have a null pointer'); + } else { + assert.ok(data.error, "Expected errors in 'error' field"); + } + } else { + const indexOfTest = JSON.stringify(data.tests).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathDiscoveryErrorWorkspace); + + const discoveryAdapter = new UnittestTestDiscoveryAdapter( + pythonTestServer, + configService, + testOutputChannel.object, + resultResolver, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await discoveryAdapter.discoverTests(workspaceUri).finally(() => { + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); + test('pytest discovery seg fault error handling', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveDiscovery = async (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + traceLog(`add one to call count, is now ${callCount}`); + traceLog(`pytest discovery adapter seg fault error handling \n ${JSON.stringify(data)}`); + try { + if (data.status === 'error') { + if (data.error === undefined) { + // Dereference a NULL pointer + const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); + assert.notDeepEqual(indexOfTest, -1, 'Expected test to have a null pointer'); + } else { + assert.ok(data.error, "Expected errors in 'error' field"); + } + } else { + const indexOfTest = JSON.stringify(data.tests).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter( + pythonTestServer, + configService, + testOutputChannel.object, + resultResolver, + ); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathDiscoveryErrorWorkspace); + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + assert.ok( + callCount >= 1, + `Expected _resolveDiscovery to be called at least once, call count was instead ${callCount}`, ); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); + test('unittest execution adapter seg fault error handling', async () => { + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + traceLog(`unittest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); + try { + if (data.status === 'error') { + if (data.error === undefined) { + // Dereference a NULL pointer + const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); + assert.notDeepEqual(indexOfTest, -1, 'Expected test to have a null pointer'); + } else { + assert.ok(data.error, "Expected errors in 'error' field"); + } + } else { + const indexOfTest = JSON.stringify(data.result).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } + assert.ok(data.result, 'Expected results to be present'); + // make sure the testID is found in the results + const indexOfTest = JSON.stringify(data).search('test_seg_fault.TestSegmentationFault.test_segfault'); + assert.notDeepEqual(indexOfTest, -1, 'Expected testId to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; + + const testId = `test_seg_fault.TestSegmentationFault.test_segfault`; + const testIds: string[] = [testId]; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathErrorWorkspace); + + // run pytest execution + const executionAdapter = new UnittestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel.object, + resultResolver, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object).finally(() => { + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); + test('pytest execution adapter seg fault error handling', async () => { + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + console.log(`pytest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); + callCount = callCount + 1; + try { + if (data.status === 'error') { + assert.ok(data.error, "Expected errors in 'error' field"); + } else { + const indexOfTest = JSON.stringify(data.result).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } + assert.ok(data.result, 'Expected results to be present'); + // make sure the testID is found in the results + const indexOfTest = JSON.stringify(data).search( + 'test_seg_fault.py::TestSegmentationFault::test_segfault', + ); + assert.notDeepEqual(indexOfTest, -1, 'Expected testId to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; + + const testId = `${rootPathErrorWorkspace}/test_seg_fault.py::TestSegmentationFault::test_segfault`; + const testIds: string[] = [testId]; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathErrorWorkspace); + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel.object, + resultResolver, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object, pythonExecFactory).finally(() => { + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); }); diff --git a/extensions/positron-python/src/test/testing/common/testingPayloadsEot.test.ts b/extensions/positron-python/src/test/testing/common/testingPayloadsEot.test.ts new file mode 100644 index 000000000000..a30b1efe288c --- /dev/null +++ b/extensions/positron-python/src/test/testing/common/testingPayloadsEot.test.ts @@ -0,0 +1,214 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { TestController, TestRun, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as path from 'path'; +import * as assert from 'assert'; +import * as net from 'net'; +import { Observable } from 'rxjs'; +import * as crypto from 'crypto'; +// import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; +import * as sinon from 'sinon'; +import { ITestController, ITestResultResolver } from '../../../client/testing/testController/common/types'; +import { PythonTestServer } from '../../../client/testing/testController/common/server'; +import { IPythonExecutionFactory, IPythonExecutionService, Output } from '../../../client/common/process/types'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { initialize } from '../../initialize'; +import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { PythonResultResolver } from '../../../client/testing/testController/common/resultResolver'; +import { PYTEST_PROVIDER } from '../../../client/testing/common/constants'; +import { MockChildProcess } from '../../mocks/mockChildProcess'; +import { + PAYLOAD_SINGLE_CHUNK, + PAYLOAD_MULTI_CHUNK, + PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY, + DataWithPayloadChunks, + PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY, + PAYLOAD_ONLY_HEADER_MULTI_CHUNK, +} from '../testController/payloadTestCases'; +import { traceLog } from '../../../client/logging'; + +const FAKE_UUID = 'fake-u-u-i-d'; +export interface TestCase { + name: string; + value: DataWithPayloadChunks; +} + +const testCases: Array = [ + { + name: 'header in single chunk edge case', + value: PAYLOAD_ONLY_HEADER_MULTI_CHUNK(FAKE_UUID), + }, + { + name: 'single payload single chunk', + value: PAYLOAD_SINGLE_CHUNK(FAKE_UUID), + }, + { + name: 'multiple payloads per buffer chunk', + value: PAYLOAD_MULTI_CHUNK(FAKE_UUID), + }, + { + name: 'single payload across multiple buffer chunks', + value: PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY(FAKE_UUID), + }, + { + name: 'two chunks, payload split and two payloads in a chunk', + value: PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY(FAKE_UUID), + }, +]; + +suite('EOT tests', () => { + let resultResolver: ITestResultResolver; + let pythonTestServer: PythonTestServer; + let debugLauncher: ITestDebugLauncher; + let configService: IConfigurationService; + let serviceContainer: IServiceContainer; + let workspaceUri: Uri; + let testOutputChannel: typeMoq.IMock; + let testController: TestController; + let stubExecutionFactory: typeMoq.IMock; + let client: net.Socket; + let mockProc: MockChildProcess; + const sandbox = sinon.createSandbox(); + // const unittestProvider: TestProvider = UNITTEST_PROVIDER; + // const pytestProvider: TestProvider = PYTEST_PROVIDER; + const rootPathSmallWorkspace = path.join('src'); + suiteSetup(async () => { + serviceContainer = (await initialize()).serviceContainer; + }); + + setup(async () => { + // create objects that were injected + configService = serviceContainer.get(IConfigurationService); + debugLauncher = serviceContainer.get(ITestDebugLauncher); + testController = serviceContainer.get(ITestController); + + // create client to act as python server which sends testing result response + client = new net.Socket(); + client.on('error', (error) => { + traceLog('Socket connection error:', error); + }); + + mockProc = new MockChildProcess('', ['']); + const output2 = new Observable>(() => { + /* no op */ + }); + + // stub out execution service and factory so mock data is returned from client. + const stubExecutionService = ({ + execObservable: () => { + client.connect(pythonTestServer.getPort()); + return { + proc: mockProc, + out: output2, + dispose: () => { + /* no-body */ + }, + }; + }, + } as unknown) as IPythonExecutionService; + + stubExecutionFactory = typeMoq.Mock.ofType(); + stubExecutionFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(stubExecutionService)); + + // stub create UUID + + const v4Stub = sandbox.stub(crypto, 'randomUUID'); + v4Stub.returns(FAKE_UUID); + + // create python test server + pythonTestServer = new PythonTestServer(stubExecutionFactory.object, debugLauncher); + await pythonTestServer.serverReady(); + // handles output from client + testOutputChannel = typeMoq.Mock.ofType(); + testOutputChannel + .setup((x) => x.append(typeMoq.It.isAny())) + .callback((appendVal: any) => { + traceLog('out - ', appendVal.toString()); + }) + .returns(() => { + // Whatever you need to return + }); + testOutputChannel + .setup((x) => x.appendLine(typeMoq.It.isAny())) + .callback((appendVal: any) => { + traceLog('outL - ', appendVal.toString()); + }) + .returns(() => { + // Whatever you need to return + }); + }); + teardown(async () => { + pythonTestServer.dispose(); + sandbox.restore(); + }); + testCases.forEach((testCase) => { + test(`Testing Payloads: ${testCase.name}`, async () => { + let actualCollectedResult = ''; + client.on('connect', async () => { + traceLog('socket connected, sending stubbed data'); + // payload is a string array, each string represents one line written to the buffer + const { payloadArray } = testCase.value; + for (let i = 0; i < payloadArray.length; i = i + 1) { + await (async (clientSub, payloadSub) => { + if (!clientSub.write(payloadSub)) { + // If write returns false, wait for the 'drain' event before proceeding + await new Promise((resolve) => clientSub.once('drain', resolve)); + } + })(client, payloadArray[i]); + } + mockProc.emit('close', 0, null); + client.end(); + }); + + resultResolver = new PythonResultResolver(testController, PYTEST_PROVIDER, workspaceUri); + resultResolver._resolveExecution = async (payload, _token?) => { + // the payloads that get to the _resolveExecution are all data and should be successful. + actualCollectedResult = actualCollectedResult + JSON.stringify(payload.result); + assert.strictEqual(payload.status, 'success', "Expected status to be 'success'"); + assert.ok(payload.result, 'Expected results to be present'); + + return Promise.resolve(); + }; + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel.object, + resultResolver, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter + .runTests( + workspaceUri, + [`${rootPathSmallWorkspace}/test_simple.py::test_a`], + false, + testRun.object, + stubExecutionFactory.object, + ) + .then(() => { + assert.strictEqual( + testCase.value.data, + actualCollectedResult, + "Expected collected result to match 'data'", + ); + }); + }); + }); +}); diff --git a/extensions/positron-python/src/test/testing/testController/payloadTestCases.ts b/extensions/positron-python/src/test/testing/testController/payloadTestCases.ts new file mode 100644 index 000000000000..f7f94a926f5f --- /dev/null +++ b/extensions/positron-python/src/test/testing/testController/payloadTestCases.ts @@ -0,0 +1,173 @@ +export interface DataWithPayloadChunks { + payloadArray: string[]; + data: string; +} + +const EOT_PAYLOAD = `Content-Length: 42 +Content-Type: application/json +Request-uuid: fake-u-u-i-d + +{"command_type": "execution", "eot": true}`; + +const SINGLE_UNITTEST_SUBTEST = { + cwd: '/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace', + status: 'success', + result: { + 'test_parameterized_subtest.NumbersTest.test_even (i=0)': { + test: 'test_parameterized_subtest.NumbersTest.test_even', + outcome: 'success', + message: 'None', + traceback: null, + subtest: 'test_parameterized_subtest.NumbersTest.test_even (i=0)', + }, + }, +}; + +const SINGLE_PYTEST_PAYLOAD = { + cwd: 'path/to', + status: 'success', + result: { + 'path/to/file.py::test_funct': { + test: 'path/to/file.py::test_funct', + outcome: 'success', + message: 'None', + traceback: null, + subtest: 'path/to/file.py::test_funct', + }, + }, +}; + +const SINGLE_PYTEST_PAYLOAD_TWO = { + cwd: 'path/to/second', + status: 'success', + result: { + 'path/to/workspace/parametrize_tests.py::test_adding[3+5-8]': { + test: 'path/to/workspace/parametrize_tests.py::test_adding[3+5-8]', + outcome: 'success', + message: 'None', + traceback: null, + }, + }, +}; + +function splitIntoRandomSubstrings(payload: string): string[] { + // split payload at random + const splitPayload = []; + const n = payload.length; + let remaining = n; + while (remaining > 0) { + // Randomly split what remains of the string + const randomSize = Math.floor(Math.random() * remaining) + 1; + splitPayload.push(payload.slice(n - remaining, n - remaining + randomSize)); + + remaining -= randomSize; + } + return splitPayload; +} + +export function createPayload(uuid: string, data: unknown): string { + return `Content-Length: ${JSON.stringify(data).length} +Content-Type: application/json +Request-uuid: ${uuid} + +${JSON.stringify(data)}`; +} + +export function PAYLOAD_SINGLE_CHUNK(uuid: string): DataWithPayloadChunks { + const payload = createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + + return { + payloadArray: [payload, EOT_PAYLOAD], + data: JSON.stringify(SINGLE_UNITTEST_SUBTEST.result), + }; +} + +// more than one payload (item with header) per chunk sent +// payload has 3 SINGLE_UNITTEST_SUBTEST +export function PAYLOAD_MULTI_CHUNK(uuid: string): DataWithPayloadChunks { + let payload = ''; + let result = ''; + for (let i = 0; i < 3; i = i + 1) { + payload += createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + result += JSON.stringify(SINGLE_UNITTEST_SUBTEST.result); + } + return { + payloadArray: [payload, EOT_PAYLOAD], + data: result, + }; +} + +// more than one payload, split so the first one is only 'Content-Length' to confirm headers +// with null values are ignored +export function PAYLOAD_ONLY_HEADER_MULTI_CHUNK(uuid: string): DataWithPayloadChunks { + const payloadArray: string[] = []; + const result = JSON.stringify(SINGLE_UNITTEST_SUBTEST.result); + + const val = createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + const firstSpaceIndex = val.indexOf(' '); + const payload1 = val.substring(0, firstSpaceIndex); + const payload2 = val.substring(firstSpaceIndex); + payloadArray.push(payload1); + payloadArray.push(payload2); + payloadArray.push(EOT_PAYLOAD); + return { + payloadArray, + data: result, + }; +} + +// single payload divided by an arbitrary character and split across payloads +export function PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY(uuid: string): DataWithPayloadChunks { + const payload = createPayload(uuid, SINGLE_PYTEST_PAYLOAD); + const splitPayload = splitIntoRandomSubstrings(payload); + const finalResult = JSON.stringify(SINGLE_PYTEST_PAYLOAD.result); + splitPayload.push(EOT_PAYLOAD); + return { + payloadArray: splitPayload, + data: finalResult, + }; +} + +// here a payload is split across the buffer chunks and there are multiple payloads in a single buffer chunk +export function PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY(uuid: string): DataWithPayloadChunks { + const payload = createPayload(uuid, SINGLE_PYTEST_PAYLOAD).concat(createPayload(uuid, SINGLE_PYTEST_PAYLOAD_TWO)); + const splitPayload = splitIntoRandomSubstrings(payload); + const finalResult = JSON.stringify(SINGLE_PYTEST_PAYLOAD.result).concat( + JSON.stringify(SINGLE_PYTEST_PAYLOAD_TWO.result), + ); + + splitPayload.push(EOT_PAYLOAD); + return { + payloadArray: splitPayload, + data: finalResult, + }; +} + +export function PAYLOAD_SPLIT_MULTI_CHUNK_RAN_ORDER_ARRAY(uuid: string): Array { + return [ + `Content-Length: 411 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-success", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=0)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-success", "message": "None", "traceback": null, "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=0)"}}} + +Content-Length: 411 +Content-Type: application/json +Request-uuid: 9${uuid} + +{"cwd": "/home/runner/work/vscode-`, + `python/vscode-python/path with`, + ` spaces/src" + +Content-Length: 959 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-failure", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=1)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-failure", "message": "(, AssertionError('1 != 0'), )", "traceback": " File \"/opt/hostedtoolcache/Python/3.11.4/x64/lib/python3.11/unittest/case.py\", line 57, in testPartExecutor\n yield\n File \"/opt/hostedtoolcache/Python/3.11.4/x64/lib/python3.11/unittest/case.py\", line 538, in subTest\n yield\n File \"/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py\", line 16, in test_even\n self.assertEqual(i % 2, 0)\nAssertionError: 1 != 0\n", "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=1)"}}} +Content-Length: 411 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-success", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=2)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-success", "message": "None", "traceback": null, "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=2)"}}}`, + ]; +} diff --git a/extensions/positron-python/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/extensions/positron-python/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 43b763f56e6c..a2e5c810dc86 100644 --- a/extensions/positron-python/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/extensions/positron-python/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -21,6 +21,7 @@ import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/co import * as util from '../../../../client/testing/testController/common/utils'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import { traceInfo } from '../../../../client/logging'; suite('pytest test execution adapter', () => { let testServer: typeMoq.IMock; @@ -33,7 +34,7 @@ suite('pytest test execution adapter', () => { (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; let myTestPath: string; let mockProc: MockChildProcess; - let utilsStub: sinon.SinonStub; + let utilsStartServerStub: sinon.SinonStub; setup(() => { testServer = typeMoq.Mock.ofType(); testServer.setup((t) => t.getPort()).returns(() => 12345); @@ -51,6 +52,8 @@ suite('pytest test execution adapter', () => { isTestExecution: () => false, } as unknown) as IConfigurationService; + // mock out the result resolver + // set up exec service with child process mockProc = new MockChildProcess('', ['']); const output = new Observable>(() => { @@ -67,7 +70,7 @@ suite('pytest test execution adapter', () => { }, })); execFactory = typeMoq.Mock.ofType(); - utilsStub = sinon.stub(util, 'startTestIdServer'); + utilsStartServerStub = sinon.stub(util, 'startTestIdServer'); debugLauncher = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) @@ -79,13 +82,6 @@ suite('pytest test execution adapter', () => { deferred.resolve(); return Promise.resolve({ stdout: '{}' }); }); - debugLauncher - .setup((d) => d.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => { - deferred.resolve(); - return Promise.resolve(); - }); - execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); @@ -104,7 +100,7 @@ suite('pytest test execution adapter', () => { deferred2.resolve(); return Promise.resolve(execService.object); }); - utilsStub.callsFake(() => { + utilsStartServerStub.callsFake(() => { deferred3.resolve(); return Promise.resolve(54321); }); @@ -131,7 +127,7 @@ suite('pytest test execution adapter', () => { mockProc.trigger('close'); // assert - sinon.assert.calledWithExactly(utilsStub, testIds); + sinon.assert.calledWithExactly(utilsStartServerStub, testIds); }); test('pytest execution called with correct args', async () => { const deferred2 = createDeferred(); @@ -143,7 +139,7 @@ suite('pytest test execution adapter', () => { deferred2.resolve(); return Promise.resolve(execService.object); }); - utilsStub.callsFake(() => { + utilsStartServerStub.callsFake(() => { deferred3.resolve(); return Promise.resolve(54321); }); @@ -175,7 +171,6 @@ suite('pytest test execution adapter', () => { TEST_UUID: 'uuid123', TEST_PORT: '12345', }; - // execService.verify((x) => x.exec(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); execService.verify( (x) => x.execObservable( @@ -203,7 +198,7 @@ suite('pytest test execution adapter', () => { deferred2.resolve(); return Promise.resolve(execService.object); }); - utilsStub.callsFake(() => { + utilsStartServerStub.callsFake(() => { deferred3.resolve(); return Promise.resolve(54321); }); @@ -262,12 +257,28 @@ suite('pytest test execution adapter', () => { }); test('Debug launched correctly for pytest', async () => { const deferred3 = createDeferred(); - utilsStub.callsFake(() => { + const deferredEOT = createDeferred(); + utilsStartServerStub.callsFake(() => { deferred3.resolve(); return Promise.resolve(54321); }); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async () => { + traceInfo('stubs launch debugger'); + deferredEOT.resolve(); + }); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => deferredEOT); const testRun = typeMoq.Mock.ofType(); - testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer @@ -298,5 +309,6 @@ suite('pytest test execution adapter', () => { ), typeMoq.Times.once(), ); + testServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); }); }); diff --git a/extensions/positron-python/src/test/testing/testController/resultResolver.unit.test.ts b/extensions/positron-python/src/test/testing/testController/resultResolver.unit.test.ts index 09a68128167d..2078c72e8cf6 100644 --- a/extensions/positron-python/src/test/testing/testController/resultResolver.unit.test.ts +++ b/extensions/positron-python/src/test/testing/testController/resultResolver.unit.test.ts @@ -14,6 +14,8 @@ import { import * as testItemUtilities from '../../../client/testing/testController/common/testItemUtilities'; import * as ResultResolver from '../../../client/testing/testController/common/resultResolver'; import * as util from '../../../client/testing/testController/common/utils'; +import { Deferred, createDeferred } from '../../../client/common/utils/async'; +import { traceLog } from '../../../client/logging'; suite('Result Resolver tests', () => { suite('Test discovery', () => { @@ -87,7 +89,8 @@ suite('Result Resolver tests', () => { const populateTestTreeStub = sinon.stub(util, 'populateTestTree').returns(); // call resolve discovery - resultResolver.resolveDiscovery(payload, cancelationToken); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveDiscovery(payload, deferredTillEOT, cancelationToken); // assert the stub functions were called with the correct parameters @@ -126,7 +129,8 @@ suite('Result Resolver tests', () => { const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); // call resolve discovery - resultResolver.resolveDiscovery(payload); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveDiscovery(payload, deferredTillEOT, cancelationToken); // assert the stub functions were called with the correct parameters @@ -171,7 +175,8 @@ suite('Result Resolver tests', () => { // stub out functionality of populateTestTreeStub which is called in resolveDiscovery const populateTestTreeStub = sinon.stub(util, 'populateTestTree').returns(); // call resolve discovery - resultResolver.resolveDiscovery(payload, cancelationToken); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveDiscovery(payload, deferredTillEOT, cancelationToken); // assert the stub functions were called with the correct parameters @@ -270,22 +275,23 @@ suite('Result Resolver tests', () => { testProvider, workspaceUri, ); - const mockSubtestItem = createMockTestItem('parentTest subTest'); + const subtestName = 'parentTest [subTest with spaces and [brackets]]'; + const mockSubtestItem = createMockTestItem(subtestName); // add a mock test item to the map of known VSCode ids to run ids resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); // creates a mock test item with a space which will be used to split the runId - resultResolver.runIdToVSid.set('parentTest subTest', 'parentTest subTest'); + resultResolver.runIdToVSid.set(subtestName, subtestName); // add this mock test to the map of known test items resultResolver.runIdToTestItem.set('parentTest', mockTestItem2); - resultResolver.runIdToTestItem.set('parentTest subTest', mockSubtestItem); + resultResolver.runIdToTestItem.set(subtestName, mockSubtestItem); let generatedId: string | undefined; testControllerMock .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny())) .callback((id: string) => { generatedId = id; - console.log('createTestItem function called with id:', id); + traceLog('createTestItem function called with id:', id); }) .returns(() => ({ id: 'id_this', label: 'label_this', uri: workspaceUri } as TestItem)); @@ -294,23 +300,24 @@ suite('Result Resolver tests', () => { cwd: workspaceUri.fsPath, status: 'success', result: { - 'parentTest subTest': { - test: 'test', + 'parentTest [subTest with spaces and [brackets]]': { + test: 'parentTest', outcome: 'subtest-success', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess message: 'message', traceback: 'traceback', - subtest: 'subtest', + subtest: subtestName, }, }, error: '', }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item assert.ok(generatedId); - assert.strictEqual(generatedId, 'subTest'); + assert.strictEqual(generatedId, '[subTest with spaces and [brackets]]'); }); test('resolveExecution handles failed tests correctly', async () => { // test specific constants used expected values @@ -346,7 +353,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item runInstance.verify((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); @@ -385,7 +393,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item runInstance.verify((r) => r.skipped(typemoq.It.isAny()), typemoq.Times.once()); @@ -424,7 +433,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item runInstance.verify((r) => r.errored(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); @@ -463,7 +473,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item runInstance.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.once()); @@ -484,7 +495,8 @@ suite('Result Resolver tests', () => { error: 'error', }; - resultResolver.resolveExecution(errorPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(errorPayload, runInstance.object, deferredTillEOT); // verify that none of these functions are called diff --git a/extensions/positron-python/src/test/testing/testController/server.unit.test.ts b/extensions/positron-python/src/test/testing/testController/server.unit.test.ts index 53c2b72e40f7..02c35e806156 100644 --- a/extensions/positron-python/src/test/testing/testController/server.unit.test.ts +++ b/extensions/positron-python/src/test/testing/testController/server.unit.test.ts @@ -6,58 +6,66 @@ import * as assert from 'assert'; import * as net from 'net'; import * as sinon from 'sinon'; import * as crypto from 'crypto'; -import { OutputChannel, Uri } from 'vscode'; import { Observable } from 'rxjs'; import * as typeMoq from 'typemoq'; +import { OutputChannel, Uri } from 'vscode'; import { IPythonExecutionFactory, IPythonExecutionService, + ObservableExecutionResult, Output, - SpawnOptions, } from '../../../client/common/process/types'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; import { ITestDebugLauncher } from '../../../client/testing/common/types'; import { Deferred, createDeferred } from '../../../client/common/utils/async'; import { MockChildProcess } from '../../mocks/mockChildProcess'; - -suite('Python Test Server', () => { - const fakeUuid = 'fake-uuid'; - - let stubExecutionFactory: IPythonExecutionFactory; - let stubExecutionService: IPythonExecutionService; +import { + PAYLOAD_MULTI_CHUNK, + PAYLOAD_SINGLE_CHUNK, + PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY, + DataWithPayloadChunks, +} from './payloadTestCases'; +import { traceLog } from '../../../client/logging'; + +const testCases = [ + { + val: () => PAYLOAD_SINGLE_CHUNK('fake-uuid'), + }, + { + val: () => PAYLOAD_MULTI_CHUNK('fake-uuid'), + }, + { + val: () => PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY('fake-uuid'), + }, +]; + +suite('Python Test Server, DataWithPayloadChunks', () => { + const FAKE_UUID = 'fake-uuid'; let server: PythonTestServer; - let sandbox: sinon.SinonSandbox; let v4Stub: sinon.SinonStub; let debugLauncher: ITestDebugLauncher; let mockProc: MockChildProcess; let execService: typeMoq.IMock; let deferred: Deferred; - let execFactory = typeMoq.Mock.ofType(); - - setup(() => { - sandbox = sinon.createSandbox(); - v4Stub = sandbox.stub(crypto, 'randomUUID'); + const sandbox = sinon.createSandbox(); - v4Stub.returns(fakeUuid); - stubExecutionService = ({ - execObservable: () => Promise.resolve({ stdout: '', stderr: '' }), - } as unknown) as IPythonExecutionService; + setup(async () => { + // set up test command options - stubExecutionFactory = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService), - } as unknown) as IPythonExecutionFactory; + v4Stub = sandbox.stub(crypto, 'randomUUID'); + v4Stub.returns(FAKE_UUID); // set up exec service with child process mockProc = new MockChildProcess('', ['']); execService = typeMoq.Mock.ofType(); - const output = new Observable>(() => { + const outputObservable = new Observable>(() => { /* no op */ }); execService .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ proc: mockProc, - out: output, + out: outputObservable, dispose: () => { /* no-body */ }, @@ -70,53 +78,164 @@ suite('Python Test Server', () => { server.dispose(); }); + testCases.forEach((testCase) => { + test(`run correctly`, async () => { + const testCaseDataObj: DataWithPayloadChunks = testCase.val(); + let eventData = ''; + const client = new net.Socket(); + + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output2 = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { + client.connect(server.getPort()); + return { + proc: mockProc, + out: output2, + dispose: () => { + /* no-body */ + }, + }; + }, + } as unknown) as IPythonExecutionService; + + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); + const uuid = server.createUUID(); + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid, + }; + + const dataWithPayloadChunks = testCaseDataObj; + + await server.serverReady(); + + server.onRunDataReceived(({ data }) => { + try { + const resultData = JSON.parse(data).result; + eventData = eventData + JSON.stringify(resultData); + } catch (e) { + assert(false, 'Error parsing data'); + } + deferred.resolve(); + }); + client.on('connect', () => { + traceLog('Socket connected, local port:', client.localPort); + // since this test is a single payload as a single chunk there should be a single line in the payload. + for (const line of dataWithPayloadChunks.payloadArray) { + client.write(line); + } + client.end(); + }); + client.on('error', (error) => { + traceLog('Socket connection error:', error); + }); + + server.sendCommand(options); + await deferred.promise; + const expectedResult = dataWithPayloadChunks.data; + assert.deepStrictEqual(eventData, expectedResult); + }); + }); +}); + +suite('Python Test Server, Send command etc', () => { + const FAKE_UUID = 'fake-uuid'; + let server: PythonTestServer; + let v4Stub: sinon.SinonStub; + let debugLauncher: ITestDebugLauncher; + let mockProc: MockChildProcess; + let execService: typeMoq.IMock; + let deferred: Deferred; + const sandbox = sinon.createSandbox(); + + setup(async () => { + // set up test command options + + v4Stub = sandbox.stub(crypto, 'randomUUID'); + v4Stub.returns(FAKE_UUID); + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + execService = typeMoq.Mock.ofType(); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + }); + + teardown(() => { + sandbox.restore(); + server.dispose(); + }); test('sendCommand should add the port to the command being sent and add the correct extra spawn variables', async () => { - const options = { - command: { - script: 'myscript', - args: ['-foo', 'foo'], - }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - const expectedSpawnOptions = { - cwd: '/foo/bar', - outputChannel: undefined, - token: undefined, - throwOnStdErr: true, - extraVariables: { - PYTHONPATH: '/foo/bar', - RUN_TEST_IDS_PORT: '56789', - }, - } as SpawnOptions; const deferred2 = createDeferred(); - execFactory = typeMoq.Mock.ofType(); + const RUN_TEST_IDS_PORT_CONST = '5678'; + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((_args, options2) => { + try { + assert.strictEqual( + options2.extraVariables.PYTHONPATH, + '/foo/bar', + 'Expect python path to exist as extra variable and be set correctly', + ); + assert.strictEqual( + options2.extraVariables.RUN_TEST_IDS_PORT, + RUN_TEST_IDS_PORT_CONST, + 'Expect test id port to be in extra variables and set correctly', + ); + assert.strictEqual( + options2.extraVariables.TEST_UUID, + FAKE_UUID, + 'Expect test uuid to be in extra variables and set correctly', + ); + assert.strictEqual( + options2.extraVariables.TEST_PORT, + 12345, + 'Expect server port to be set correctly as a env var', + ); + } catch (e) { + assert(false, 'Error parsing data, extra variables do not match'); + } + return typeMoq.Mock.ofType>().object; + }); + const execFactory = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) .returns(() => { deferred2.resolve(); return Promise.resolve(execService.object); }); - server = new PythonTestServer(execFactory.object, debugLauncher); + sinon.stub(server, 'getPort').returns(12345); + // const portServer = server.getPort(); await server.serverReady(); - - server.sendCommand(options, '56789'); + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: FAKE_UUID, + }; + server.sendCommand(options, RUN_TEST_IDS_PORT_CONST); // add in await and trigger await deferred2.promise; mockProc.trigger('close'); - const port = server.getPort(); - const expectedArgs = ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']; - execService.verify((x) => x.execObservable(expectedArgs, expectedSpawnOptions), typeMoq.Times.once()); + const expectedArgs = ['myscript', '-foo', 'foo']; + execService.verify((x) => x.execObservable(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); }); test('sendCommand should write to an output channel if it is provided as an option', async () => { - const output: string[] = []; + const output2: string[] = []; const outChannel = { appendLine: (str: string) => { - output.push(str); + output2.push(str); }, } as OutputChannel; const options = { @@ -126,11 +245,11 @@ suite('Python Test Server', () => { }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', - uuid: fakeUuid, + uuid: FAKE_UUID, outChannel, }; deferred = createDeferred(); - execFactory = typeMoq.Mock.ofType(); + const execFactory = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) .returns(() => { @@ -146,330 +265,49 @@ suite('Python Test Server', () => { await deferred.promise; mockProc.trigger('close'); - const port = server.getPort(); - const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); + const expected = ['python', 'myscript', '-foo', 'foo'].join(' '); - assert.deepStrictEqual(output, [expected]); + assert.deepStrictEqual(output2, [expected]); }); test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { let eventData: { status: string; errors: string[] } | undefined; - stubExecutionService = ({ - execObservable: () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + const stubExecutionService = typeMoq.Mock.ofType(); + stubExecutionService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + stubExecutionService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred3.resolve(); throw new Error('Failed to execute'); - }, - } as unknown) as IPythonExecutionService; + }); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', - uuid: fakeUuid, + uuid: FAKE_UUID, }; + const stubExecutionFactory = typeMoq.Mock.ofType(); + stubExecutionFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(stubExecutionService.object); + }); - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(stubExecutionFactory.object, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = JSON.parse(data); }); - await server.sendCommand(options); - + server.sendCommand(options); + await deferred2.promise; + await deferred3.promise; assert.notEqual(eventData, undefined); assert.deepStrictEqual(eventData?.status, 'error'); assert.deepStrictEqual(eventData?.errors, ['Failed to execute']); }); - - test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - - deferred = createDeferred(); - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - await server.serverReady(); - server.onDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write('malformed data'); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - // add in await and trigger - await deferred.promise; - mockProc.trigger('close'); - - assert.deepStrictEqual(eventData, ''); - }); - - test('If the server doesnt recognize the UUID it should ignore it', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - await server.serverReady(); - server.onDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write('{"Request-uuid": "unknown-uuid"}'); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - await deferred.promise; - assert.deepStrictEqual(eventData, ''); - }); - - // required to have "tests" or "results" - // the heading length not being equal and yes being equal - // multiple payloads - test('Error if payload does not have a content length header', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - await server.serverReady(); - server.onDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write('{"not content length": "5"}'); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - await deferred.promise; - assert.deepStrictEqual(eventData, ''); - }); - - const testData = [ - { - testName: 'fires discovery correctly on test payload', - payload: `Content-Length: 52 -Content-Type: application/json -Request-uuid: UUID_HERE - -{"cwd": "path", "status": "success", "tests": "xyz"}`, - expectedResult: '{"cwd": "path", "status": "success", "tests": "xyz"}', - }, - // Add more test data as needed - ]; - - testData.forEach(({ testName, payload, expectedResult }) => { - test(`test: ${testName}`, async () => { - // Your test logic here - let eventData: string | undefined; - const client = new net.Socket(); - - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - await server.serverReady(); - const uuid = server.createUUID(); - payload = payload.replace('UUID_HERE', uuid); - server.onDiscoveryDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write(payload); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - await deferred.promise; - assert.deepStrictEqual(eventData, expectedResult); - }); - }); - - test('Calls run resolver if the result header is in the payload', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - - await server.serverReady(); - const uuid = server.createUUID(); - server.onRunDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - const payload = `Content-Length: 87 -Content-Type: application/json -Request-uuid: ${uuid} - -{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}`; - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write(payload); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - await deferred.promise; - const expectedResult = - '{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}'; - assert.deepStrictEqual(eventData, expectedResult); - }); }); diff --git a/extensions/positron-python/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/extensions/positron-python/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts new file mode 100644 index 000000000000..e85cd2b62834 --- /dev/null +++ b/extensions/positron-python/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -0,0 +1,379 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { CancellationTokenSource, TestRun, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Observable } from 'rxjs'; +import { IPythonExecutionFactory, IPythonExecutionService, Output } from '../../../client/common/process/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; +import { Deferred, createDeferred } from '../../../client/common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { ITestServer } from '../../../client/testing/testController/common/types'; +import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; +import { MockChildProcess } from '../../mocks/mockChildProcess'; +import * as util from '../../../client/testing/testController/common/utils'; + +suite('Execution Flow Run Adapters', () => { + let testServer: typeMoq.IMock; + let configService: IConfigurationService; + let execFactory = typeMoq.Mock.ofType(); + let adapter: PytestTestExecutionAdapter; + let execService: typeMoq.IMock; + let deferred: Deferred; + let debugLauncher: typeMoq.IMock; + (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; + let myTestPath: string; + let mockProc: MockChildProcess; + let utilsStartServerStub: sinon.SinonStub; + + setup(() => { + testServer = typeMoq.Mock.ofType(); + testServer.setup((t) => t.getPort()).returns(() => 12345); + testServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'], unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + + // mock out the result resolver + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + execService = typeMoq.Mock.ofType(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); + execFactory = typeMoq.Mock.ofType(); + utilsStartServerStub = sinon.stub(util, 'startTestIdServer'); + debugLauncher = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + deferred = createDeferred(); + execService + .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve({ stdout: '{}' }); + }); + execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + myTestPath = path.join('/', 'my', 'test', 'path', '/'); + }); + teardown(() => { + sinon.restore(); + }); + test('PYTEST cancelation token called mid-run resolves correctly', async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + // mock exec service and exec factory + const execServiceMock = typeMoq.Mock.ofType(); + execServiceMock + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + cancellationToken.cancel(); + return { + proc: mockProc, + out: typeMoq.Mock.ofType>>().object, + dispose: () => { + /* no-body */ + }, + }; + }); + const execFactoryMock = typeMoq.Mock.ofType(); + execFactoryMock + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceMock.object)); + execFactoryMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + const deferredStartServer = createDeferred(); + utilsStartServerStub.callsFake(() => { + deferredStartServer.resolve(); + return Promise.resolve(54321); + }); + // mock EOT token & ExecClose token + const deferredEOT = createDeferred(); + const deferredExecClose = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => { + if (utilsCreateEOTStub.callCount === 1) { + return deferredEOT; + } + return deferredExecClose; + }); + // set up test server + testServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => 'uuid123'); + adapter = new PytestTestExecutionAdapter( + testServer.object, + configService, + typeMoq.Mock.ofType().object, + ); + await adapter.runTests( + Uri.file(myTestPath), + [], + false, + testRunMock.object, + execFactoryMock.object, + debugLauncher.object, + ); + // wait for server to start to keep test from failing + await deferredStartServer.promise; + + testServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); + }); + test('PYTEST cancelation token called mid-debug resolves correctly', async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + // mock exec service and exec factory + const execServiceMock = typeMoq.Mock.ofType(); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .callback((_options, callback) => { + if (callback) { + callback(); + } + }) + .returns(async () => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + const execFactoryMock = typeMoq.Mock.ofType(); + execFactoryMock + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceMock.object)); + execFactoryMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + const deferredStartServer = createDeferred(); + utilsStartServerStub.callsFake(() => { + deferredStartServer.resolve(); + return Promise.resolve(54321); + }); + // mock EOT token & ExecClose token + const deferredEOT = createDeferred(); + const deferredExecClose = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => { + if (utilsCreateEOTStub.callCount === 1) { + return deferredEOT; + } + return deferredExecClose; + }); + // set up test server + testServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => 'uuid123'); + adapter = new PytestTestExecutionAdapter( + testServer.object, + configService, + typeMoq.Mock.ofType().object, + ); + await adapter.runTests( + Uri.file(myTestPath), + [], + true, + testRunMock.object, + execFactoryMock.object, + debugLauncher.object, + ); + // wait for server to start to keep test from failing + await deferredStartServer.promise; + + testServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); + }); + test('UNITTEST cancelation token called mid-run resolves correctly', async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + + // Stub send command to then have token canceled + const stubTestServer = typeMoq.Mock.ofType(); + stubTestServer + .setup((t) => + t.sendCommand( + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + ) + .returns(() => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + + stubTestServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => 'uuid123'); + stubTestServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + // mock exec service and exec factory + const execServiceMock = typeMoq.Mock.ofType(); + execServiceMock + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + cancellationToken.cancel(); + return { + proc: mockProc, + out: typeMoq.Mock.ofType>>().object, + dispose: () => { + /* no-body */ + }, + }; + }); + const execFactoryMock = typeMoq.Mock.ofType(); + execFactoryMock + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceMock.object)); + execFactoryMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + const deferredStartServer = createDeferred(); + utilsStartServerStub.callsFake(() => { + deferredStartServer.resolve(); + return Promise.resolve(54321); + }); + // mock EOT token & ExecClose token + const deferredEOT = createDeferred(); + const deferredExecClose = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => { + if (utilsCreateEOTStub.callCount === 1) { + return deferredEOT; + } + return deferredExecClose; + }); + // set up test server + const unittestAdapter = new UnittestTestExecutionAdapter( + stubTestServer.object, + configService, + typeMoq.Mock.ofType().object, + ); + await unittestAdapter.runTests(Uri.file(myTestPath), [], false, testRunMock.object); + // wait for server to start to keep test from failing + await deferredStartServer.promise; + + stubTestServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); + }); + test('UNITTEST cancelation token called mid-debug resolves correctly', async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + + // Stub send command to then have token canceled + const stubTestServer = typeMoq.Mock.ofType(); + stubTestServer + .setup((t) => + t.sendCommand( + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + ) + .returns(() => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + + stubTestServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => 'uuid123'); + stubTestServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + // mock exec service and exec factory + const execServiceMock = typeMoq.Mock.ofType(); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async () => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + const execFactoryMock = typeMoq.Mock.ofType(); + execFactoryMock + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceMock.object)); + execFactoryMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + const deferredStartServer = createDeferred(); + utilsStartServerStub.callsFake(() => { + deferredStartServer.resolve(); + return Promise.resolve(54321); + }); + // mock EOT token & ExecClose token + const deferredEOT = createDeferred(); + const deferredExecClose = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => { + if (utilsCreateEOTStub.callCount === 1) { + return deferredEOT; + } + return deferredExecClose; + }); + // set up test server + const unittestAdapter = new UnittestTestExecutionAdapter( + stubTestServer.object, + configService, + typeMoq.Mock.ofType().object, + ); + await unittestAdapter.runTests(Uri.file(myTestPath), [], false, testRunMock.object); + // wait for server to start to keep test from failing + await deferredStartServer.promise; + + stubTestServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); + }); +}); diff --git a/extensions/positron-python/src/test/testing/testController/utils.unit.test.ts b/extensions/positron-python/src/test/testing/testController/utils.unit.test.ts index d971c7d37c9f..014261a40232 100644 --- a/extensions/positron-python/src/test/testing/testController/utils.unit.test.ts +++ b/extensions/positron-python/src/test/testing/testController/utils.unit.test.ts @@ -6,15 +6,16 @@ import { JSONRPC_CONTENT_LENGTH_HEADER, JSONRPC_CONTENT_TYPE_HEADER, JSONRPC_UUID_HEADER, - jsonRPCContent, - jsonRPCHeaders, + ExtractJsonRPCData, + parseJsonRPCHeadersAndData, + splitTestNameWithRegex, } from '../../../client/testing/testController/common/utils'; suite('Test Controller Utils: JSON RPC', () => { test('Empty raw data string', async () => { const rawDataString = ''; - const output = jsonRPCHeaders(rawDataString); + const output = parseJsonRPCHeadersAndData(rawDataString); assert.deepStrictEqual(output.headers.size, 0); assert.deepStrictEqual(output.remainingRawData, ''); }); @@ -22,20 +23,20 @@ suite('Test Controller Utils: JSON RPC', () => { test('Valid data empty JSON', async () => { const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: 2\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n{}`; - const rpcHeaders = jsonRPCHeaders(rawDataString); + const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString); assert.deepStrictEqual(rpcHeaders.headers.size, 3); assert.deepStrictEqual(rpcHeaders.remainingRawData, '{}'); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); assert.deepStrictEqual(rpcContent.extractedJSON, '{}'); }); test('Valid data NO JSON', async () => { const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: 0\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n`; - const rpcHeaders = jsonRPCHeaders(rawDataString); + const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString); assert.deepStrictEqual(rpcHeaders.headers.size, 3); assert.deepStrictEqual(rpcHeaders.remainingRawData, ''); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); assert.deepStrictEqual(rpcContent.extractedJSON, ''); }); @@ -45,10 +46,10 @@ suite('Test Controller Utils: JSON RPC', () => { '{"jsonrpc": "2.0", "method": "initialize", "params": {"processId": 1234, "rootPath": "/home/user/project", "rootUri": "file:///home/user/project", "capabilities": {}}, "id": 0}'; const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: ${json.length}\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n${json}`; - const rpcHeaders = jsonRPCHeaders(rawDataString); + const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString); assert.deepStrictEqual(rpcHeaders.headers.size, 3); assert.deepStrictEqual(rpcHeaders.remainingRawData, json); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); assert.deepStrictEqual(rpcContent.extractedJSON, json); }); @@ -58,10 +59,103 @@ suite('Test Controller Utils: JSON RPC', () => { const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: ${json.length}\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n${json}`; const rawDataString2 = rawDataString + rawDataString; - const rpcHeaders = jsonRPCHeaders(rawDataString2); + const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString2); assert.deepStrictEqual(rpcHeaders.headers.size, 3); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); assert.deepStrictEqual(rpcContent.extractedJSON, json); assert.deepStrictEqual(rpcContent.remainingRawData, rawDataString); }); + + test('Valid constant', async () => { + const data = `{"cwd": "/Users/eleanorboyd/testingFiles/inc_dec_example", "status": "success", "result": {"test_dup_class.test_a.TestSomething.test_a": {"test": "test_dup_class.test_a.TestSomething.test_a", "outcome": "success", "message": "None", "traceback": null, "subtest": null}}}`; + const secondPayload = `Content-Length: 270 +Content-Type: application/json +Request-uuid: 496c86b1-608f-4886-9436-ec00538e144c + +${data}`; + const payload = `Content-Length: 270 +Content-Type: application/json +Request-uuid: 496c86b1-608f-4886-9436-ec00538e144c + +${data}${secondPayload}`; + + const rpcHeaders = parseJsonRPCHeadersAndData(payload); + assert.deepStrictEqual(rpcHeaders.headers.size, 3); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); + assert.deepStrictEqual(rpcContent.extractedJSON, data); + assert.deepStrictEqual(rpcContent.remainingRawData, secondPayload); + }); + test('Valid content length as only header with carriage return', async () => { + const payload = `Content-Length: 7 + `; + + const rpcHeaders = parseJsonRPCHeadersAndData(payload); + assert.deepStrictEqual(rpcHeaders.headers.size, 1); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); + assert.deepStrictEqual(rpcContent.extractedJSON, ''); + assert.deepStrictEqual(rpcContent.remainingRawData, ''); + }); + test('Valid content length header with no value', async () => { + const payload = `Content-Length:`; + + const rpcHeaders = parseJsonRPCHeadersAndData(payload); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); + assert.deepStrictEqual(rpcContent.extractedJSON, ''); + assert.deepStrictEqual(rpcContent.remainingRawData, ''); + }); + + suite('Test Controller Utils: Other', () => { + interface TestCase { + name: string; + input: string; + expectedParent: string; + expectedSubtest: string; + } + + const testCases: Array = [ + { + name: 'Single parameter, named', + input: 'test_package.ClassName.test_method (param=value)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param=value)', + }, + { + name: 'Single parameter, unnamed', + input: 'test_package.ClassName.test_method [value]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '[value]', + }, + { + name: 'Multiple parameters, named', + input: 'test_package.ClassName.test_method (param1=value1, param2=value2)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param1=value1, param2=value2)', + }, + { + name: 'Multiple parameters, unnamed', + input: 'test_package.ClassName.test_method [value1, value2]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '[value1, value2]', + }, + { + name: 'Names with special characters', + input: 'test_package.ClassName.test_method (param1=value/1, param2=value+2)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param1=value/1, param2=value+2)', + }, + { + name: 'Names with spaces', + input: 'test_package.ClassName.test_method ["a b c d"]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '["a b c d"]', + }, + ]; + + testCases.forEach((testCase) => { + test(`splitTestNameWithRegex: ${testCase.name}`, () => { + const splitResult = splitTestNameWithRegex(testCase.input); + assert.deepStrictEqual(splitResult, [testCase.expectedParent, testCase.expectedSubtest]); + }); + }); + }); }); diff --git a/extensions/positron-python/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py b/extensions/positron-python/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py new file mode 100644 index 000000000000..5aac911b575a --- /dev/null +++ b/extensions/positron-python/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +import ctypes + +ctypes.string_at(0) # Dereference a NULL pointer + + +class TestSegmentationFault(unittest.TestCase): + def test_segfault(self): + assert True + + +if __name__ == "__main__": + unittest.main() diff --git a/extensions/positron-python/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py b/extensions/positron-python/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py new file mode 100644 index 000000000000..bad7ff8fcbbd --- /dev/null +++ b/extensions/positron-python/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +import ctypes + + +class TestSegmentationFault(unittest.TestCase): + def cause_segfault(self): + ctypes.string_at(0) # Dereference a NULL pointer + + def test_segfault(self): + assert True + self.cause_segfault() + + +if __name__ == "__main__": + unittest.main() diff --git a/extensions/positron-python/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py b/extensions/positron-python/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py index 3e84df0a2d9f..a76856ebb929 100644 --- a/extensions/positron-python/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py +++ b/extensions/positron-python/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py @@ -4,13 +4,13 @@ import unittest -@pytest.mark.parametrize("num", range(0, 200)) +@pytest.mark.parametrize("num", range(0, 2000)) def test_odd_even(num): - return num % 2 == 0 + assert num % 2 == 0 class NumbersTest(unittest.TestCase): def test_even(self): - for i in range(0, 200): + for i in range(0, 2000): with self.subTest(i=i): self.assertEqual(i % 2, 0) diff --git a/extensions/positron-python/yarn.lock b/extensions/positron-python/yarn.lock index e414f152c901..c724a723ea2e 100644 --- a/extensions/positron-python/yarn.lock +++ b/extensions/positron-python/yarn.lock @@ -25,7 +25,16 @@ "@azure/abort-controller" "^1.0.0" tslib "^2.2.0" -"@azure/core-rest-pipeline@^1.10.0": +"@azure/core-auth@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.5.0.tgz#a41848c5c31cb3b7c84c409885267d55a2c92e44" + integrity sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw== + dependencies: + "@azure/abort-controller" "^1.0.0" + "@azure/core-util" "^1.1.0" + tslib "^2.2.0" + +"@azure/core-rest-pipeline@1.10.1": version "1.10.1" resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.1.tgz#348290847ca31b9eecf9cf5de7519aaccdd30968" integrity sha512-Kji9k6TOFRDB5ZMTw8qUf2IJ+CeJtsuMdAHox9eqpTf1cefiNMpzrfnF6sINEBZJsaVaWgQ0o48B6kcUH68niA== @@ -41,13 +50,21 @@ tslib "^2.2.0" uuid "^8.3.0" -"@azure/core-tracing@^1.0.1": +"@azure/core-tracing@^1.0.0", "@azure/core-tracing@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@azure/core-tracing/-/core-tracing-1.0.1.tgz#352a38cbea438c4a83c86b314f48017d70ba9503" integrity sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw== dependencies: tslib "^2.2.0" +"@azure/core-util@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.2.0.tgz#3499deba1fc36dda6f1912b791809b6f15d4a392" + integrity sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng== + dependencies: + "@azure/abort-controller" "^1.0.0" + tslib "^2.2.0" + "@azure/core-util@^1.0.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.1.1.tgz#8f87b3dd468795df0f0849d9f096c3e7b29452c1" @@ -56,6 +73,14 @@ "@azure/abort-controller" "^1.0.0" tslib "^2.2.0" +"@azure/core-util@^1.1.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.5.0.tgz#ffe49c3e867044da67daeb8122143fa065e1eb0e" + integrity sha512-GZBpVFDtQ/15hW1OgBcRdT4Bl7AEpcEZqLfbAvOtm1CQUncKWiYapFHVD588hmlV27NbOOtSm3cnLF3lvoHi4g== + dependencies: + "@azure/abort-controller" "^1.0.0" + tslib "^2.2.0" + "@azure/logger@^1.0.0": version "1.0.3" resolved "https://registry.yarnpkg.com/@azure/logger/-/logger-1.0.3.tgz#6e36704aa51be7d4a1bae24731ea580836293c96" @@ -63,6 +88,18 @@ dependencies: tslib "^2.2.0" +"@azure/opentelemetry-instrumentation-azure-sdk@^1.0.0-beta.5": + version "1.0.0-beta.5" + resolved "https://registry.yarnpkg.com/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0-beta.5.tgz#78809e6c005d08450701e5d37f087f6fce2f86eb" + integrity sha512-fsUarKQDvjhmBO4nIfaZkfNSApm1hZBzcvpNbSrXdcUBxu7lRvKsV5DnwszX7cnhLyVOW9yl1uigtRQ1yDANjA== + dependencies: + "@azure/core-tracing" "^1.0.0" + "@azure/logger" "^1.0.0" + "@opentelemetry/api" "^1.4.1" + "@opentelemetry/core" "^1.15.2" + "@opentelemetry/instrumentation" "^0.41.2" + tslib "^2.2.0" + "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" @@ -382,66 +419,88 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@microsoft/1ds-core-js@3.2.11", "@microsoft/1ds-core-js@^3.2.9": - version "3.2.11" - resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-3.2.11.tgz#0a3857562a9bde7c15a2fa141c647f40aabd52e2" - integrity sha512-UuqZ1iWjaEFsBsnB+J0Q4IbgJdvJAq3LcNArAdMQQq9+wBWpjyGG4yu9gL6fS5AKfpF6yy73mtgWD7WzvphMLQ== +"@microsoft/1ds-core-js@3.2.14", "@microsoft/1ds-core-js@^3.2.13": + version "3.2.14" + resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-3.2.14.tgz#407c83999b0f75d9939bb32f6fc10b1439938cef" + integrity sha512-UW1YrUTPuvXmAarzqVSeYU1vwTzxCCZdd+ANcf/SDkxm53mCCgxSodVP9yrxtGOQdKhE9Y3rZdETRSvBBdKdXg== dependencies: - "@microsoft/applicationinsights-core-js" "2.8.13" + "@microsoft/applicationinsights-core-js" "2.8.16" "@microsoft/applicationinsights-shims" "^2.0.2" "@microsoft/dynamicproto-js" "^1.1.7" -"@microsoft/1ds-post-js@^3.2.9": - version "3.2.11" - resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-3.2.11.tgz#bbfc935bafb43982ccaf94ebd12f1fabf5ef8ebe" - integrity sha512-86prrN8cKjfpqfeyC4k0rFqg7fJDkTwPeKLHnkUgfq9mKVzu+BdtlEzaJtc56MroKWNYPkitZ/PWSwwpTPIgMA== +"@microsoft/1ds-post-js@^3.2.13": + version "3.2.14" + resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-3.2.14.tgz#1ebd873a47eab9d97fa0cff593de303f641cdb51" + integrity sha512-pXTfyW8SI6LS2r92W2Zaxl/YjJCHTEX9ej/GdabtlMAikGGgAlEsMlk5a4SJ3BI3K8sDUSGi+oTmJ1FtVes+bQ== dependencies: - "@microsoft/1ds-core-js" "3.2.11" + "@microsoft/1ds-core-js" "3.2.14" "@microsoft/applicationinsights-shims" "^2.0.2" "@microsoft/dynamicproto-js" "^1.1.7" -"@microsoft/applicationinsights-channel-js@2.8.13": - version "2.8.13" - resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.8.13.tgz#bd117cc7b00ec929e74a6555cb7462ab4fccdf62" - integrity sha512-zc2BSsHk4HAqK5STdNzKGV817jKNbiTZPYpNt3zuE+jO5druJgloqrvclUfLnCoa7zwrQ2UxoAXlpJGmroGZPA== +"@microsoft/applicationinsights-channel-js@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.3.tgz#24adc0fd292321a3046b315831e66b7e87d5380d" + integrity sha512-CWcJQCMKU3CxWLFIE8iPa3G5KB3v2RFkAvPICfY8/fwWZq4tWY7zosgvRPDZ+dIkz8Z/+CMy0+KblYzIKDdG4A== dependencies: - "@microsoft/applicationinsights-common" "2.8.13" - "@microsoft/applicationinsights-core-js" "2.8.13" - "@microsoft/applicationinsights-shims" "2.0.2" - "@microsoft/dynamicproto-js" "^1.1.9" + "@microsoft/applicationinsights-common" "3.0.3" + "@microsoft/applicationinsights-core-js" "3.0.3" + "@microsoft/applicationinsights-shims" "3.0.1" + "@microsoft/dynamicproto-js" "^2.0.2" + "@nevware21/ts-async" ">= 0.3.0 < 2.x" + "@nevware21/ts-utils" ">= 0.10.1 < 2.x" -"@microsoft/applicationinsights-common@2.8.13": - version "2.8.13" - resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-common/-/applicationinsights-common-2.8.13.tgz#e3457bdc54a61cbfa481494da25e55bb9156096e" - integrity sha512-UYLLGVtuzrWUEmGYRroMzLyTi2fHqL6SwJUlmVWPJrmdK43PGpviRix/sBW0Qs+6qjiI1Z6CiG4Xah6w/HylhA== +"@microsoft/applicationinsights-common@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.3.tgz#5b2267c02c027e9ff9e07ca8c49741bf139ddf4e" + integrity sha512-GwnerHHPexry2CMTr6gP2Rjm0e3rgVXZzFCbrxcoOQk8MqrEFDWur6Xs66FwXpGFnY3KV3Zsujkfcl0oePs4Cg== dependencies: - "@microsoft/applicationinsights-core-js" "2.8.13" - "@microsoft/applicationinsights-shims" "2.0.2" - "@microsoft/dynamicproto-js" "^1.1.9" + "@microsoft/applicationinsights-core-js" "3.0.3" + "@microsoft/applicationinsights-shims" "3.0.1" + "@microsoft/dynamicproto-js" "^2.0.2" + "@nevware21/ts-utils" ">= 0.10.1 < 2.x" -"@microsoft/applicationinsights-core-js@2.8.13": - version "2.8.13" - resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.13.tgz#45c2b8fff35e5aa519355dd3a69e3758293b08f4" - integrity sha512-PP7Xjplvy0d5G2Tk7DcSDYRmgDYRv+7n3wEiqgm63DrSoa8rEuoODavjWunhX058zPNIeKbus59NE+DusLLyZg== +"@microsoft/applicationinsights-core-js@2.8.16": + version "2.8.16" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.16.tgz#b209c908a63128b4603d00d30357d646bb7da8d3" + integrity sha512-pO5rR6UuiPymiHFj8XxNXhQgBSTvyHWygf+gdEVDh0xpUXYFO99bZe0Ux0D0HqYqVkJrRfXzL1Ocru6+S0x53Q== dependencies: "@microsoft/applicationinsights-shims" "2.0.2" "@microsoft/dynamicproto-js" "^1.1.9" +"@microsoft/applicationinsights-core-js@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.3.tgz#8904e1a1bb023f71c37c20eac27b106f71d76ae8" + integrity sha512-ymtdPHgUhCwIwQZx2ZN3Xw3cq+Z5KHzGmFV8QvURSdUzfaHbjYcHXIQkEZbgCCGOTMLtx9lZqP7J1gbBy0O8GQ== + dependencies: + "@microsoft/applicationinsights-shims" "3.0.1" + "@microsoft/dynamicproto-js" "^2.0.2" + "@nevware21/ts-async" ">= 0.3.0 < 2.x" + "@nevware21/ts-utils" ">= 0.10.1 < 2.x" + "@microsoft/applicationinsights-shims@2.0.2", "@microsoft/applicationinsights-shims@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-2.0.2.tgz#92b36a09375e2d9cb2b4203383b05772be837085" integrity sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg== -"@microsoft/applicationinsights-web-basic@^2.8.11": - version "2.8.13" - resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-2.8.13.tgz#a3934fa7b7f221d09fbf403fcfeb06a8c18ca246" - integrity sha512-DgPx1ryZucLWc285qkAEBG6LCcTSG6Gdb4u4yAlmAW0G+Qau49GoJnfJGR+cDXvyXSwHcH0dsainqzeYYY1K7A== +"@microsoft/applicationinsights-shims@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz#3865b73ace8405b9c4618cc5c571f2fe3876f06f" + integrity sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg== dependencies: - "@microsoft/applicationinsights-channel-js" "2.8.13" - "@microsoft/applicationinsights-common" "2.8.13" - "@microsoft/applicationinsights-core-js" "2.8.13" - "@microsoft/applicationinsights-shims" "2.0.2" - "@microsoft/dynamicproto-js" "^1.1.9" + "@nevware21/ts-utils" ">= 0.9.4 < 2.x" + +"@microsoft/applicationinsights-web-basic@^3.0.2": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.3.tgz#f2c136dfda31a0f22ca06b417e750de8c7c064cb" + integrity sha512-PCKfbCTJUbcyZnRzasxlYgV3L1ORVxpggRB1Uohx0rNyEyKPwk7lcCcFos11NEA+pn/XqZrZY5FYSdaLT45wNA== + dependencies: + "@microsoft/applicationinsights-channel-js" "3.0.3" + "@microsoft/applicationinsights-common" "3.0.3" + "@microsoft/applicationinsights-core-js" "3.0.3" + "@microsoft/applicationinsights-shims" "3.0.1" + "@microsoft/dynamicproto-js" "^2.0.2" + "@nevware21/ts-async" ">= 0.3.0 < 2.x" + "@nevware21/ts-utils" ">= 0.10.1 < 2.x" "@microsoft/applicationinsights-web-snippet@^1.0.1": version "1.0.1" @@ -458,6 +517,25 @@ resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz#7437db7aa061162ee94e4131b69a62b8dad5dea6" integrity sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ== +"@microsoft/dynamicproto-js@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz#e57fbec2e7067d48b7e8e1e1c1d354028ef718a6" + integrity sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg== + dependencies: + "@nevware21/ts-utils" ">= 0.9.4 < 2.x" + +"@nevware21/ts-async@>= 0.3.0 < 2.x": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@nevware21/ts-async/-/ts-async-0.3.0.tgz#a8b97ba01065fc930de9a3f4dd4a05e862becc6c" + integrity sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA== + dependencies: + "@nevware21/ts-utils" ">= 0.10.0 < 2.x" + +"@nevware21/ts-utils@>= 0.10.0 < 2.x", "@nevware21/ts-utils@>= 0.10.1 < 2.x", "@nevware21/ts-utils@>= 0.9.4 < 2.x": + version "0.10.1" + resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz#aa65abc71eba06749a396598f22263d26f796ac7" + integrity sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -479,39 +557,50 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@opentelemetry/api@^1.0.4": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.4.0.tgz#2c91791a9ba6ca0a0f4aaac5e45d58df13639ac8" - integrity sha512-IgMK9i3sFGNUqPMbjABm0G26g0QCKCUBfglhQ7rQq6WcxbKfEHRcmwsoER4hZcuYqJgkYn2OeuoJIv7Jsftp7g== +"@opentelemetry/api@^1.4.1": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.6.0.tgz#de2c6823203d6f319511898bb5de7e70f5267e19" + integrity sha512-OWlrQAnWn9577PhVgqjUvMr1pg57Bc4jv0iL4w0PRuOSRvq67rvHW9Ie/dZVMvCzhSCB+UxhcY/PmCmFj33Q+g== + +"@opentelemetry/core@1.17.0", "@opentelemetry/core@^1.15.2": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.17.0.tgz#6a72425f5f953dc68b4c7c66d947c018173d30d2" + integrity sha512-tfnl3h+UefCgx1aeN2xtrmr6BmdWGKXypk0pflQR0urFS40aE88trnkOMc2HTJZbMrqEEl4HsaBeFhwLVXsrJg== + dependencies: + "@opentelemetry/semantic-conventions" "1.17.0" -"@opentelemetry/core@1.9.1", "@opentelemetry/core@^1.0.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.9.1.tgz#e343337e1a7bf30e9a6aef3ef659b9b76379762a" - integrity sha512-6/qon6tw2I8ZaJnHAQUUn4BqhTbTNRS0WP8/bA0ynaX+Uzp/DDbd0NS0Cq6TMlh8+mrlsyqDE7mO50nmv2Yvlg== +"@opentelemetry/instrumentation@^0.41.2": + version "0.41.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.41.2.tgz#cae11fa64485dcf03dae331f35b315b64bc6189f" + integrity sha512-rxU72E0pKNH6ae2w5+xgVYZLzc5mlxAbGzF4shxMVK8YC2QQsfN38B2GPbj0jvrKWWNUElfclQ+YTykkNg/grw== dependencies: - "@opentelemetry/semantic-conventions" "1.9.1" + "@types/shimmer" "^1.0.2" + import-in-the-middle "1.4.2" + require-in-the-middle "^7.1.1" + semver "^7.5.1" + shimmer "^1.2.1" -"@opentelemetry/resources@1.9.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.9.1.tgz#5ad3d80ba968a3a0e56498ce4bc82a6a01f2682f" - integrity sha512-VqBGbnAfubI+l+yrtYxeLyOoL358JK57btPMJDd3TCOV3mV5TNBmzvOfmesM4NeTyXuGJByd3XvOHvFezLn3rQ== +"@opentelemetry/resources@1.17.0": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.17.0.tgz#ee29144cfd7d194c69698c8153dbadec7fe6819f" + integrity sha512-+u0ciVnj8lhuL/qGRBPeVYvk7fL+H/vOddfvmOeJaA1KC+5/3UED1c9KoZQlRsNT5Kw1FaK8LkY2NVLYfOVZQw== dependencies: - "@opentelemetry/core" "1.9.1" - "@opentelemetry/semantic-conventions" "1.9.1" + "@opentelemetry/core" "1.17.0" + "@opentelemetry/semantic-conventions" "1.17.0" -"@opentelemetry/sdk-trace-base@^1.0.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.9.1.tgz#c349491b432a7e0ae7316f0b48b2d454d79d2b84" - integrity sha512-Y9gC5M1efhDLYHeeo2MWcDDMmR40z6QpqcWnPCm4Dmh+RHAMf4dnEBBntIe1dDpor686kyU6JV1D29ih1lZpsQ== +"@opentelemetry/sdk-trace-base@^1.15.2": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.17.0.tgz#05a21763c9efa72903c20b8930293cdde344b681" + integrity sha512-2T5HA1/1iE36Q9eg6D4zYlC4Y4GcycI1J6NsHPKZY9oWfAxWsoYnRlkPfUqyY5XVtocCo/xHpnJvGNHwzT70oQ== dependencies: - "@opentelemetry/core" "1.9.1" - "@opentelemetry/resources" "1.9.1" - "@opentelemetry/semantic-conventions" "1.9.1" + "@opentelemetry/core" "1.17.0" + "@opentelemetry/resources" "1.17.0" + "@opentelemetry/semantic-conventions" "1.17.0" -"@opentelemetry/semantic-conventions@1.9.1", "@opentelemetry/semantic-conventions@^1.0.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.9.1.tgz#ad3367684a57879392513479e0a436cb2ac46dad" - integrity sha512-oPQdbFDmZvjXk5ZDoBGXG8B4tSB/qW5vQunJWQMFUBp7Xe8O1ByPANueJ+Jzg58esEBegyyxZ7LRmfJr7kFcFg== +"@opentelemetry/semantic-conventions@1.17.0", "@opentelemetry/semantic-conventions@^1.15.2": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.17.0.tgz#af10baa9f05ce1e64a14065fc138b5739bfb65f6" + integrity sha512-+fguCd2d8d2qruk0H0DsCEy2CTK3t0Tugg7MhZ/UQMvmewbZLNnJ6heSYyzIZWG5IPfAXzoj4f4F/qpM7l4VBA== "@polka/url@^1.0.0-next.20": version "1.0.0-next.21" @@ -739,16 +828,21 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850" integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg== -"@types/node@^16.17.0": - version "16.18.25" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.25.tgz#8863940fefa1234d3fcac7a4b7a48a6c992d67af" - integrity sha512-rUDO6s9Q/El1R1I21HG4qw/LstTHCPO/oQNAwI/4b2f9EWvMnqt4d3HJwPMawfZ3UvodB8516Yg+VAq54YM+eA== +"@types/node@^18.17.1": + version "18.18.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.4.tgz#519fef47a13cf869be290c20fc6ae9b7fe887aa7" + integrity sha512-t3rNFBgJRugIhackit2mVcLfF6IRc0JE4oeizPQL8Zrm8n2WY/0wOdpOPhdtG0V9Q2TlW/axbF1MJ6z+Yj/kKQ== "@types/semver@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ== +"@types/shimmer@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.0.3.tgz#2259274874392a4413fb6f6717a7ec209a8651a7" + integrity sha512-F/IjUGnV6pIN7R4ZV4npHJVoNtaLZWvb+2/9gctxjb99wkpI7Ozg8VPogwDiTRyjLwZXAYxjvdg1KS8LTHKdDA== + "@types/shortid@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/shortid/-/shortid-0.0.29.tgz#8093ee0416a6e2bf2aa6338109114b3fbffa0e9b" @@ -873,15 +967,15 @@ resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== -"@vscode/extension-telemetry@^0.7.7": - version "0.7.7" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.7.7.tgz#8213bfbdb1afa216befb146d6563ec5d200d7608" - integrity sha512-uW508BPjkWDBOKvvvSym3ZmGb7kHIiWaAfB/1PHzLz2x9TrC33CfjmFEI+CywIL/jBv4bqZxxjN4tfefB61F+g== +"@vscode/extension-telemetry@^0.8.4": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.5.tgz#3db305be907c01656160e25d91f5d2840175d199" + integrity sha512-YFKANBT2F3qdWQstjcr40XX8BLsdKlKM7a7YPi/jNuMjuiPhb1Jn7YsDR3WZaVEzAqeqGy4gzXsFCBbuZ+L1Tg== dependencies: - "@microsoft/1ds-core-js" "^3.2.9" - "@microsoft/1ds-post-js" "^3.2.9" - "@microsoft/applicationinsights-web-basic" "^2.8.11" - applicationinsights "2.5.0" + "@microsoft/1ds-core-js" "^3.2.13" + "@microsoft/1ds-post-js" "^3.2.13" + "@microsoft/applicationinsights-web-basic" "^3.0.2" + applicationinsights "^2.7.1" "@vscode/jupyter-lsp-middleware@^0.2.50": version "0.2.50" @@ -904,15 +998,15 @@ vscode-languageserver-protocol "^3.17.3-next.1" vscode-uri "^3.0.2" -"@vscode/test-electron@^2.1.3": - version "2.2.3" - resolved "https://registry.yarnpkg.com/@vscode/test-electron/-/test-electron-2.2.3.tgz#bf6a77542970b5d34561d0671259900632e5eb94" - integrity sha512-7DmdGYQTqRNaLHKG3j56buc9DkstriY4aV0S3Zj32u0U9/T0L8vwWAC9QGCh1meu1VXDEla1ze27TkqysHGP0Q== +"@vscode/test-electron@^2.3.4": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@vscode/test-electron/-/test-electron-2.3.5.tgz#c472c5bdce1329aeb4762b8aa7a2cbe7aa783aac" + integrity sha512-lAW7nQ0HuPqJnGJrtCzEKZCICtRizeP6qNanyCrjmdCOAAWjX3ixiG8RVPwqsYPQBWLPgYuE12qQlwXsOR/2fQ== dependencies: http-proxy-agent "^4.0.1" https-proxy-agent "^5.0.0" - rimraf "^3.0.2" - unzipper "^0.10.11" + jszip "^3.10.1" + semver "^7.5.2" "@vscode/vsce@^2.18.0": version "2.19.0" @@ -1095,6 +1189,11 @@ acorn-import-assertions@^1.7.6: resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== +acorn-import-assertions@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== + acorn-jsx@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -1115,6 +1214,11 @@ acorn@^8.0.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== +acorn@^8.8.2: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -1243,22 +1347,24 @@ append-transform@^2.0.0: dependencies: default-require-extensions "^3.0.0" -applicationinsights@2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-2.5.0.tgz#f008580b2f68267a5d233cce4e1f50b587bdf3c4" - integrity sha512-6kIFmpANRok+6FhCOmO7ZZ/mh7fdNKn17BaT13cg/RV5roLPJlA6q8srWexayHd3MPcwMb9072e8Zp0P47s/pw== +applicationinsights@^2.7.1: + version "2.8.0" + resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-2.8.0.tgz#7a294c9c173e04bf4813e263055afe5763623f33" + integrity sha512-pxVOdCPwXTal1A904yGmzOOUJrIeQ89xQir0ifr7fLl+e0BlGrZ1P4StcIDuEXk93gV9CGxGm5Mol8ksPk2mcg== dependencies: - "@azure/core-auth" "^1.4.0" - "@azure/core-rest-pipeline" "^1.10.0" + "@azure/core-auth" "^1.5.0" + "@azure/core-rest-pipeline" "1.10.1" + "@azure/core-util" "1.2.0" + "@azure/opentelemetry-instrumentation-azure-sdk" "^1.0.0-beta.5" "@microsoft/applicationinsights-web-snippet" "^1.0.1" - "@opentelemetry/api" "^1.0.4" - "@opentelemetry/core" "^1.0.1" - "@opentelemetry/sdk-trace-base" "^1.0.1" - "@opentelemetry/semantic-conventions" "^1.0.1" + "@opentelemetry/api" "^1.4.1" + "@opentelemetry/core" "^1.15.2" + "@opentelemetry/sdk-trace-base" "^1.15.2" + "@opentelemetry/semantic-conventions" "^1.15.2" cls-hooked "^4.2.2" continuation-local-storage "^3.2.1" - diagnostic-channel "1.1.0" - diagnostic-channel-publishers "1.0.5" + diagnostic-channel "1.1.1" + diagnostic-channel-publishers "1.0.7" arch@^2.1.0: version "2.2.0" @@ -1610,11 +1716,6 @@ bent@^7.3.12: caseless "~0.12.0" is-stream "^2.0.0" -big-integer@^1.6.17: - version "1.6.51" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" - integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== - big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -1630,14 +1731,6 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -binary@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" - integrity sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg== - dependencies: - buffers "~0.1.1" - chainsaw "~0.1.0" - bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -1662,11 +1755,6 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" -bluebird@~3.4.1: - version "3.4.7" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" - integrity sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA== - bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" @@ -1834,11 +1922,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer-indexof-polyfill@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c" - integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A== - buffer-xor@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" @@ -1869,11 +1952,6 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -buffers@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" - integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ== - builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -1985,13 +2063,6 @@ chai@^4.1.2: pathval "^1.1.1" type-detect "^4.0.5" -chainsaw@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" - integrity sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ== - dependencies: - traverse ">=0.3.0 <0.4" - chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -2101,6 +2172,11 @@ circular-json@^0.3.1: resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A== +cjs-module-lexer@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" + integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -2725,17 +2801,17 @@ detect-libc@^2.0.0: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== -diagnostic-channel-publishers@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.5.tgz#df8c317086c50f5727fdfb5d2fce214d2e4130ae" - integrity sha512-dJwUS0915pkjjimPJVDnS/QQHsH0aOYhnZsLJdnZIMOrB+csj8RnZhWTuwnm8R5v3Z7OZs+ksv5luC14DGB7eg== +diagnostic-channel-publishers@1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.7.tgz#9b7f8d5ee1295481aee19c827d917e96fedf2c4a" + integrity sha512-SEECbY5AiVt6DfLkhkaHNeshg1CogdLLANA8xlG/TKvS+XUgvIKl7VspJGYiEdL5OUyzMVnr7o0AwB7f+/Mjtg== -diagnostic-channel@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-1.1.0.tgz#6985e9dfedfbc072d91dc4388477e4087147756e" - integrity sha512-fwujyMe1gj6rk6dYi9hMZm0c8Mz8NDMVl2LB4iaYh3+LIAThZC8RKFGXWG0IML2OxAit/ZFRgZhMkhQ3d/bobQ== +diagnostic-channel@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-1.1.1.tgz#44b60972de9ee055c16216535b0e9db3f6a0efd0" + integrity sha512-r2HV5qFkUICyoaKlBEpLKHjxMXATUf/l+h8UZPGBHGLy4DDiY2sOLcIctax4eRnTw5wH2jTMExLntGPJ8eOJxw== dependencies: - semver "^5.3.0" + semver "^7.5.3" diff-match-patch@^1.0.0: version "1.0.5" @@ -2844,13 +2920,6 @@ download@^8.0.0: p-event "^2.1.0" pify "^4.0.1" -duplexer2@~0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" - integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA== - dependencies: - readable-stream "^2.0.2" - duplexer3@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" @@ -3793,16 +3862,6 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== -fstream@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" - integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== - dependencies: - graceful-fs "^4.1.2" - inherits "~2.0.0" - mkdirp ">=0.5 0" - rimraf "2" - function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -4073,7 +4132,7 @@ got@^8.3.1: url-parse-lax "^3.0.0" url-to-options "^1.0.1" -graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== @@ -4363,6 +4422,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -4371,6 +4435,16 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" +import-in-the-middle@1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz#2a266676e3495e72c04bbaa5ec14756ba168391b" + integrity sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw== + dependencies: + acorn "^8.8.2" + acorn-import-assertions "^1.9.0" + cjs-module-lexer "^1.2.2" + module-details-from-path "^1.0.3" + import-local@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" @@ -4397,7 +4471,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -5039,6 +5113,16 @@ jsonfile@^6.0.1: array-includes "^3.1.5" object.assign "^4.1.3" +jszip@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" + integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + setimmediate "^1.0.5" + just-debounce@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.1.0.tgz#2f81a3ad4121a76bc7cb45dbf704c0d76a8e5ddf" @@ -5142,6 +5226,13 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + liftoff@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3" @@ -5163,11 +5254,6 @@ linkify-it@^3.0.1: dependencies: uc.micro "^1.0.1" -listenercount@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" - integrity sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ== - load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -5514,7 +5600,7 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.6: +mkdirp@^0.5.0, mkdirp@^0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -5575,6 +5661,11 @@ mocha@^9.2.2: yargs-parser "20.2.4" yargs-unparser "2.0.0" +module-details-from-path@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" + integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A== + mrmime@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" @@ -6155,7 +6246,7 @@ package-hash@^4.0.0: lodash.flattendeep "^4.4.0" release-zalgo "^1.0.0" -pako@~1.0.5: +pako@~1.0.2, pako@~1.0.5: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== @@ -6768,6 +6859,15 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +require-in-the-middle@^7.1.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz#b539de8f00955444dc8aed95e17c69b0a4f10fcf" + integrity sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw== + dependencies: + debug "^4.1.1" + module-details-from-path "^1.0.3" + resolve "^1.22.1" + require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" @@ -6864,13 +6964,6 @@ rewiremock@^3.13.0: wipe-node-cache "^2.1.2" wipe-webpack-cache "^2.1.0" -rimraf@2: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -6981,7 +7074,7 @@ semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: dependencies: lru-cache "^6.0.0" -semver@^7.3.7, semver@^7.5.2: +semver@^7.3.7, semver@^7.5.1, semver@^7.5.2, semver@^7.5.3: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -7017,7 +7110,7 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" -setimmediate@^1.0.4, setimmediate@~1.0.4: +setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== @@ -7061,7 +7154,7 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shimmer@^1.1.0, shimmer@^1.2.0: +shimmer@^1.1.0, shimmer@^1.2.0, shimmer@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== @@ -7735,11 +7828,6 @@ totalist@^1.0.0: resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== -"traverse@>=0.3.0 <0.4": - version "0.3.9" - resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" - integrity sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ== - trim-repeated@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" @@ -8017,22 +8105,6 @@ untildify@^4.0.0: resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== -unzipper@^0.10.11: - version "0.10.11" - resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.11.tgz#0b4991446472cbdb92ee7403909f26c2419c782e" - integrity sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw== - dependencies: - big-integer "^1.6.17" - binary "~0.3.0" - bluebird "~3.4.1" - buffer-indexof-polyfill "~1.0.0" - duplexer2 "~0.1.4" - fstream "^1.0.12" - graceful-fs "^4.2.2" - listenercount "~1.0.1" - readable-stream "~2.3.6" - setimmediate "~1.0.4" - upath@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"