diff --git a/.github/release_plan.md b/.github/release_plan.md index 540e141b56f1..295c8f733cdd 100644 --- a/.github/release_plan.md +++ b/.github/release_plan.md @@ -19,7 +19,7 @@ - [ ] Touch up news entries (e.g. add missing periods) - [ ] Check the Markdown rendering to make sure everything looks good - [ ] Add any relevant news entries for `debugpy` and the language server if they were updated - - [ ] Update [`ThirdPartyNotices-Distribution.txt`](https://github.com/Microsoft/vscode-python/blob/main/ThirdPartyNotices-Distribution.txt) by using https://tools.opensource.microsoft.com/notice (Notes for this process are in the Team OneNote under Python VS Code -> Dev Process -> Third-Party Notices / TPN file) + - [ ] Update [`ThirdPartyNotices-Distribution.txt`](https://github.com/Microsoft/vscode-python/blob/main/ThirdPartyNotices-Distribution.txt) by using https://tools.opensource.microsoft.com/notice (Notes for this process are in the Team OneNote under Python VS Code → Dev Process → Third-Party Notices / TPN file) - [ ] Update [`ThirdPartyNotices-Repository.txt`](https://github.com/Microsoft/vscode-python/blob/main/ThirdPartyNotices-Repository.txt) as appropriate. This file is manually edited so you can check with the teams if anything needs to be added here. - [ ] Create a pull request against `main` (🤖) - [ ] Merge pull request into `main` @@ -33,10 +33,10 @@ - [ ] Create a pull request against `main` - [ ] Merge pull request into `main` - [ ] Announce the code freeze is over on the same channels -- [ ] Update [Component Governance](https://dev.azure.com/ms/vscode-python/_componentGovernance) (Click on "microsoft/vscode-python" on that page). Notes are in the OneNote under Python VS Code -> Dev Process -> Component Governance. - - [ ] Provide details for any automatically detected npm dependencies - - [ ] Manually add any repository dependencies -- [ ] GDPR bookkeeping (@brettcannon) (🤖; Notes in OneNote under Python VS Code -> Dev Process -> GDPR) +- [ ] Update Component Governance (Notes are in the team OneNote under Python VS Code → Dev Process → Component Governance). + - [ ] Make sure there are no active alerts + - [ ] Manually add any repository/embedded/CG-incompatible dependencies +- [ ] GDPR bookkeeping (@brettcannon) (🤖; Notes in OneNote under Python VS Code → Dev Process → GDPR) - [ ] Open appropriate [documentation issues](https://github.com/microsoft/vscode-docs/issues?q=is%3Aissue+is%3Aopen+label%3Apython) - new features - settings changes diff --git a/.github/workflows/assignIssue.yml b/.github/workflows/assignIssue.yml new file mode 100644 index 000000000000..d5de296921e1 --- /dev/null +++ b/.github/workflows/assignIssue.yml @@ -0,0 +1,123 @@ +name: Assign DS issue to someone +on: + issues: + types: [opened] +jobs: + assignIssue: + name: Assign Issue to Someone + runs-on: ubuntu-latest + if: github.repository == 'microsoft/vscode-python' + steps: + - name: Created internally + id: internal + env: + ISSUE_OWNER: ${{github.event.issue.owner.login}} + run: | + echo ::set-output name=result::$(node -p -e "['rchiodo', 'greazer', 'joyceerhl', 'DavidKutu', 'claudiaregio', 'IanMatthewHuff', 'DonJayamanne'].filter(item => process.env.ISSUE_OWNER.toLowerCase() === item.toLowerCase()).length > 0 ? 1 : 0") + shell: bash + - name: Should we proceed + id: proceed + env: + ISSUE_LABELS: ${{toJson(github.event.issue.labels)}} + ISSUE_ASSIGNEES: ${{toJson(github.event.issue.assignees)}} + ISSUE_IS_INTERNAL: ${{steps.internal.outputs.result}} + run: | + echo ::set-output name=result::$(node -p -e "process.env.ISSUE_IS_INTERNAL === '0' && JSON.parse(process.env.ISSUE_ASSIGNEES).length === 0 && JSON.parse(process.env.ISSUE_LABELS).filter(item => item.name.indexOf('data science') >= 0).length === 1 ? 1 : 0") + shell: bash + - uses: actions/checkout@v2 + if: steps.proceed.outputs.result == 1 + - name: Day of week + if: steps.proceed.outputs.result == 1 + id: day + run: | + echo ::set-output name=number::$(node -p -e "new Date().getDay()") + shell: bash + - name: Hour of day + if: steps.proceed.outputs.result == 1 + id: hour + run: | + echo ::set-output name=hour::$(node -p -e "(new Date().getUTCHours() - 7)%24") + shell: bash + - name: Week Number + if: steps.proceed.outputs.result == 1 + id: week + run: | + echo ::set-output name=odd::$(node .github/workflows/week.js) + shell: bash + - name: Print day and week + if: steps.proceed.outputs.result == 1 + run: | + echo ${{steps.day.outputs.number}} + echo ${{steps.week.outputs.odd}} + echo ${{steps.hour.outputs.hour}} + shell: bash + - name: Even late friday (David) + if: steps.proceed.outputs.result == 1 && steps.week.outputs.odd == 0 && steps.day.outputs.number == 5 && steps.hour.outputs.hour >= 16 + uses: actions/github@v1.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: assign DavidKutu + - name: Odd late friday (Joyce) + if: steps.proceed.outputs.result == 1 && steps.week.outputs.odd == 1 && steps.day.outputs.number == 5 && steps.hour.outputs.hour >= 16 + uses: actions/github@v1.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: assign joyceerhl + - name: Odd weekends (David) + if: steps.proceed.outputs.result == 1 && steps.week.outputs.odd == 1 && (steps.day.outputs.number == 6 || steps.day.outputs.number == 0) + uses: actions/github@v1.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: assign DavidKutu + - name: Even weekends (Joyce) + if: steps.proceed.outputs.result == 1 && steps.week.outputs.odd == 0 && (steps.day.outputs.number == 6 || steps.day.outputs.number == 0) + uses: actions/github@v1.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: assign joyceerhl + - name: Odd Monday (David) + if: steps.proceed.outputs.result == 1 && steps.week.outputs.odd == 1 && steps.day.outputs.number == 1 && steps.hour.outputs.hour < 16 + uses: actions/github@v1.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: assign DavidKutu + - name: Even Monday (Joyce) + if: steps.proceed.outputs.result == 1 && steps.week.outputs.odd == 0 && steps.day.outputs.number == 1 && steps.hour.outputs.hour < 16 + uses: actions/github@v1.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: assign joyceerhl + - name: Tuesday (Ian) + if: steps.proceed.outputs.result == 1 && (steps.day.outputs.number == 1 && steps.hour.outputs.hour >= 16) || (steps.day.outputs.number == 2 && steps.hour.outputs.hour < 16) + uses: actions/github@v1.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: assign IanMatthewHuff + - name: Wednesday (Rich) + if: steps.proceed.outputs.result == 1 && (steps.day.outputs.number == 2 && steps.hour.outputs.hour >= 16) || (steps.day.outputs.number == 3 && steps.hour.outputs.hour < 16) + uses: actions/github@v1.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: assign rchiodo + - name: Thursday (Don) + if: steps.proceed.outputs.result == 1 && (steps.day.outputs.number == 3 && steps.hour.outputs.hour >= 16) || (steps.day.outputs.number == 4 && steps.hour.outputs.hour < 16) + uses: actions/github@v1.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: assign DonJayamanne + - name: Friday (Claudia) + if: steps.proceed.outputs.result == 1 && (steps.day.outputs.number == 4 && steps.hour.outputs.hour >= 16) || (steps.day.outputs.number == 5 && steps.hour.outputs.hour < 16) + uses: actions/github@v1.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: assign claudiaregio diff --git a/.github/workflows/week.js b/.github/workflows/week.js new file mode 100644 index 000000000000..b8a11013283b --- /dev/null +++ b/.github/workflows/week.js @@ -0,0 +1,29 @@ +/* For a given date, get the ISO week number + * + * Based on information at: + * + * http://www.merlyn.demon.co.uk/weekcalc.htm#WNR + * + * Algorithm is to find nearest thursday, it's year + * is the year of the week number. Then get weeks + * between that date and the first day of that year. + * + * Note that dates in one year can be weeks of previous + * or next year, overlap is up to 3 days. + * + * e.g. 2014/12/29 is Monday in week 1 of 2015 + * 2012/1/1 is Sunday in week 52 of 2011 + */ +function getWeekNumber(d) { + // Copy date so don't modify original + d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())); + // Set to nearest Thursday: current date + 4 - current day number + // Make Sunday's day number 7 + d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7)); + // Get first day of year + var yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + // Calculate full weeks to nearest Thursday + return Math.ceil(((d - yearStart) / 86400000 + 1) / 7); +} +// Whether it is an odd or event week. +console.log(getWeekNumber(new Date()) % 2); diff --git a/.vscode/launch.json b/.vscode/launch.json index b19e0297f349..81757ac7977e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -152,6 +152,7 @@ "skipFiles": ["/**"] }, { + "name": "Tests (Multiroot, VS Code, *.test.ts)", "type": "extensionHost", "request": "launch", diff --git a/CHANGELOG.md b/CHANGELOG.md index dc0ec9e5af07..b1fb41aa0ef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,68 @@ # Changelog +## 2020.9.1 (29 September 2020) + +### Fixes + +1. Fix IPyKernel install issue with windows paths. + ([#13493](https://github.com/microsoft/vscode-python/issues/13493)) +1. Fix escaping of output to encode HTML chars correctly. + ([#5678](https://github.com/Microsoft/vscode-python/issues/5678)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [Pylance](https://github.com/microsoft/pylance-release) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + ## 2020.9.0 (23 September 2020) ### Enhancements diff --git a/news/2 Fixes/14169.md b/news/2 Fixes/14169.md new file mode 100644 index 000000000000..5588237a9faf --- /dev/null +++ b/news/2 Fixes/14169.md @@ -0,0 +1 @@ +Support nbconvert version 6+ for exporting notebooks to python code. \ No newline at end of file diff --git a/news/2 Fixes/14182.md b/news/2 Fixes/14182.md new file mode 100644 index 000000000000..53103b73ee62 --- /dev/null +++ b/news/2 Fixes/14182.md @@ -0,0 +1 @@ +Do not escape output in the actual ipynb file. \ No newline at end of file diff --git a/news/requirements.txt b/news/requirements.txt index 2656a4bd16de..2fdb54237f62 100644 --- a/news/requirements.txt +++ b/news/requirements.txt @@ -5,13 +5,12 @@ # pip-compile # attrs==19.3.0 # via pytest -docopt==0.6.2 # via -r news/requirements.in +docopt==0.6.2 # via -r requirements.in iniconfig==1.0.1 # via pytest -more-itertools==8.0.0 # via pytest packaging==20.4 # via pytest pluggy==0.13.1 # via pytest py==1.9.0 # via pytest pyparsing==2.4.5 # via packaging -pytest==6.0.1 # via -r news/requirements.in +pytest==6.1.0 # via -r requirements.in six==1.13.0 # via packaging toml==0.10.1 # via pytest diff --git a/package-lock.json b/package-lock.json index 16469d045b25..cdd4fdf790e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2574,9 +2574,9 @@ } }, "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" }, "ws": { "version": "7.2.0", @@ -6250,9 +6250,9 @@ "dev": true }, "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", "dev": true, "requires": { "readable-stream": "^2.3.5", @@ -6266,9 +6266,9 @@ "dev": true }, "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, "requires": { "core-util-is": "~1.0.0", @@ -17879,9 +17879,9 @@ } }, "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" }, "node-gyp-build": { "version": "4.2.1", @@ -26689,9 +26689,9 @@ } }, "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", "dev": true } } diff --git a/package.json b/package.json index 50eec096d24d..f5e116b61eed 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "onCommand:python.resetInterpreterSecurityStorage", "onCommand:python.startPage.open", "onCommand:python.enableSourceMapSupport", - "onNotebookEditor:jupyter-notebook", + "onNotebook:jupyter-notebook", "workspaceContains:mspythonconfig.json", "workspaceContains:pyproject.toml", "onCustomEditor:ms-python.python.notebook.ipynb" @@ -2007,7 +2007,7 @@ "md5": "^2.2.1", "minimatch": "^3.0.4", "named-js-regexp": "^1.3.3", - "node-fetch": "^2.6.0", + "node-fetch": "^2.6.1", "node-stream-zip": "^1.6.0", "onigasm": "^2.2.2", "pdfkit": "^0.11.0", diff --git a/src/client/common/utils/misc.ts b/src/client/common/utils/misc.ts index 8c6783b78c9f..0a1186b6da0a 100644 --- a/src/client/common/utils/misc.ts +++ b/src/client/common/utils/misc.ts @@ -130,6 +130,53 @@ export function isUri(resource?: Uri | any): resource is Uri { return typeof uri.path === 'string' && typeof uri.scheme === 'string'; } +/** + * Create a filter func that determine if the given URI and candidate match. + * + * The scheme must match, as well as path. + * + * @param checkParent - if `true`, match if the candidate is rooted under `uri` + * @param checkChild - if `true`, match if `uri` is rooted under the candidate + * @param checkExact - if `true`, match if the candidate matches `uri` exactly + */ +export function getURIFilter( + uri: Uri, + opts: { + checkParent?: boolean; + checkChild?: boolean; + checkExact?: boolean; + } = { checkExact: true } +): (u: Uri) => boolean { + let uriPath = uri.path; + while (uri.path.endsWith('/')) { + uriPath = uriPath.slice(0, -1); + } + const uriRoot = `${uriPath}/`; + function filter(candidate: Uri): boolean { + if (candidate.scheme !== uri.scheme) { + return false; + } + let candidatePath = candidate.path; + while (candidate.path.endsWith('/')) { + candidatePath = candidatePath.slice(0, -1); + } + if (opts.checkExact && candidatePath === uriPath) { + return true; + } + if (opts.checkParent && candidatePath.startsWith(uriRoot)) { + return true; + } + if (opts.checkChild) { + const candidateRoot = `{candidatePath}/`; + if (uriPath.startsWith(candidateRoot)) { + return true; + } + } + return false; + } + return filter; +} + export function isNotebookCell(documentOrUri: TextDocument | Uri): boolean { const uri = isUri(documentOrUri) ? documentOrUri : documentOrUri.uri; return uri.scheme.includes(NotebookCellScheme); diff --git a/src/client/pythonEnvironments/base/envsCache.ts b/src/client/pythonEnvironments/base/envsCache.ts new file mode 100644 index 000000000000..f0de585b3eff --- /dev/null +++ b/src/client/pythonEnvironments/base/envsCache.ts @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { cloneDeep } from 'lodash'; +import { getGlobalPersistentStore, IPersistentStore } from '../common/externalDependencies'; +import { PythonEnvInfo } from './info'; +import { areSameEnv } from './info/env'; + +/** + * Represents the environment info cache to be used by the cache locator. + */ +export interface IEnvsCache { + /** + * Initialization logic to be done outside of the constructor, for example reading from persistent storage. + */ + initialize(): void; + + /** + * Return all environment info currently in memory for this session. + * + * @return An array of cached environment info, or `undefined` if there are none. + */ + getAllEnvs(): PythonEnvInfo[] | undefined; + + /** + * Replace all environment info currently in memory for this session. + * + * @param envs The array of environment info to store in the in-memory cache. + */ + setAllEnvs(envs: PythonEnvInfo[]): void; + + /** + * If the cache has been initialized, return environmnent info objects that match a query object. + * If none of the environments in the cache match the query data, return an empty array. + * If the in-memory cache has not been initialized prior to calling `filterEnvs`, return `undefined`. + * + * @param env The environment info data that will be used to look for + * environment info objects in the cache, or a unique environment key. + * If passing an environment info object, it may contain incomplete environment info. + * @return The environment info objects matching the `env` param, + * or `undefined` if the in-memory cache is not initialized. + */ + filterEnvs(env: PythonEnvInfo | string): PythonEnvInfo[] | undefined; + + /** + * Writes the content of the in-memory cache to persistent storage. + */ + flush(): Promise; +} + +type CompleteEnvInfoFunction = (envInfo: PythonEnvInfo) => boolean; + +/** + * Environment info cache using persistent storage to save and retrieve pre-cached env info. + */ +export class PythonEnvInfoCache implements IEnvsCache { + private initialized = false; + + private envsList: PythonEnvInfo[] | undefined; + + private persistentStorage: IPersistentStore | undefined; + + constructor(private readonly isComplete: CompleteEnvInfoFunction) {} + + public initialize(): void { + if (this.initialized) { + return; + } + + this.initialized = true; + this.persistentStorage = getGlobalPersistentStore('PYTHON_ENV_INFO_CACHE'); + this.envsList = this.persistentStorage?.get(); + } + + public getAllEnvs(): PythonEnvInfo[] | undefined { + return cloneDeep(this.envsList); + } + + public setAllEnvs(envs: PythonEnvInfo[]): void { + this.envsList = cloneDeep(envs); + } + + public filterEnvs(env: PythonEnvInfo | string): PythonEnvInfo[] | undefined { + const result = this.envsList?.filter((info) => areSameEnv(info, env)); + + if (result) { + return cloneDeep(result); + } + + return undefined; + } + + public async flush(): Promise { + const completeEnvs = this.envsList?.filter(this.isComplete); + + if (completeEnvs?.length) { + await this.persistentStorage?.set(completeEnvs); + } + } +} diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts new file mode 100644 index 000000000000..bb72186ca7a5 --- /dev/null +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -0,0 +1,350 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { cloneDeep } from 'lodash'; +import * as path from 'path'; +import { Architecture } from '../../../common/utils/platform'; +import { arePathsSame } from '../../common/externalDependencies'; +import { areEqualVersions, areEquivalentVersions } from './pythonVersion'; + +import { + FileInfo, + PythonDistroInfo, + PythonEnvInfo, + PythonEnvKind, + PythonReleaseLevel, + PythonVersion, +} from '.'; + +/** + * Create a new info object with all values empty. + * + * @param init - if provided, these values are applied to the new object + */ +export function buildEnvInfo(init?: { + kind?: PythonEnvKind; + executable?: string; + location?: string; + version?: PythonVersion; +}): PythonEnvInfo { + const env = { + kind: PythonEnvKind.Unknown, + executable: { + filename: '', + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + name: '', + location: '', + version: { + major: -1, + minor: -1, + micro: -1, + release: { + level: PythonReleaseLevel.Final, + serial: 0, + }, + }, + arch: Architecture.Unknown, + distro: { + org: '', + }, + }; + if (init !== undefined) { + updateEnv(env, init); + } + return env; +} + +/** + * Return a deep copy of the given env info. + * + * @param updates - if provided, these values are applied to the copy + */ +export function copyEnvInfo( + env: PythonEnvInfo, + updates?: { + kind?: PythonEnvKind, + }, +): PythonEnvInfo { + // We don't care whether or not extra/hidden properties + // get preserved, so we do the easy thing here. + const copied = cloneDeep(env); + if (updates !== undefined) { + updateEnv(copied, updates); + } + return copied; +} + +function updateEnv(env: PythonEnvInfo, updates: { + kind?: PythonEnvKind; + executable?: string; + location?: string; + version?: PythonVersion; +}): void { + if (updates.kind !== undefined) { + env.kind = updates.kind; + } + if (updates.executable !== undefined) { + env.executable.filename = updates.executable; + } + if (updates.location !== undefined) { + env.location = updates.location; + } + if (updates.version !== undefined) { + env.version = updates.version; + } +} + +/** + * For the given data, build a normalized partial info object. + * + * If insufficient data is provided to generate a minimal object, such + * that it is not identifiable, then `undefined` is returned. + */ +export function getMinimalPartialInfo(env: string | Partial): Partial | undefined { + if (typeof env === 'string') { + if (env === '') { + return undefined; + } + return { + executable: { filename: env, sysPrefix: '', ctime: -1, mtime: -1 }, + }; + } + if (env.executable === undefined) { + return undefined; + } + if (env.executable.filename === '') { + return undefined; + } + return env; +} + +/** + * Checks if two environments are same. + * @param {string | PythonEnvInfo} left: environment to compare. + * @param {string | PythonEnvInfo} right: environment to compare. + * @param {boolean} allowPartialMatch: allow partial matches of properties when comparing. + * + * Remarks: The current comparison assumes that if the path to the executables are the same + * then it is the same environment. Additionally, if the paths are not same but executables + * are in the same directory and the version of python is the same than we can assume it + * to be same environment. This later case is needed for comparing windows store python, + * where multiple versions of python executables are all put in the same directory. + */ +export function areSameEnv( + left: string | Partial, + right: string | Partial, + allowPartialMatch?: boolean, +): boolean | undefined { + const leftInfo = getMinimalPartialInfo(left); + const rightInfo = getMinimalPartialInfo(right); + if (leftInfo === undefined || rightInfo === undefined) { + return undefined; + } + const leftFilename = leftInfo.executable!.filename; + const rightFilename = rightInfo.executable!.filename; + + // For now we assume that matching executable means they are the same. + if (arePathsSame(leftFilename, rightFilename)) { + return true; + } + + if (arePathsSame(path.dirname(leftFilename), path.dirname(rightFilename))) { + const leftVersion = typeof left === 'string' ? undefined : left.version; + const rightVersion = typeof right === 'string' ? undefined : right.version; + if (leftVersion && rightVersion) { + if ( + areEqualVersions(leftVersion, rightVersion) + || (allowPartialMatch && areEquivalentVersions(leftVersion, rightVersion)) + ) { + return true; + } + } + } + return false; +} + +/** + * Returns a heuristic value on how much information is available in the given version object. + * @param {PythonVersion} version version object to generate heuristic from. + * @returns A heuristic value indicating the amount of info available in the object + * weighted by most important to least important fields. + * Wn > Wn-1 + Wn-2 + ... W0 + */ +function getPythonVersionInfoHeuristic(version:PythonVersion): number { + let infoLevel = 0; + if (version.major > 0) { + infoLevel += 20; // W4 + } + + if (version.minor >= 0) { + infoLevel += 10; // W3 + } + + if (version.micro >= 0) { + infoLevel += 5; // W2 + } + + if (version.release?.level) { + infoLevel += 3; // W1 + } + + if (version.release?.serial || version.sysVersion) { + infoLevel += 1; // W0 + } + + return infoLevel; +} + +/** + * Returns a heuristic value on how much information is available in the given executable object. + * @param {FileInfo} executable executable object to generate heuristic from. + * @returns A heuristic value indicating the amount of info available in the object + * weighted by most important to least important fields. + * Wn > Wn-1 + Wn-2 + ... W0 + */ +function getFileInfoHeuristic(file:FileInfo): number { + let infoLevel = 0; + if (file.filename.length > 0) { + infoLevel += 5; // W2 + } + + if (file.mtime) { + infoLevel += 2; // W1 + } + + if (file.ctime) { + infoLevel += 1; // W0 + } + + return infoLevel; +} + +/** + * Returns a heuristic value on how much information is available in the given distro object. + * @param {PythonDistroInfo} distro distro object to generate heuristic from. + * @returns A heuristic value indicating the amount of info available in the object + * weighted by most important to least important fields. + * Wn > Wn-1 + Wn-2 + ... W0 + */ +function getDistroInfoHeuristic(distro:PythonDistroInfo):number { + let infoLevel = 0; + if (distro.org.length > 0) { + infoLevel += 20; // W3 + } + + if (distro.defaultDisplayName) { + infoLevel += 10; // W2 + } + + if (distro.binDir) { + infoLevel += 5; // W1 + } + + if (distro.version) { + infoLevel += 2; + } + + return infoLevel; +} + +/** + * Gets a prioritized list of environment types for identification. + * @returns {PythonEnvKind[]} : List of environments ordered by identification priority + * + * Remarks: This is the order of detection based on how the various distributions and tools + * configure the environment, and the fall back for identification. + * Top level we have the following environment types, since they leave a unique signature + * in the environment or * use a unique path for the environments they create. + * 1. Conda + * 2. Windows Store + * 3. PipEnv + * 4. Pyenv + * 5. Poetry + * + * Next level we have the following virtual environment tools. The are here because they + * are consumed by the tools above, and can also be used independently. + * 1. venv + * 2. virtualenvwrapper + * 3. virtualenv + * + * Last category is globally installed python, or system python. + */ +export function getPrioritizedEnvironmentKind(): PythonEnvKind[] { + return [ + PythonEnvKind.CondaBase, + PythonEnvKind.Conda, + PythonEnvKind.WindowsStore, + PythonEnvKind.Pipenv, + PythonEnvKind.Pyenv, + PythonEnvKind.Poetry, + PythonEnvKind.Venv, + PythonEnvKind.VirtualEnvWrapper, + PythonEnvKind.VirtualEnv, + PythonEnvKind.OtherVirtual, + PythonEnvKind.OtherGlobal, + PythonEnvKind.MacDefault, + PythonEnvKind.System, + PythonEnvKind.Custom, + PythonEnvKind.Unknown, + ]; +} + +/** + * Selects an environment based on the environment selection priority. This should + * match the priority in the environment identifier. + */ +export function sortEnvInfoByPriority(...envs: PythonEnvInfo[]): PythonEnvInfo[] { + // tslint:disable-next-line: no-suspicious-comment + // TODO: When we consolidate the PythonEnvKind and EnvironmentType we should have + // one location where we define priority and + const envKindByPriority:PythonEnvKind[] = getPrioritizedEnvironmentKind(); + return envs.sort( + (a:PythonEnvInfo, b:PythonEnvInfo) => envKindByPriority.indexOf(a.kind) - envKindByPriority.indexOf(b.kind), + ); +} + +/** + * Merges properties of the `target` environment and `other` environment and returns the merged environment. + * if the value in the `target` environment is not defined or has less information. This does not mutate + * the `target` instead it returns a new object that contains the merged results. + * @param {PythonEnvInfo} target : Properties of this object are favored. + * @param {PythonEnvInfo} other : Properties of this object are used to fill the gaps in the merged result. + */ +export function mergeEnvironments(target: PythonEnvInfo, other: PythonEnvInfo): PythonEnvInfo { + const merged = cloneDeep(target); + + const version = cloneDeep( + getPythonVersionInfoHeuristic(target.version) > getPythonVersionInfoHeuristic(other.version) + ? target.version : other.version, + ); + + const executable = cloneDeep( + getFileInfoHeuristic(target.executable) > getFileInfoHeuristic(other.executable) + ? target.executable : other.executable, + ); + executable.sysPrefix = target.executable.sysPrefix ?? other.executable.sysPrefix; + + const distro = cloneDeep( + getDistroInfoHeuristic(target.distro) > getDistroInfoHeuristic(other.distro) + ? target.distro : other.distro, + ); + + merged.arch = merged.arch === Architecture.Unknown ? other.arch : target.arch; + merged.defaultDisplayName = merged.defaultDisplayName ?? other.defaultDisplayName; + merged.distro = distro; + merged.executable = executable; + + // No need to check this just use preferred kind. Since the first thing we do is figure out the + // preferred env based on kind. + merged.kind = target.kind; + + merged.location = merged.location ?? other.location; + merged.name = merged.name ?? other.name; + merged.searchLocation = merged.searchLocation ?? other.searchLocation; + merged.version = version; + + return merged; +} diff --git a/src/client/pythonEnvironments/base/info/index.ts b/src/client/pythonEnvironments/base/info/index.ts index c24cf8dc2aa3..c623fca6b7e9 100644 --- a/src/client/pythonEnvironments/base/info/index.ts +++ b/src/client/pythonEnvironments/base/info/index.ts @@ -17,30 +17,38 @@ export enum PythonEnvKind { WindowsStore = 'global-windows-store', Pyenv = 'global-pyenv', CondaBase = 'global-conda-base', + Poetry = 'global-poetry', Custom = 'global-custom', OtherGlobal = 'global-other', // "virtual" Venv = 'virt-venv', VirtualEnv = 'virt-virtualenv', + VirtualEnvWrapper = 'virt-virtualenvwrapper', Pipenv = 'virt-pipenv', Conda = 'virt-conda', OtherVirtual = 'virt-other' } /** - * Information about a Python binary/executable. + * A (system-global) unique ID for a single Python environment. + */ +export type PythonEnvID = string; + +/** + * Information about a file. */ -export type PythonExecutableInfo = { +export type FileInfo = { filename: string; - sysPrefix: string; ctime: number; mtime: number; }; /** - * A (system-global) unique ID for a single Python environment. + * Information about a Python binary/executable. */ -export type PythonEnvID = string; +export type PythonExecutableInfo = FileInfo & { + sysPrefix: string; +}; /** * The most fundamental information about a Python environment. @@ -56,7 +64,6 @@ export type PythonEnvID = string; * @prop location - the env's location (on disk), if relevant */ export type PythonEnvBaseInfo = { - id: PythonEnvID; kind: PythonEnvKind; executable: PythonExecutableInfo; // One of (name, location) must be non-empty. @@ -92,7 +99,7 @@ export type PythonVersionRelease = { * @prop sysVersion - the raw text from `sys.version` */ export type PythonVersion = BasicVersionInfo & { - release: PythonVersionRelease; + release?: PythonVersionRelease; sysVersion?: string; }; diff --git a/src/client/pythonEnvironments/base/info/pythonVersion.ts b/src/client/pythonEnvironments/base/info/pythonVersion.ts index 58248f2c1789..0e281eb4662c 100644 --- a/src/client/pythonEnvironments/base/info/pythonVersion.ts +++ b/src/client/pythonEnvironments/base/info/pythonVersion.ts @@ -1,9 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { PythonReleaseLevel, PythonVersion } from '.'; import { EMPTY_VERSION, parseBasicVersionInfo } from '../../../common/utils/version'; +import { PythonReleaseLevel, PythonVersion } from '.'; + +/** + * Convert the given string into the corresponding Python version object. + */ export function parseVersion(versionStr: string): PythonVersion { const parsed = parseBasicVersionInfo(versionStr); if (!parsed) { @@ -33,3 +37,34 @@ export function parseVersion(versionStr: string): PythonVersion { } return version; } + +/** + * Checks if all the fields in the version object match. + * @param {PythonVersion} left + * @param {PythonVersion} right + * @returns {boolean} + */ +export function areEqualVersions(left: PythonVersion, right:PythonVersion): boolean { + return left === right; +} + +/** + * Checks if major and minor version fields match. True here means that the python ABI is the + * same, but the micro version could be different. But for the purpose this is being used + * it does not matter. + * @param {PythonVersion} left + * @param {PythonVersion} right + * @returns {boolean} + */ +export function areEquivalentVersions(left: PythonVersion, right:PythonVersion): boolean { + if (left.major === 2 && right.major === 2) { + // We are going to assume that if the major version is 2 then the version is 2.7 + return true; + } + + // In the case of 3.* if major and minor match we assume that they are equivalent versions + return ( + left.major === right.major + && left.minor === right.minor + ); +} diff --git a/src/client/pythonEnvironments/base/locator.ts b/src/client/pythonEnvironments/base/locator.ts index 03eb206445bd..a1b372fb6988 100644 --- a/src/client/pythonEnvironments/base/locator.ts +++ b/src/client/pythonEnvironments/base/locator.ts @@ -15,17 +15,18 @@ import { * A single update to a previously provided Python env object. */ export type PythonEnvUpdatedEvent = { + /** + * The iteration index of The env info that was previously provided. + */ + index: number; /** * The env info that was previously provided. - * - * If the event comes from `IPythonEnvsIterator.onUpdated` then - * `old` was previously yielded during iteration. */ - old: PythonEnvInfo; + old?: PythonEnvInfo; /** * The env info that replaces the old info. */ - new: PythonEnvInfo; + update: PythonEnvInfo; }; /** @@ -73,23 +74,39 @@ export const NOOP_ITERATOR: IPythonEnvsIterator = iterEmpty(); * This is directly correlated with the `BasicPythonEnvsChangedEvent` * emitted by watchers. * - * @prop kinds - if provided, results should be limited to these env kinds + * @prop kinds - if provided, results should be limited to these env + * kinds; if not provided, the kind of each evnironment + * is not considered when filtering */ export type BasicPythonLocatorQuery = { kinds?: PythonEnvKind[]; }; +/** + * The portion of a query related to env search locations. + */ +export type SearchLocations = { + /** + * The locations under which to look for environments. + */ + roots: Uri[]; + /** + * If true, also look for environments that do not have a search location. + */ + includeNonRooted?: boolean; +}; + /** * The full set of possible info to send to a locator when requesting environments. * * This is directly correlated with the `PythonEnvsChangedEvent` * emitted by watchers. - * - * @prop - searchLocations - if provided, results should be limited to - * within these locations */ export type PythonLocatorQuery = BasicPythonLocatorQuery & { - searchLocations?: Uri[]; + /** + * If provided, results should be limited to within these locations. + */ + searchLocations?: SearchLocations; }; type QueryForEvent = E extends PythonEnvsChangedEvent ? PythonLocatorQuery : BasicPythonLocatorQuery; diff --git a/src/client/pythonEnvironments/base/locatorUtils.ts b/src/client/pythonEnvironments/base/locatorUtils.ts new file mode 100644 index 000000000000..b7c2e496809f --- /dev/null +++ b/src/client/pythonEnvironments/base/locatorUtils.ts @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { createDeferred } from '../../common/utils/async'; +import { getURIFilter } from '../../common/utils/misc'; +import { PythonEnvInfo } from './info'; +import { + IPythonEnvsIterator, + PythonEnvUpdatedEvent, + PythonLocatorQuery, +} from './locator'; + +/** + * Create a filter function to match the given query. + */ +export function getQueryFilter(query: PythonLocatorQuery): (env: PythonEnvInfo) => boolean { + const kinds = (query.kinds !== undefined && query.kinds.length > 0) + ? query.kinds + : undefined; + let includeNonRooted = true; + if (query.searchLocations !== undefined) { + if (query.searchLocations.includeNonRooted !== undefined) { + includeNonRooted = query.searchLocations.includeNonRooted; + } else { + // We default to `false`. + includeNonRooted = false; + } + } + const locationFilters = getSearchLocationFilters(query); + function checkKind(env: PythonEnvInfo): boolean { + if (kinds === undefined) { + return true; + } + return kinds.includes(env.kind); + } + function checkSearchLocation(env: PythonEnvInfo): boolean { + if (env.searchLocation === undefined) { + // It is not a "rooted" env. + return includeNonRooted; + } else { + // It is a "rooted" env. + const loc = env.searchLocation; + if (locationFilters !== undefined) { + // Check against the requested roots. (There may be none.) + return locationFilters.some((filter) => filter(loc)); + } + return true; + } + } + return (env) => { + if (!checkKind(env)) { + return false; + } + if (!checkSearchLocation(env)) { + return false; + } + return true; + }; +} + +function getSearchLocationFilters(query: PythonLocatorQuery): ((u: Uri) => boolean)[] | undefined { + if (query.searchLocations === undefined) { + return undefined; + } + if (query.searchLocations.roots.length === 0) { + return []; + } + return query.searchLocations.roots.map((loc) => getURIFilter(loc, { + checkParent: true, + checkExact: true, + })); +} + +/** + * Unroll the given iterator into an array. + * + * This includes applying any received updates. + */ +export async function getEnvs(iterator: IPythonEnvsIterator): Promise { + const envs: PythonEnvInfo[] = []; + + const updatesDone = createDeferred(); + if (iterator.onUpdated === undefined) { + updatesDone.resolve(); + } else { + iterator.onUpdated((event: PythonEnvUpdatedEvent | null) => { + if (event === null) { + updatesDone.resolve(); + return; + } + const oldEnv = envs[event.index]; + if (oldEnv === undefined) { + // XXX log or fail + } else { + envs[event.index] = event.update; + } + }); + } + + let result = await iterator.next(); + while (!result.done) { + envs.push(result.value); + // eslint-disable-next-line no-await-in-loop + result = await iterator.next(); + } + + await updatesDone.promise; + return envs; +} diff --git a/src/client/pythonEnvironments/base/locators/composite/environmentsReducer.ts b/src/client/pythonEnvironments/base/locators/composite/environmentsReducer.ts index ba00eac2a546..6c3a71444df8 100644 --- a/src/client/pythonEnvironments/base/locators/composite/environmentsReducer.ts +++ b/src/client/pythonEnvironments/base/locators/composite/environmentsReducer.ts @@ -5,7 +5,8 @@ import { cloneDeep, isEqual } from 'lodash'; import { Event, EventEmitter } from 'vscode'; import { traceVerbose } from '../../../../common/logger'; import { createDeferred } from '../../../../common/utils/async'; -import { areSameEnvironment, PythonEnvInfo, PythonEnvKind } from '../../info'; +import { PythonEnvInfo, PythonEnvKind } from '../../info'; +import { areSameEnv } from '../../info/env'; import { ILocator, IPythonEnvsIterator, PythonEnvUpdatedEvent, PythonLocatorQuery, } from '../../locator'; @@ -28,13 +29,13 @@ export class PythonEnvsReducer implements ILocator { iterator.onUpdated!((event) => { if (event === null) { waitForUpdatesDeferred.resolve(); - } else if (environment && areSameEnvironment(environment, event.new)) { - environment = event.new; + } else if (environment && areSameEnv(environment, event.update)) { + environment = event.update; } }); let result = await iterator.next(); while (!result.done) { - if (areSameEnvironment(result.value, env)) { + if (areSameEnv(result.value, env)) { environment = result.value; } // eslint-disable-next-line no-await-in-loop @@ -72,13 +73,13 @@ async function* iterEnvsIterator( state.done = true; checkIfFinishedAndNotify(state, didUpdate); } else { - const oldIndex = seen.findIndex((s) => areSameEnvironment(s, event.old)); - if (oldIndex !== -1) { + if (seen[event.index] !== undefined) { state.pending += 1; - resolveDifferencesInBackground(oldIndex, event.new, state, didUpdate, seen).ignoreErrors(); + resolveDifferencesInBackground(event.index, event.update, state, didUpdate, seen) + .ignoreErrors(); } else { // This implies a problem in a downstream locator - traceVerbose(`Expected already iterated env, got ${event.old}`); + traceVerbose(`Expected already iterated env, got ${event.old} (#${event.index})`); } } }); @@ -87,7 +88,7 @@ async function* iterEnvsIterator( let result = await iterator.next(); while (!result.done) { const currEnv = result.value; - const oldIndex = seen.findIndex((s) => areSameEnvironment(s, currEnv)); + const oldIndex = seen.findIndex((s) => areSameEnv(s, currEnv)); if (oldIndex !== -1) { state.pending += 1; resolveDifferencesInBackground(oldIndex, currEnv, state, didUpdate, seen).ignoreErrors(); @@ -115,8 +116,8 @@ async function resolveDifferencesInBackground( const oldEnv = seen[oldIndex]; const merged = mergeEnvironments(oldEnv, newEnv); if (!isEqual(oldEnv, merged)) { - didUpdate.fire({ old: oldEnv, new: merged }); seen[oldIndex] = merged; + didUpdate.fire({ index: oldIndex, old: oldEnv, update: merged }); } state.pending -= 1; checkIfFinishedAndNotify(state, didUpdate); diff --git a/src/client/pythonEnvironments/base/locators/composite/environmentsResolver.ts b/src/client/pythonEnvironments/base/locators/composite/environmentsResolver.ts index 347785f73660..305c9f22824f 100644 --- a/src/client/pythonEnvironments/base/locators/composite/environmentsResolver.ts +++ b/src/client/pythonEnvironments/base/locators/composite/environmentsResolver.ts @@ -5,7 +5,7 @@ import { cloneDeep } from 'lodash'; import { Event, EventEmitter } from 'vscode'; import { traceVerbose } from '../../../../common/logger'; import { IEnvironmentInfoService } from '../../../info/environmentInfoService'; -import { areSameEnvironment, PythonEnvInfo } from '../../info'; +import { PythonEnvInfo } from '../../info'; import { InterpreterInformation } from '../../info/interpreter'; import { ILocator, IPythonEnvsIterator, PythonEnvUpdatedEvent, PythonLocatorQuery, @@ -62,14 +62,14 @@ export class PythonEnvsResolver implements ILocator { state.done = true; checkIfFinishedAndNotify(state, didUpdate); } else { - const oldIndex = seen.findIndex((s) => areSameEnvironment(s, event.old)); - if (oldIndex !== -1) { - seen[oldIndex] = event.new; + if (seen[event.index] !== undefined) { + seen[event.index] = event.update; state.pending += 1; - this.resolveInBackground(oldIndex, state, didUpdate, seen).ignoreErrors(); + this.resolveInBackground(event.index, state, didUpdate, seen) + .ignoreErrors(); } else { // This implies a problem in a downstream locator - traceVerbose(`Expected already iterated env in resolver, got ${event.old}`); + traceVerbose(`Expected already iterated env, got ${event.old} (#${event.index})`); } } }); @@ -102,8 +102,9 @@ export class PythonEnvsResolver implements ILocator { ); if (interpreterInfo) { const resolvedEnv = getResolvedEnv(interpreterInfo, seen[envIndex]); - didUpdate.fire({ old: seen[envIndex], new: resolvedEnv }); + const old = seen[envIndex]; seen[envIndex] = resolvedEnv; + didUpdate.fire({ old, index: envIndex, update: resolvedEnv }); } state.pending -= 1; checkIfFinishedAndNotify(state, didUpdate); diff --git a/src/client/pythonEnvironments/common/environmentIdentifier.ts b/src/client/pythonEnvironments/common/environmentIdentifier.ts index b56c84e2b73e..3340cb01bdaf 100644 --- a/src/client/pythonEnvironments/common/environmentIdentifier.ts +++ b/src/client/pythonEnvironments/common/environmentIdentifier.ts @@ -11,9 +11,8 @@ import { isWindowsStoreEnvironment } from '../discovery/locators/services/window import { EnvironmentType } from '../info'; /** - * Returns environment type. - * @param {string} interpreterPath : Absolute path to the python interpreter binary. - * @returns {EnvironmentType} + * Gets a prioritized list of environment types for identification. + * @deprecated * * Remarks: This is the order of detection based on how the various distributions and tools * configure the environment, and the fall back for identification. @@ -33,36 +32,56 @@ import { EnvironmentType } from '../info'; * * Last category is globally installed python, or system python. */ -export async function identifyEnvironment(interpreterPath: string): Promise { - if (await isCondaEnvironment(interpreterPath)) { - return EnvironmentType.Conda; - } - - if (await isWindowsStoreEnvironment(interpreterPath)) { - return EnvironmentType.WindowsStore; - } - - if (await isPipenvEnvironment(interpreterPath)) { - return EnvironmentType.Pipenv; - } +export function getPrioritizedEnvironmentType():EnvironmentType[] { + return [ + EnvironmentType.Conda, + EnvironmentType.WindowsStore, + EnvironmentType.Pipenv, + EnvironmentType.Pyenv, + EnvironmentType.Poetry, + EnvironmentType.Venv, + EnvironmentType.VirtualEnvWrapper, + EnvironmentType.VirtualEnv, + EnvironmentType.Global, + EnvironmentType.System, + EnvironmentType.Unknown, + ]; +} - if (await isPyenvEnvironment(interpreterPath)) { - return EnvironmentType.Pyenv; - } +function getIdentifiers(): Map Promise> { + const notImplemented = () => Promise.resolve(false); + const defaultTrue = () => Promise.resolve(true); + const identifier: Map Promise> = new Map(); + Object.keys(EnvironmentType).forEach((k:string) => { + identifier.set(k as EnvironmentType, notImplemented); + }); - if (await isVenvEnvironment(interpreterPath)) { - return EnvironmentType.Venv; - } - - if (await isVirtualenvwrapperEnvironment(interpreterPath)) { - return EnvironmentType.VirtualEnvWrapper; - } + identifier.set(EnvironmentType.Conda, isCondaEnvironment); + identifier.set(EnvironmentType.WindowsStore, isWindowsStoreEnvironment); + identifier.set(EnvironmentType.Pipenv, isPipenvEnvironment); + identifier.set(EnvironmentType.Pyenv, isPyenvEnvironment); + identifier.set(EnvironmentType.Venv, isVenvEnvironment); + identifier.set(EnvironmentType.VirtualEnvWrapper, isVirtualenvwrapperEnvironment); + identifier.set(EnvironmentType.VirtualEnv, isVirtualenvEnvironment); + identifier.set(EnvironmentType.Unknown, defaultTrue); + return identifier; +} - if (await isVirtualenvEnvironment(interpreterPath)) { - return EnvironmentType.VirtualEnv; +/** + * Returns environment type. + * @param {string} interpreterPath : Absolute path to the python interpreter binary. + * @returns {EnvironmentType} + */ +export async function identifyEnvironment(interpreterPath: string): Promise { + const identifiers = getIdentifiers(); + const prioritizedEnvTypes = getPrioritizedEnvironmentType(); + // eslint-disable-next-line no-restricted-syntax + for (const e of prioritizedEnvTypes) { + const identifier = identifiers.get(e); + // eslint-disable-next-line no-await-in-loop + if (identifier && await identifier(interpreterPath)) { + return e; + } } - - // additional identifiers go here - return EnvironmentType.Unknown; } diff --git a/src/client/pythonEnvironments/common/externalDependencies.ts b/src/client/pythonEnvironments/common/externalDependencies.ts index 6fc3f400f8ab..dd44f69c224f 100644 --- a/src/client/pythonEnvironments/common/externalDependencies.ts +++ b/src/client/pythonEnvironments/common/externalDependencies.ts @@ -4,6 +4,7 @@ import * as fsapi from 'fs-extra'; import * as path from 'path'; import { ExecutionResult, IProcessServiceFactory } from '../../common/process/types'; +import { IPersistentStateFactory } from '../../common/types'; import { getOSType, OSType } from '../../common/utils/platform'; import { IServiceContainer } from '../../ioc/types'; @@ -37,3 +38,30 @@ export function arePathsSame(path1: string, path2: string): boolean { } return path1 === path2; } + +function getPersistentStateFactory(): IPersistentStateFactory { + return internalServiceContainer.get(IPersistentStateFactory); +} + +export interface IPersistentStore { + get(): T | undefined; + set(value: T): Promise; +} + +export function getGlobalPersistentStore(key: string): IPersistentStore { + const factory = getPersistentStateFactory(); + const state = factory.createGlobalPersistentState(key, undefined); + + return { + get() { return state.value; }, + set(value: T) { return state.updateValue(value); }, + }; +} + +export async function getFileInfo(filePath: string): Promise<{ctime:number, mtime:number}> { + const data = await fsapi.lstat(filePath); + return { + ctime: data.ctime.getUTCDate(), + mtime: data.mtime.getUTCDate(), + }; +} diff --git a/src/client/pythonEnvironments/discovery/locators/index.ts b/src/client/pythonEnvironments/discovery/locators/index.ts index 67f38e0fe769..49bec156a39b 100644 --- a/src/client/pythonEnvironments/discovery/locators/index.ts +++ b/src/client/pythonEnvironments/discovery/locators/index.ts @@ -9,6 +9,7 @@ import { traceDecorators } from '../../../common/logger'; import { IPlatformService } from '../../../common/platform/types'; import { IDisposableRegistry } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; +import { getURIFilter } from '../../../common/utils/misc'; import { OSType } from '../../../common/utils/platform'; import { CONDA_ENV_FILE_SERVICE, @@ -97,10 +98,18 @@ export class WorkspaceLocators extends Locator { } public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + if (query?.searchLocations === null) { + // Workspace envs all have searchLocation, so there's nothing to do. + return NOOP_ITERATOR; + } const iterators = Object.keys(this.locators).map((key) => { - if (query?.searchLocations) { + if (query?.searchLocations !== undefined) { const root = this.roots[key]; - if (!matchURI(root, ...query.searchLocations)) { + // Match any related search location. + const filter = getURIFilter(root, { checkParent: true, checkChild: true, checkExact: true }); + // Ignore any requests for global envs. + if (!query.searchLocations.roots.some(filter)) { + // This workspace folder did not match the query, so skip it! return NOOP_ITERATOR; } } @@ -168,19 +177,6 @@ export class WorkspaceLocators extends Locator { } } -/** - * Determine if the given URI matches one of the candidates. - * - * The scheme must match, as well as path. The path must match exactly - * or the URI must be a parent of one of the candidates. - */ -function matchURI(uri: Uri, ...candidates: Uri[]): boolean { - const uriPath = uri.path.endsWith('/') ? uri.path : '{uri.path}/'; - const matchedUri = candidates.find((candidate) => (candidate.scheme === uri.scheme) - && (candidate.path === uri.path || candidate.path.startsWith(uriPath))); - return matchedUri !== undefined; -} - // The parts of IComponentAdapter used here. interface IComponent { hasInterpreters: Promise; diff --git a/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts b/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts index 6eae9dac5126..14f8cb1fc482 100644 --- a/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts +++ b/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts @@ -4,7 +4,14 @@ import * as fsapi from 'fs-extra'; import * as path from 'path'; import { traceWarning } from '../../../../common/logger'; -import { getEnvironmentVariable } from '../../../../common/utils/platform'; +import { Architecture, getEnvironmentVariable } from '../../../../common/utils/platform'; +import { + PythonEnvInfo, PythonEnvKind, PythonReleaseLevel, PythonVersion, +} from '../../../base/info'; +import { parseVersion } from '../../../base/info/pythonVersion'; +import { ILocator, IPythonEnvsIterator } from '../../../base/locator'; +import { PythonEnvsWatcher } from '../../../base/watcher'; +import { getFileInfo } from '../../../common/externalDependencies'; import { isWindowsPythonExe } from '../../../common/windowsUtils'; /** @@ -107,5 +114,51 @@ export async function getWindowsStorePythonExes(): Promise { .filter(isWindowsPythonExe); } -// tslint:disable-next-line: no-suspicious-comment -// TODO: The above APIs will be consumed by the Windows Store locator class when we have it. +export class WindowsStoreLocator extends PythonEnvsWatcher implements ILocator { + private readonly kind:PythonEnvKind = PythonEnvKind.WindowsStore; + + public iterEnvs(): IPythonEnvsIterator { + const buildEnvInfo = (exe:string) => this.buildEnvInfo(exe); + const iterator = async function* () { + const exes = await getWindowsStorePythonExes(); + yield* exes.map(buildEnvInfo); + }; + return iterator(); + } + + public async resolveEnv(env: string | PythonEnvInfo): Promise { + const executablePath = typeof env === 'string' ? env : env.executable.filename; + if (await isWindowsStoreEnvironment(executablePath)) { + return this.buildEnvInfo(executablePath); + } + return undefined; + } + + private async buildEnvInfo(exe:string): Promise { + let version:PythonVersion; + try { + version = parseVersion(path.basename(exe)); + } catch (e) { + version = { + major: 3, + minor: -1, + micro: -1, + release: { level: PythonReleaseLevel.Final, serial: -1 }, + sysVersion: undefined, + }; + } + return { + name: '', + location: '', + kind: this.kind, + executable: { + filename: exe, + sysPrefix: '', + ...(await getFileInfo(exe)), + }, + version, + arch: Architecture.x64, + distro: { org: 'Microsoft' }, + }; + } +} diff --git a/src/client/pythonEnvironments/index.ts b/src/client/pythonEnvironments/index.ts index d737eb255562..d461d93ebb90 100644 --- a/src/client/pythonEnvironments/index.ts +++ b/src/client/pythonEnvironments/index.ts @@ -55,7 +55,7 @@ export function createAPI(): [PythonEnvironments, () => void] { () => { activateLocators(); // Any other activation needed for the API will go here later. - } + }, ]; } diff --git a/src/client/pythonEnvironments/info/index.ts b/src/client/pythonEnvironments/info/index.ts index ba0c90b5ab68..51fc84285638 100644 --- a/src/client/pythonEnvironments/info/index.ts +++ b/src/client/pythonEnvironments/info/index.ts @@ -92,7 +92,7 @@ export function normalizeEnvironment(environment: PartialPythonEnvironment): voi /** * Convert the Python environment type to a user-facing name. */ -export function getEnvironmentTypeName(environmentType: EnvironmentType) { +export function getEnvironmentTypeName(environmentType: EnvironmentType): string { switch (environmentType) { case EnvironmentType.Conda: { return 'conda'; @@ -109,6 +109,12 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType) { case EnvironmentType.VirtualEnv: { return 'virtualenv'; } + case EnvironmentType.WindowsStore: { + return 'windows store'; + } + case EnvironmentType.Poetry: { + return 'poetry'; + } default: { return ''; } @@ -121,7 +127,7 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType) { * @param environment1 - one of the two envs to compare * @param environment2 - one of the two envs to compare */ -export function areSameEnvironment( +export function areSamePartialEnvironment( environment1: PartialPythonEnvironment | undefined, environment2: PartialPythonEnvironment | undefined, fs: IFileSystem, @@ -185,7 +191,7 @@ export function mergeEnvironments( fs: IFileSystem, ): PartialPythonEnvironment[] { return environments.reduce((accumulator, current) => { - const existingItem = accumulator.find((item) => areSameEnvironment(current, item, fs)); + const existingItem = accumulator.find((item) => areSamePartialEnvironment(current, item, fs)); if (!existingItem) { const copied: PartialPythonEnvironment = { ...current }; normalizeEnvironment(copied); diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index fbeb6cc097a3..d6c199d9e301 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -3,8 +3,6 @@ import { injectable } from 'inversify'; import * as vscode from 'vscode'; -import { createDeferred } from '../common/utils/async'; -import { Architecture } from '../common/utils/platform'; import { getVersionString, parseVersion } from '../common/utils/version'; import { CONDA_ENV_FILE_SERVICE, @@ -29,7 +27,9 @@ import { import { IPipEnvServiceHelper, IPythonInPathCommandProvider } from '../interpreter/locators/types'; import { IServiceContainer, IServiceManager } from '../ioc/types'; import { PythonEnvInfo, PythonEnvKind, PythonReleaseLevel } from './base/info'; +import { buildEnvInfo } from './base/info/env'; import { ILocator, PythonLocatorQuery } from './base/locator'; +import { getEnvs } from './base/locatorUtils'; import { initializeExternalDependencies } from './common/externalDependencies'; import { PythonInterpreterLocatorService } from './discovery/locators'; import { InterpreterLocatorHelper } from './discovery/locators/helpers'; @@ -106,13 +106,19 @@ function convertEnvInfo(info: PythonEnvInfo): PythonEnvironment { if (version !== undefined) { const { release, sysVersion } = version; - const { level, serial } = release; - const releaseStr = level === PythonReleaseLevel.Final - ? 'final' - : `${level}${serial}`; - const versionStr = `${getVersionString(version)}-${releaseStr}`; - env.version = parseVersion(versionStr); - env.sysVersion = sysVersion; + if (release === undefined) { + const versionStr = `${getVersionString(version)}-final`; + env.version = parseVersion(versionStr); + env.sysVersion = ''; + } else { + const { level, serial } = release; + const releaseStr = level === PythonReleaseLevel.Final + ? 'final' + : `${level}${serial}`; + const versionStr = `${getVersionString(version)}-${releaseStr}`; + env.version = parseVersion(versionStr); + env.sysVersion = sysVersion; + } } if (distro !== undefined && distro.org !== '') { @@ -124,34 +130,6 @@ function convertEnvInfo(info: PythonEnvInfo): PythonEnvironment { return env; } -function buildEmptyEnvInfo(): PythonEnvInfo { - return { - id: '', - kind: PythonEnvKind.Unknown, - executable: { - filename: '', - sysPrefix: '', - ctime: -1, - mtime: -1, - }, - name: '', - location: '', - version: { - major: -1, - minor: -1, - micro: -1, - release: { - level: PythonReleaseLevel.Final, - serial: 0, - }, - }, - arch: Architecture.Unknown, - distro: { - org: '', - }, - }; -} - interface IPythonEnvironments extends ILocator {} @injectable() @@ -201,8 +179,7 @@ class ComponentAdapter implements IComponentAdapter { if (!this.enabled) { return undefined; } - const info = buildEmptyEnvInfo(); - info.executable.filename = pythonPath; + const info = buildEnvInfo({ executable: pythonPath }); if (resource !== undefined) { const wsFolder = vscode.workspace.getWorkspaceFolder(resource); if (wsFolder !== undefined) { @@ -290,43 +267,13 @@ class ComponentAdapter implements IComponentAdapter { if (resource !== undefined) { const wsFolder = vscode.workspace.getWorkspaceFolder(resource); if (wsFolder !== undefined) { - query.searchLocations = [wsFolder.uri]; + query.searchLocations = { roots: [wsFolder.uri] }; } } - const deferred = createDeferred(); - const envs: PythonEnvironment[] = []; - const executableToLegacy: Record = {}; const iterator = this.api.iterEnvs(query); - - if (iterator.onUpdated !== undefined) { - iterator.onUpdated((event) => { - if (event === null) { - deferred.resolve(envs); - } else { - // Replace the old one. - const old = executableToLegacy[event.old.executable.filename]; - if (old !== undefined) { - const index = envs.indexOf(old); - if (index !== -1) { - envs[index] = convertEnvInfo(event.new); - } - } - } - }); - } else { - deferred.resolve(envs); - } - - let res = await iterator.next(); - while (!res.done) { - const env = convertEnvInfo(res.value); - envs.push(env); - executableToLegacy[env.path] = env; - res = await iterator.next(); // eslint-disable-line no-await-in-loop - } - - return deferred.promise; + const envs = await getEnvs(iterator); + return envs.map(convertEnvInfo); } } diff --git a/src/startPage-ui/react-common/postOffice.ts b/src/startPage-ui/react-common/postOffice.ts index 6fdbe58fc061..5055ca40d82a 100644 --- a/src/startPage-ui/react-common/postOffice.ts +++ b/src/startPage-ui/react-common/postOffice.ts @@ -89,7 +89,7 @@ export class PostOffice implements IDisposable { // See ./src/startPage-ui/startPage/index.html // tslint:disable-next-line: no-any const api = (this.vscodeApi as any) as { handleMessage?: Function }; - if (api.handleMessage) { + if (api && api.handleMessage) { api.handleMessage(this.handleMessages.bind(this)); } } catch { diff --git a/src/test/pythonEnvironments/base/common.ts b/src/test/pythonEnvironments/base/common.ts index 7b0516d92a65..62077e46aa20 100644 --- a/src/test/pythonEnvironments/base/common.ts +++ b/src/test/pythonEnvironments/base/common.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as path from 'path'; import { Event } from 'vscode'; import { createDeferred, flattenIterator, iterable, mapToIterator, @@ -10,54 +11,38 @@ import { PythonEnvInfo, PythonEnvKind, } from '../../../client/pythonEnvironments/base/info'; +import { buildEnvInfo } from '../../../client/pythonEnvironments/base/info/env'; import { parseVersion } from '../../../client/pythonEnvironments/base/info/pythonVersion'; -import { IPythonEnvsIterator, Locator, PythonEnvUpdatedEvent, PythonLocatorQuery } from '../../../client/pythonEnvironments/base/locator'; +import { + IPythonEnvsIterator, Locator, PythonEnvUpdatedEvent, PythonLocatorQuery, +} from '../../../client/pythonEnvironments/base/locator'; import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher'; -export function createEnv( - name: string, +export function createLocatedEnv( + locationStr: string, versionStr: string, - kind?: PythonEnvKind, - executable?: string, - idStr?: string + kind = PythonEnvKind.Unknown, + execStr = 'python', ): PythonEnvInfo { - if (kind === undefined) { - kind = PythonEnvKind.Unknown; - } - if (executable === undefined || executable === '') { - executable = 'python'; - } - const id = idStr ? idStr : `${kind}-${name}`; + const location = locationStr === '' ? '' : path.normalize(locationStr); + const normalizedExecutable = path.normalize(execStr); + const executable = location === '' || path.isAbsolute(normalizedExecutable) + ? normalizedExecutable + : path.join(location, 'bin', normalizedExecutable); const version = parseVersion(versionStr); - return { - id, - kind, - version, - name, - location: '', - arch: Architecture.x86, - executable: { - filename: executable, - sysPrefix: '', - mtime: -1, - ctime: -1 - }, - distro: { org: '' } - }; + const env = buildEnvInfo({ kind, executable, location, version }); + env.arch = Architecture.x86; + return env; } -export function createLocatedEnv( - location: string, +export function createNamedEnv( + name: string, versionStr: string, - kind = PythonEnvKind.Unknown, - executable = 'python', - idStr?: string + kind?: PythonEnvKind, + execStr = 'python', ): PythonEnvInfo { - if (!idStr) { - idStr = `${kind}-${location}`; - } - const env = createEnv('', versionStr, kind, executable, idStr); - env.location = location; + const env = createLocatedEnv('', versionStr, kind, execStr); + env.name = name; return env; } @@ -123,7 +108,7 @@ export class SimpleLocator extends Locator { return iterator; } public async resolveEnv(env: string | PythonEnvInfo): Promise { - const envInfo: PythonEnvInfo = typeof env === 'string' ? createEnv('', '', undefined, env) : env; + const envInfo: PythonEnvInfo = typeof env === 'string' ? createLocatedEnv('', '', undefined, env) : env; if (this.callbacks?.resolve === undefined) { return envInfo; } else if (this.callbacks?.resolve === null) { diff --git a/src/test/pythonEnvironments/base/envsCache.unit.test.ts b/src/test/pythonEnvironments/base/envsCache.unit.test.ts new file mode 100644 index 000000000000..530711063910 --- /dev/null +++ b/src/test/pythonEnvironments/base/envsCache.unit.test.ts @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { PythonEnvInfoCache } from '../../../client/pythonEnvironments/base/envsCache'; +import { PythonEnvInfo, PythonEnvKind } from '../../../client/pythonEnvironments/base/info'; +import * as externalDependencies from '../../../client/pythonEnvironments/common/externalDependencies'; + +suite('Environment Info cache', () => { + let getGlobalPersistentStoreStub: sinon.SinonStub; + let updatedValues: PythonEnvInfo[] | undefined; + + const allEnvsComplete = () => true; + const envInfoArray = [ + { + kind: PythonEnvKind.Conda, executable: { filename: 'my-conda-env' }, + }, + { + kind: PythonEnvKind.Venv, executable: { filename: 'my-venv-env' }, + }, + { + kind: PythonEnvKind.Pyenv, executable: { filename: 'my-pyenv-env' }, + }, + ] as PythonEnvInfo[]; + + setup(() => { + getGlobalPersistentStoreStub = sinon.stub(externalDependencies, 'getGlobalPersistentStore'); + getGlobalPersistentStoreStub.returns({ + get() { return envInfoArray; }, + set(envs: PythonEnvInfo[]) { + updatedValues = envs; + return Promise.resolve(); + }, + }); + }); + + teardown(() => { + getGlobalPersistentStoreStub.restore(); + updatedValues = undefined; + }); + + test('`initialize` reads from persistent storage', () => { + const envsCache = new PythonEnvInfoCache(allEnvsComplete); + + envsCache.initialize(); + + assert.ok(getGlobalPersistentStoreStub.calledOnce); + }); + + test('The in-memory env info array is undefined if there is no value in persistent storage when initializing the cache', () => { + const envsCache = new PythonEnvInfoCache(allEnvsComplete); + + getGlobalPersistentStoreStub.returns({ get() { return undefined; } }); + envsCache.initialize(); + const result = envsCache.getAllEnvs(); + + assert.strictEqual(result, undefined); + }); + + test('`getAllEnvs` should return a deep copy of the environments currently in memory', () => { + const envsCache = new PythonEnvInfoCache(allEnvsComplete); + + envsCache.initialize(); + const envs = envsCache.getAllEnvs()!; + + envs[0].name = 'some-other-name'; + + assert.ok(envs[0] !== envInfoArray[0]); + }); + + test('`getAllEnvs` should return undefined if nothing has been set', () => { + const envsCache = new PythonEnvInfoCache(allEnvsComplete); + + const envs = envsCache.getAllEnvs(); + + assert.deepStrictEqual(envs, undefined); + }); + + test('`setAllEnvs` should clone the environment info array passed as a parameter', () => { + const envsCache = new PythonEnvInfoCache(allEnvsComplete); + + envsCache.setAllEnvs(envInfoArray); + const envs = envsCache.getAllEnvs(); + + assert.deepStrictEqual(envs, envInfoArray); + assert.strictEqual(envs === envInfoArray, false); + }); + + test('`filterEnvs` should return environments that match its argument using areSameEnvironmnet', () => { + const env:PythonEnvInfo = { executable: { filename: 'my-venv-env' } } as unknown as PythonEnvInfo; + const envsCache = new PythonEnvInfoCache(allEnvsComplete); + + envsCache.initialize(); + + const result = envsCache.filterEnvs(env); + + assert.deepStrictEqual(result, [{ + kind: PythonEnvKind.Venv, executable: { filename: 'my-venv-env' }, + }]); + }); + + test('`filterEnvs` should return a deep copy of the matched environments', () => { + const envToFind = { + kind: PythonEnvKind.System, executable: { filename: 'my-system-env' }, + } as unknown as PythonEnvInfo; + const env:PythonEnvInfo = { executable: { filename: 'my-system-env' } } as unknown as PythonEnvInfo; + const envsCache = new PythonEnvInfoCache(allEnvsComplete); + + envsCache.setAllEnvs([...envInfoArray, envToFind]); + + const result = envsCache.filterEnvs(env)!; + result[0].name = 'some-other-name'; + + assert.notDeepStrictEqual(result[0], envToFind); + }); + + test('`filterEnvs` should return an empty array if no environment matches the properties of its argument', () => { + const env:PythonEnvInfo = { executable: { filename: 'my-nonexistent-env' } } as unknown as PythonEnvInfo; + const envsCache = new PythonEnvInfoCache(allEnvsComplete); + + envsCache.initialize(); + + const result = envsCache.filterEnvs(env); + + assert.deepStrictEqual(result, []); + }); + + test('`filterEnvs` should return undefined if the cache hasn\'t been initialized', () => { + const env:PythonEnvInfo = { executable: { filename: 'my-nonexistent-env' } } as unknown as PythonEnvInfo; + const envsCache = new PythonEnvInfoCache(allEnvsComplete); + + const result = envsCache.filterEnvs(env); + + assert.strictEqual(result, undefined); + }); + + test('`flush` should write complete environment info objects to persistent storage', async () => { + const otherEnv = { + kind: PythonEnvKind.OtherGlobal, + executable: { filename: 'my-other-env' }, + defaultDisplayName: 'other-env', + }; + const updatedEnvInfoArray = [ + otherEnv, { kind: PythonEnvKind.System, executable: { filename: 'my-system-env' } }, + ] as PythonEnvInfo[]; + const expected = [ + otherEnv, + ]; + const envsCache = new PythonEnvInfoCache((env) => env.defaultDisplayName !== undefined); + + envsCache.initialize(); + envsCache.setAllEnvs(updatedEnvInfoArray); + await envsCache.flush(); + + assert.deepStrictEqual(updatedValues, expected); + }); + + test('`flush` should not write to persistent storage if there are no environment info objects in-memory', async () => { + const envsCache = new PythonEnvInfoCache((env) => env.kind === PythonEnvKind.MacDefault); + + await envsCache.flush(); + + assert.strictEqual(updatedValues, undefined); + }); + + test('`flush` should not write to persistent storage if there are no complete environment info objects', async () => { + const envsCache = new PythonEnvInfoCache((env) => env.kind === PythonEnvKind.MacDefault); + + envsCache.initialize(); + await envsCache.flush(); + + assert.strictEqual(updatedValues, undefined); + }); +}); diff --git a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts new file mode 100644 index 000000000000..fe297360e7a8 --- /dev/null +++ b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts @@ -0,0 +1,472 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import { EventEmitter, Uri } from 'vscode'; +import { getValues as getEnumValues } from '../../../client/common/utils/enum'; +import { PythonEnvInfo, PythonEnvKind } from '../../../client/pythonEnvironments/base/info'; +import { copyEnvInfo } from '../../../client/pythonEnvironments/base/info/env'; +import { + IPythonEnvsIterator, + PythonEnvUpdatedEvent, + PythonLocatorQuery, +} from '../../../client/pythonEnvironments/base/locator'; +import { getEnvs, getQueryFilter } from '../../../client/pythonEnvironments/base/locatorUtils'; +import { + createLocatedEnv, + createNamedEnv, +} from './common'; + +const homeDir = path.normalize('/home/me'); +const workspaceRoot = Uri.file('workspace-root'); +const doesNotExist = Uri.file(path.normalize('does-not-exist')); + +function setSearchLocation(env: PythonEnvInfo, location?: string): void { + const locationStr = location === undefined + ? path.dirname(env.location) + : path.normalize(location); + env.searchLocation = Uri.file(locationStr); +} + +const env1 = createNamedEnv('env1', '3.8', PythonEnvKind.System, '/usr/bin/python3.8'); +const env2 = createNamedEnv('env2', '3.8.1rc2', PythonEnvKind.Pyenv, '/pyenv/3.8.1rc2/bin/python'); +const env3 = createNamedEnv('env3', '3.9.1b2', PythonEnvKind.Unknown, 'python3.9'); +const env4 = createNamedEnv('env4', '2.7.11', PythonEnvKind.Pyenv, '/pyenv/2.7.11/bin/python'); +const env5 = createNamedEnv('env5', '2.7', PythonEnvKind.System, 'python2'); +const env6 = createNamedEnv('env6', '3.7.4', PythonEnvKind.Conda, 'python'); +const plainEnvs = [env1, env2, env3, env4, env5, env6]; + +const envL1 = createLocatedEnv('/.venvs/envL1', '3.9.0', PythonEnvKind.Venv); +const envL2 = createLocatedEnv('/conda/envs/envL2', '3.8.3', PythonEnvKind.Conda); +const locatedEnvs = [envL1, envL2]; + +const envS1 = createNamedEnv('env S1', '3.9', PythonEnvKind.OtherVirtual, `${homeDir}/some-dir/bin/python`); +setSearchLocation(envS1, homeDir); +const envS2 = createNamedEnv('env S2', '3.9', PythonEnvKind.OtherVirtual, `${homeDir}/some-dir2/bin/python`); +setSearchLocation(envS2, homeDir); +const envS3 = createNamedEnv('env S2', '3.9', PythonEnvKind.OtherVirtual, `${workspaceRoot.fsPath}/p/python`); +envS3.searchLocation = workspaceRoot; +const rootedEnvs = [envS1, envS2, envS3]; + +const envSL1 = createLocatedEnv(`${homeDir}/.venvs/envSL1`, '3.9.0', PythonEnvKind.Venv); +setSearchLocation(envSL1); +const envSL2 = createLocatedEnv(`${workspaceRoot.fsPath}/.venv`, '3.8.2', PythonEnvKind.Pipenv); +setSearchLocation(envSL2); +const envSL3 = createLocatedEnv(`${homeDir}/.conda-envs/envSL3`, '3.8.2', PythonEnvKind.Conda); +setSearchLocation(envSL3); +const envSL4 = createLocatedEnv('/opt/python3.10', '3.10.0a1', PythonEnvKind.Custom); +setSearchLocation(envSL4); +const envSL5 = createLocatedEnv(`${homeDir}/.venvs/envSL5`, '3.9.0', PythonEnvKind.Venv); +setSearchLocation(envSL5); +const rootedLocatedEnvs = [envSL1, envSL2, envSL3, envSL4, envSL5]; + +const envs = [ + ...plainEnvs, + ...locatedEnvs, + ...rootedEnvs, + ...rootedLocatedEnvs, +]; + +suite('Python envs locator utils - getQueryFilter', () => { + suite('empty query', () => { + const queries: PythonLocatorQuery[] = [ + {}, + { kinds: [] }, + // Any "defined" value for searchLocations causes filtering... + ]; + queries.forEach((query) => { + test(`all envs kept (query ${query})`, () => { + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, envs); + }); + }); + }); + + suite('kinds', () => { + test('match none', () => { + const query: PythonLocatorQuery = { kinds: [PythonEnvKind.MacDefault] }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, []); + }); + + ([ + [PythonEnvKind.Unknown, [env3]], + [PythonEnvKind.System, [env1, env5]], + [PythonEnvKind.WindowsStore, []], + [PythonEnvKind.Pyenv, [env2, env4]], + [PythonEnvKind.Venv, [envL1, envSL1, envSL5]], + [PythonEnvKind.Conda, [env6, envL2, envSL3]], + ] as [PythonEnvKind, PythonEnvInfo[]][]).forEach(([kind, expected]) => { + test(`match some (one kind: ${kind})`, () => { + const query: PythonLocatorQuery = { kinds: [kind] }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + }); + + test('match some (many kinds)', () => { + const expected = [env6, envL1, envL2, envSL1, envSL2, envSL3, envSL4, envSL5]; + const kinds = [ + PythonEnvKind.Venv, + PythonEnvKind.VirtualEnv, + PythonEnvKind.Pipenv, + PythonEnvKind.Conda, + PythonEnvKind.Custom, + ]; + const query: PythonLocatorQuery = { kinds }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all', () => { + const kinds: PythonEnvKind[] = getEnumValues(PythonEnvKind); + const query: PythonLocatorQuery = { kinds }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, envs); + }); + }); + + suite('searchLocations', () => { + test('match none', () => { + const query: PythonLocatorQuery = { + searchLocations: { + roots: [doesNotExist], + }, + }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, []); + }); + + test('match one (multiple locations)', () => { + const expected = [envSL4]; + const searchLocations = { + roots: [ + envSL4.searchLocation!, + doesNotExist, + envSL4.searchLocation!, // repeated + ], + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match multiple (one location)', () => { + const expected = [envS3, envSL2]; + const searchLocations = { + roots: [ + workspaceRoot, + ], + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match multiple (multiple locations)', () => { + const expected = [envS3, ...rootedLocatedEnvs]; + const searchLocations = { + roots: rootedLocatedEnvs.map((env) => env.searchLocation!), + }; + searchLocations.roots.push(doesNotExist); + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match multiple (include non-searched envs)', () => { + const expected = [...plainEnvs, ...locatedEnvs, envS3, ...rootedLocatedEnvs]; + const searchLocations = { + roots: rootedLocatedEnvs.map((env) => env.searchLocation!), + includeNonRooted: true, + }; + searchLocations.roots.push(doesNotExist); + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all searched', () => { + const expected = [...rootedEnvs, ...rootedLocatedEnvs]; + const searchLocations = { + roots: expected.map((env) => env.searchLocation!), + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all (including non-searched)', () => { + const expected = envs; + const searchLocations = { + roots: expected.map((e) => e.searchLocation!).filter((e) => !!e), + includeNonRooted: true, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all searched under one root', () => { + const expected = [envS1, envS2, envSL1, envSL3, envSL5]; + const searchLocations = { + roots: [Uri.file(homeDir)], + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match only non-searched envs (empty roots)', () => { + const expected = [...plainEnvs, ...locatedEnvs]; + const searchLocations = { + roots: [], + includeNonRooted: true, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match only non-searched envs (with unmatched location)', () => { + const expected = [...plainEnvs, ...locatedEnvs]; + const searchLocations = { + roots: [doesNotExist], + includeNonRooted: true, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + }); + + suite('mixed query', () => { + test('match none', () => { + const query: PythonLocatorQuery = { + kinds: [PythonEnvKind.OtherGlobal], + searchLocations: { + roots: [doesNotExist], + }, + }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, []); + }); + + test('match some', () => { + const expected = [envSL1, envSL4, envSL5]; + const kinds = [PythonEnvKind.Venv, PythonEnvKind.Custom]; + const searchLocations = { + roots: rootedLocatedEnvs.map((env) => env.searchLocation!), + }; + searchLocations.roots.push(doesNotExist); + const query: PythonLocatorQuery = { kinds, searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all', () => { + const expected = [...rootedEnvs, ...rootedLocatedEnvs]; + const kinds: PythonEnvKind[] = getEnumValues(PythonEnvKind); + const searchLocations = { + roots: expected.map((env) => env.searchLocation!), + }; + const query: PythonLocatorQuery = { kinds, searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + }); +}); + +suite('Python envs locator utils - getEnvs', () => { + test('empty, no update emitter', async () => { + const iterator = (async function* () { + // Yield nothing. + }()) as IPythonEnvsIterator; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, []); + }); + + test('empty, with unused update emitter', async () => { + const emitter = new EventEmitter(); + // eslint-disable-next-line require-yield + const iterator = (async function* () { + // Yield nothing. + emitter.fire(null); + }()) as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, []); + }); + + test('yield one, no update emitter', async () => { + const iterator = (async function* () { + yield env1; + }()) as IPythonEnvsIterator; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, [env1]); + }); + + test('yield one, no update', async () => { + const emitter = new EventEmitter(); + const iterator = (async function* () { + yield env1; + emitter.fire(null); + }()) as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, [env1]); + }); + + test('yield one, with update', async () => { + const expected = [envSL2]; + const old = copyEnvInfo(envSL2, { kind: PythonEnvKind.Venv }); + const emitter = new EventEmitter(); + const iterator = (async function* () { + yield old; + emitter.fire({ index: 0, old, update: envSL2 }); + emitter.fire(null); + }()) as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); + + test('yield many, no update emitter', async () => { + const expected = rootedLocatedEnvs; + const iterator = (async function* () { + yield* expected; + }()) as IPythonEnvsIterator; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); + + test('yield many, none updated', async () => { + const expected = rootedLocatedEnvs; + const emitter = new EventEmitter(); + const iterator = (async function* () { + yield* expected; + emitter.fire(null); + }()) as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); + + test('yield many, some updated', async () => { + const expected = rootedLocatedEnvs; + const emitter = new EventEmitter(); + const iterator = (async function* () { + const original = [...expected]; + const updated = [1, 2, 4]; + const kind = PythonEnvKind.Unknown; + updated.forEach((index) => { + original[index] = copyEnvInfo(expected[index], { kind }); + }); + + yield* original; + + updated.forEach((index) => { + emitter.fire({ index, old: original[index], update: expected[index] }); + }); + emitter.fire(null); + }()) as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); + + test('yield many, all updated', async () => { + const expected = rootedLocatedEnvs; + const emitter = new EventEmitter(); + const iterator = (async function* () { + const kind = PythonEnvKind.Unknown; + const original = expected.map((env) => copyEnvInfo(env, { kind })); + + yield original[0]; + yield original[1]; + emitter.fire({ index: 0, old: original[0], update: expected[0] }); + yield* original.slice(2); + original.forEach((old, index) => { + if (index > 0) { + emitter.fire({ index, old, update: expected[index] }); + } + }); + emitter.fire(null); + }()) as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators.unit.test.ts b/src/test/pythonEnvironments/base/locators.unit.test.ts index d7500fc37a05..4c76dfdff9e0 100644 --- a/src/test/pythonEnvironments/base/locators.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators.unit.test.ts @@ -8,7 +8,7 @@ import { PythonEnvInfo, PythonEnvKind } from '../../../client/pythonEnvironments import { PythonLocatorQuery } from '../../../client/pythonEnvironments/base/locator'; import { DisableableLocator, Locators } from '../../../client/pythonEnvironments/base/locators'; import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher'; -import { createEnv, createLocatedEnv, getEnvs, SimpleLocator } from './common'; +import { createLocatedEnv, createNamedEnv, getEnvs, SimpleLocator } from './common'; suite('Python envs locators - Locators', () => { suite('onChanged consolidates', () => { @@ -63,7 +63,7 @@ suite('Python envs locators - Locators', () => { }); test('one', async () => { - const env1 = createEnv('foo', '3.8', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8', PythonEnvKind.Venv); const expected: PythonEnvInfo[] = [env1]; const sub1 = new SimpleLocator(expected); const locators = new Locators([sub1]); @@ -75,11 +75,11 @@ suite('Python envs locators - Locators', () => { }); test('many', async () => { - const env1 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); const env2 = createLocatedEnv('some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.System); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.System); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.System); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.System); const expected = [env1, env2, env3, env4, env5]; const sub1 = new SimpleLocator([env1]); const sub2 = new SimpleLocator([], { before: sub1.done }); @@ -96,14 +96,14 @@ suite('Python envs locators - Locators', () => { test('with query', async () => { const expected: PythonLocatorQuery = { kinds: [PythonEnvKind.Venv], - searchLocations: [Uri.file('???')] + searchLocations: { roots: [Uri.file('???')] }, }; let query: PythonLocatorQuery | undefined; async function onQuery(q: PythonLocatorQuery | undefined, e: PythonEnvInfo[]) { query = q; return e; } - const env1 = createEnv('foo', '3.8', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8', PythonEnvKind.Venv); const sub1 = new SimpleLocator([env1], { onQuery }); const locators = new Locators([sub1]); @@ -114,13 +114,13 @@ suite('Python envs locators - Locators', () => { }); test('iterate out of order', async () => { - const env1 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); const env2 = createLocatedEnv('some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.System); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.System); - const env6 = createEnv('spam', '3.10.0a0', PythonEnvKind.Custom); - const env7 = createEnv('eggs', '3.9.1a0', PythonEnvKind.Custom); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.System); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.System); + const env6 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.Custom); + const env7 = createNamedEnv('eggs', '3.9.1a0', PythonEnvKind.Custom); const expected = [env5, env1, env2, env3, env4, env6, env7]; const sub4 = new SimpleLocator([env5]); const sub2 = new SimpleLocator([env1], { before: sub4.done }); @@ -136,11 +136,11 @@ suite('Python envs locators - Locators', () => { }); test('iterate intermingled', async () => { - const env1 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); const env2 = createLocatedEnv('some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.System); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.System); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.System); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.System); const expected = [env1, env4, env2, env5, env3]; const deferred1 = createDeferred(); const deferred2 = createDeferred(); @@ -189,7 +189,7 @@ suite('Python envs locators - Locators', () => { suite('resolveEnv()', () => { test('one wrapped', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = env1; const calls: number[] = []; const sub1 = new SimpleLocator([env1], { resolve: async (e) => { @@ -205,7 +205,7 @@ suite('Python envs locators - Locators', () => { }); test('first one resolves', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = env1; const calls: number[] = []; const sub1 = new SimpleLocator([env1], { resolve: async (e) => { @@ -225,7 +225,7 @@ suite('Python envs locators - Locators', () => { }); test('second one resolves', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = env1; const calls: number[] = []; const sub1 = new SimpleLocator([env1], { resolve: async (_e) => { @@ -245,7 +245,7 @@ suite('Python envs locators - Locators', () => { }); test('none resolve', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const calls: number[] = []; const sub1 = new SimpleLocator([env1], { resolve: async (_e) => { calls.push(1); @@ -320,7 +320,7 @@ suite('Python envs locators - DisableableLocator', () => { suite('iterEnvs()', () => { test('pass-through if enabled', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = [env1]; const sub = new SimpleLocator([env1]); const locator = new DisableableLocator(sub); @@ -332,7 +332,7 @@ suite('Python envs locators - DisableableLocator', () => { }); test('empty if disabled', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected: PythonEnvInfo[] = []; const sub = new SimpleLocator([env1]); const locator = new DisableableLocator(sub); @@ -347,7 +347,7 @@ suite('Python envs locators - DisableableLocator', () => { suite('resolveEnv()', () => { test('pass-through if enabled', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = env1; const sub = new SimpleLocator([env1]); const locator = new DisableableLocator(sub); @@ -358,7 +358,7 @@ suite('Python envs locators - DisableableLocator', () => { }); test('empty if disabled', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const sub = new SimpleLocator([env1]); const locator = new DisableableLocator(sub); diff --git a/src/test/pythonEnvironments/base/locators/composite/environmentsReducer.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/environmentsReducer.unit.test.ts index 9bb67f672768..04434b21bc3b 100644 --- a/src/test/pythonEnvironments/base/locators/composite/environmentsReducer.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/environmentsReducer.unit.test.ts @@ -13,16 +13,16 @@ import { } from '../../../../../client/pythonEnvironments/base/locators/composite/environmentsReducer'; import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; import { sleep } from '../../../../core'; -import { createEnv, getEnvs, SimpleLocator } from '../../common'; +import { createNamedEnv, getEnvs, SimpleLocator } from '../../common'; -suite('Environments Reducer', () => { +suite('Python envs locator - Environments Reducer', () => { suite('iterEnvs()', () => { test('Iterator only yields unique environments', async () => { - const env1 = createEnv('env1', '3.5', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); - const env2 = createEnv('env2', '3.8', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); - const env3 = createEnv('env3', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); - const env4 = createEnv('env4', '3.8.1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); // Same as env2 - const env5 = createEnv('env5', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); // Same as env1 + const env1 = createNamedEnv('env1', '3.5', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); + const env2 = createNamedEnv('env2', '3.8', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); + const env3 = createNamedEnv('env3', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); + const env4 = createNamedEnv('env4', '3.8.1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); // Same as env2 + const env5 = createNamedEnv('env5', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); // Same as env1 const environmentsToBeIterated = [env1, env2, env3, env4, env5]; // Contains 3 unique environments const parentLocator = new SimpleLocator(environmentsToBeIterated); const reducer = new PythonEnvsReducer(parentLocator); @@ -36,11 +36,11 @@ suite('Environments Reducer', () => { test('Single updates for multiple environments are sent correctly followed by the null event', async () => { // Arrange - const env1 = createEnv('env1', '3.5', PythonEnvKind.Unknown, path.join('path', 'to', 'exec1')); - const env2 = createEnv('env2', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); - const env3 = createEnv('env3', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); - const env4 = createEnv('env4', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); // Same as env2; - const env5 = createEnv('env5', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); // Same as env1; + const env1 = createNamedEnv('env1', '3.5', PythonEnvKind.Unknown, path.join('path', 'to', 'exec1')); + const env2 = createNamedEnv('env2', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); + const env3 = createNamedEnv('env3', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); + const env4 = createNamedEnv('env4', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); // Same as env2; + const env5 = createNamedEnv('env5', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); // Same as env1; const environmentsToBeIterated = [env1, env2, env3, env4, env5]; // Contains 3 unique environments const parentLocator = new SimpleLocator(environmentsToBeIterated); const onUpdatedEvents: (PythonEnvUpdatedEvent | null)[] = []; @@ -64,8 +64,8 @@ suite('Environments Reducer', () => { // Assert const expectedUpdates = [ - { old: env2, new: mergeEnvironments(env2, env4) }, - { old: env1, new: mergeEnvironments(env1, env5) }, + { index: 1, old: env2, update: mergeEnvironments(env2, env4) }, + { index: 0, old: env1, update: mergeEnvironments(env1, env5) }, null, ]; assert.deepEqual(expectedUpdates, onUpdatedEvents); @@ -73,9 +73,9 @@ suite('Environments Reducer', () => { test('Multiple updates for the same environment are sent correctly followed by the null event', async () => { // Arrange - const env1 = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); - const env2 = createEnv('env2', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); - const env3 = createEnv('env3', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec')); + const env1 = createNamedEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const env2 = createNamedEnv('env2', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); + const env3 = createNamedEnv('env3', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec')); const environmentsToBeIterated = [env1, env2, env3]; // All refer to the same environment const parentLocator = new SimpleLocator(environmentsToBeIterated); const onUpdatedEvents: (PythonEnvUpdatedEvent | null)[] = []; @@ -102,17 +102,24 @@ suite('Environments Reducer', () => { const env123 = mergeEnvironments(env12, env3); const expectedUpdates: (PythonEnvUpdatedEvent | null)[] = []; if (isEqual(env12, env123)) { - expectedUpdates.push({ old: env1, new: env12 }, null); + expectedUpdates.push( + { index: 0, old: env1, update: env12 }, + null, + ); } else { - expectedUpdates.push({ old: env1, new: env12 }, { old: env12, new: env123 }, null); + expectedUpdates.push( + { index: 0, old: env1, update: env12 }, + { index: 0, old: env12, update: env123 }, + null, + ); } assert.deepEqual(onUpdatedEvents, expectedUpdates); }); test('Updates to environments from the incoming iterator are passed on correctly followed by the null event', async () => { // Arrange - const env1 = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); - const env2 = createEnv('env2', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); + const env1 = createNamedEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const env2 = createNamedEnv('env2', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); const environmentsToBeIterated = [env1]; const didUpdate = new EventEmitter(); const parentLocator = new SimpleLocator(environmentsToBeIterated, { onUpdated: didUpdate.event }); @@ -133,12 +140,15 @@ suite('Environments Reducer', () => { // Act await getEnvs(iterator); - didUpdate.fire({ old: env1, new: env2 }); + didUpdate.fire({ index: 0, old: env1, update: env2 }); didUpdate.fire(null); // It is essential for the incoming iterator to fire "null" event signifying it's done await sleep(1); // Assert - const expectedUpdates = [{ old: env1, new: mergeEnvironments(env1, env2) }, null]; + const expectedUpdates = [ + { index: 0, old: env1, update: mergeEnvironments(env1, env2) }, + null, + ]; assert.deepEqual(expectedUpdates, onUpdatedEvents); didUpdate.dispose(); }); @@ -162,21 +172,21 @@ suite('Environments Reducer', () => { suite('resolveEnv()', () => { test('Iterates environments from the reducer to get resolved environment, then calls into locator manager to resolve environment further and return it', async () => { - const env1 = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); - const env2 = createEnv('env2', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); - const env3 = createEnv('env3', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec')); - const env4 = createEnv('env4', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); - const env5 = createEnv('env5', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); - const env6 = createEnv('env6', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); + const env1 = createNamedEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const env2 = createNamedEnv('env2', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); + const env3 = createNamedEnv('env3', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec')); + const env4 = createNamedEnv('env4', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); + const env5 = createNamedEnv('env5', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); + const env6 = createNamedEnv('env6', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); const environmentsToBeIterated = [env1, env2, env3, env4, env5, env6]; // env1 env3 env6 are same const env13 = mergeEnvironments(env1, env3); const env136 = mergeEnvironments(env13, env6); - const expectedResolvedEnv = createEnv('resolvedEnv', '3.8.1', PythonEnvKind.Conda, 'resolved/path/to/exec'); + const expected = createNamedEnv('resolvedEnv', '3.8.1', PythonEnvKind.Conda, 'resolved/path/to/exec'); const parentLocator = new SimpleLocator(environmentsToBeIterated, { resolve: async (e: PythonEnvInfo) => { if (isEqual(e, env136)) { - return expectedResolvedEnv; + return expected; } return undefined; }, @@ -184,18 +194,18 @@ suite('Environments Reducer', () => { const reducer = new PythonEnvsReducer(parentLocator); // Trying to resolve the environment corresponding to env1 env3 env6 - const expected = await reducer.resolveEnv(path.join('path', 'to', 'exec')); + const resolved = await reducer.resolveEnv(path.join('path', 'to', 'exec')); - assert.deepEqual(expected, expectedResolvedEnv); + assert.deepEqual(resolved, expected); }); test("If the reducer isn't able to resolve environment, return undefined", async () => { - const env1 = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); - const env2 = createEnv('env2', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); - const env3 = createEnv('env3', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec')); - const env4 = createEnv('env4', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); - const env5 = createEnv('env5', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); - const env6 = createEnv('env6', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); + const env1 = createNamedEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const env2 = createNamedEnv('env2', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); + const env3 = createNamedEnv('env3', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec')); + const env4 = createNamedEnv('env4', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); + const env5 = createNamedEnv('env5', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); + const env6 = createNamedEnv('env6', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); const environmentsToBeIterated = [env1, env2, env3, env4, env5, env6]; // env1 env3 env6 are same const env13 = mergeEnvironments(env1, env3); @@ -203,7 +213,7 @@ suite('Environments Reducer', () => { const parentLocator = new SimpleLocator(environmentsToBeIterated, { resolve: async (e: PythonEnvInfo) => { if (isEqual(e, env136)) { - return createEnv('resolvedEnv', '3.8.1', PythonEnvKind.Conda, 'resolved/path/to/exec'); + return createNamedEnv('resolvedEnv', '3.8.1', PythonEnvKind.Conda, 'resolved/path/to/exec'); } return undefined; }, diff --git a/src/test/pythonEnvironments/base/locators/composite/environmentsResolver.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/environmentsResolver.unit.test.ts index b0a7d5bbf17b..d56468212826 100644 --- a/src/test/pythonEnvironments/base/locators/composite/environmentsResolver.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/environmentsResolver.unit.test.ts @@ -16,9 +16,9 @@ import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments import * as ExternalDep from '../../../../../client/pythonEnvironments/common/externalDependencies'; import { EnvironmentInfoService } from '../../../../../client/pythonEnvironments/info/environmentInfoService'; import { sleep } from '../../../../core'; -import { createEnv, getEnvs, SimpleLocator } from '../../common'; +import { createNamedEnv, getEnvs, SimpleLocator } from '../../common'; -suite('Environments Resolver', () => { +suite('Python envs locator - Environments Resolver', () => { /** * Returns the expected environment to be returned by Environment info service */ @@ -53,10 +53,10 @@ suite('Environments Resolver', () => { }); test('Iterator yields environments as-is', async () => { - const env1 = createEnv('env1', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); - const env2 = createEnv('env2', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); - const env3 = createEnv('env3', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); - const env4 = createEnv('env4', '3.9.0rc2', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); + const env1 = createNamedEnv('env1', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); + const env2 = createNamedEnv('env2', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); + const env3 = createNamedEnv('env3', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); + const env4 = createNamedEnv('env4', '3.9.0rc2', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); const environmentsToBeIterated = [env1, env2, env3, env4]; const parentLocator = new SimpleLocator(environmentsToBeIterated); const resolver = new PythonEnvsResolver(parentLocator, new EnvironmentInfoService()); @@ -69,8 +69,8 @@ suite('Environments Resolver', () => { test('Updates for environments are sent correctly followed by the null event', async () => { // Arrange - const env1 = createEnv('env1', '3.5.12b1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec1')); - const env2 = createEnv('env2', '3.8.1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); + const env1 = createNamedEnv('env1', '3.5.12b1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec1')); + const env2 = createNamedEnv('env2', '3.8.1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); const environmentsToBeIterated = [env1, env2]; const parentLocator = new SimpleLocator(environmentsToBeIterated); const onUpdatedEvents: (PythonEnvUpdatedEvent | null)[] = []; @@ -94,8 +94,8 @@ suite('Environments Resolver', () => { // Assert const expectedUpdates = [ - { old: env1, new: createExpectedEnvInfo(env1) }, - { old: env2, new: createExpectedEnvInfo(env2) }, + { index: 0, old: env1, update: createExpectedEnvInfo(env1) }, + { index: 1, old: env2, update: createExpectedEnvInfo(env2) }, null, ]; assert.deepEqual(expectedUpdates, onUpdatedEvents); @@ -103,8 +103,8 @@ suite('Environments Resolver', () => { test('Updates to environments from the incoming iterator are sent correctly followed by the null event', async () => { // Arrange - const env = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); - const updatedEnv = createEnv('env1', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); + const env = createNamedEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const updatedEnv = createNamedEnv('env1', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); const environmentsToBeIterated = [env]; const didUpdate = new EventEmitter(); const parentLocator = new SimpleLocator(environmentsToBeIterated, { onUpdated: didUpdate.event }); @@ -126,7 +126,7 @@ suite('Environments Resolver', () => { // Act await getEnvs(iterator); await sleep(1); - didUpdate.fire({ old: env, new: updatedEnv }); + didUpdate.fire({ index: 0, old: env, update: updatedEnv }); didUpdate.fire(null); // It is essential for the incoming iterator to fire "null" event signifying it's done await sleep(1); @@ -134,7 +134,7 @@ suite('Environments Resolver', () => { // The updates can be anything, even the number of updates, but they should lead to the same final state const { length } = onUpdatedEvents; assert.deepEqual( - onUpdatedEvents[length - 2]?.new, + onUpdatedEvents[length - 2]?.update, createExpectedEnvInfo(updatedEnv), 'The final update to environment is incorrect', ); @@ -179,8 +179,8 @@ suite('Environments Resolver', () => { }); test('Calls into parent locator to get resolved environment, then calls environnment service to resolve environment further and return it', async () => { - const env = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); - const resolvedEnvReturnedByReducer = createEnv( + const env = createNamedEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const resolvedEnvReturnedByReducer = createNamedEnv( 'env1', '3.8.1', PythonEnvKind.Conda, @@ -207,8 +207,8 @@ suite('Environments Resolver', () => { reject(); }), ); - const env = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); - const resolvedEnvReturnedByReducer = createEnv( + const env = createNamedEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const resolvedEnvReturnedByReducer = createNamedEnv( 'env1', '3.8.1', PythonEnvKind.Conda, @@ -230,7 +230,7 @@ suite('Environments Resolver', () => { }); test("If the parent locator isn't able to resolve environment, return undefined", async () => { - const env = createEnv('env', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const env = createNamedEnv('env', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); const parentLocator = new SimpleLocator([], { resolve: async () => undefined, }); diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.7.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.7.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.7.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.8.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.8.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.8.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/discovery/locators/index.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/index.unit.test.ts index 2480aeed31d8..5dfed7117978 100644 --- a/src/test/pythonEnvironments/discovery/locators/index.unit.test.ts +++ b/src/test/pythonEnvironments/discovery/locators/index.unit.test.ts @@ -36,7 +36,10 @@ import { } from '../../../../client/pythonEnvironments/discovery/locators'; import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; import { - createEnv, createLocatedEnv, getEnvs, SimpleLocator, + createLocatedEnv, + createNamedEnv, + getEnvs, + SimpleLocator, } from '../../base/common'; class WorkspaceFolders { @@ -97,7 +100,7 @@ suite('WorkspaceLocators', () => { suite('onChanged', () => { test('no roots', () => { const expected: PythonEnvsChangedEvent[] = []; - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const loc1 = new SimpleLocator([env1]); const locators = new WorkspaceLocators([ () => [loc1], @@ -115,7 +118,7 @@ suite('WorkspaceLocators', () => { test('no factories', () => { const expected: PythonEnvsChangedEvent[] = []; - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const loc1 = new SimpleLocator([env1]); const locators = new WorkspaceLocators([]); const folders = new WorkspaceFolders(['foo', 'bar']); @@ -146,7 +149,7 @@ suite('WorkspaceLocators', () => { const event4: PythonEnvsChangedEvent = { kind: PythonEnvKind.Venv }; const event5: PythonEnvsChangedEvent = { kind: PythonEnvKind.Pipenv }; const event6: PythonEnvsChangedEvent = { kind: PythonEnvKind.Conda }; - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const loc1 = new SimpleLocator([env1]); const loc2 = new SimpleLocator([]); const loc3 = new SimpleLocator([]); @@ -263,7 +266,7 @@ suite('WorkspaceLocators', () => { suite('iterEnvs()', () => { test('no roots', async () => { const expected: PythonEnvInfo[] = []; - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const loc1 = new SimpleLocator([env1]); const locators = new WorkspaceLocators([ () => [loc1], @@ -307,7 +310,7 @@ suite('WorkspaceLocators', () => { test('one not empty', async () => { const root1 = Uri.file('foo'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected: PythonEnvInfo[] = [env1]; const loc1 = new SimpleLocator([env1]); const locators = new WorkspaceLocators([ @@ -324,8 +327,8 @@ suite('WorkspaceLocators', () => { test('empty locator ignored', async () => { const root1 = Uri.file('foo'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); - const env2 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env2 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); const expected: PythonEnvInfo[] = [env1, env2]; const loc1 = new SimpleLocator([env1]); const loc2 = new SimpleLocator([], { before: loc1.done }); @@ -345,14 +348,14 @@ suite('WorkspaceLocators', () => { test('consolidates envs across roots', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const env2 = createLocatedEnv('foo/some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env6 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); - const env7 = createEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); - const env8 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env6 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env7 = createNamedEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); + const env8 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); const expected: PythonEnvInfo[] = [env1, env2, env3, env4, env5, env6, env7, env8]; const loc1 = new SimpleLocator([env1, env2]); const loc2 = new SimpleLocator([env3, env4], { before: loc1.done }); @@ -374,10 +377,10 @@ suite('WorkspaceLocators', () => { test('query matches a root', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); - const env2 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env3 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env4 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env2 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env3 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env4 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); const expected: PythonEnvInfo[] = [env1, env2]; const loc1 = new SimpleLocator([env1]); const loc2 = new SimpleLocator([env2], { before: loc1.done }); @@ -389,8 +392,9 @@ suite('WorkspaceLocators', () => { ]); const folders = new WorkspaceFolders([root1, root2]); locators.activate(folders); + const query = { searchLocations: { roots: [root1] } }; - const iterators = locators.iterEnvs({ searchLocations: [root1] }); + const iterators = locators.iterEnvs(query); const envs = await getEnvs(iterators); expect(envs).to.deep.equal(expected); @@ -399,10 +403,10 @@ suite('WorkspaceLocators', () => { test('query matches all roots', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); - const env2 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env3 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env4 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env2 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env3 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env4 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); const expected: PythonEnvInfo[] = [env1, env2, env3, env4]; const loc1 = new SimpleLocator([env1]); const loc2 = new SimpleLocator([env2], { before: loc1.done }); @@ -414,8 +418,9 @@ suite('WorkspaceLocators', () => { ]); const folders = new WorkspaceFolders([root1, root2]); locators.activate(folders); + const query = { searchLocations: { roots: [root1, root2] } }; - const iterators = locators.iterEnvs({ searchLocations: [root1, root2] }); + const iterators = locators.iterEnvs(query); const envs = await getEnvs(iterators); expect(envs).to.deep.equal(expected); @@ -424,10 +429,10 @@ suite('WorkspaceLocators', () => { test('query does not match a root', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); - const env2 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env3 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env4 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env2 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env3 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env4 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); const loc1 = new SimpleLocator([env1]); const loc2 = new SimpleLocator([env2], { before: loc1.done }); const loc3 = new SimpleLocator([env3], { before: loc2.done }); @@ -438,8 +443,9 @@ suite('WorkspaceLocators', () => { ]); const folders = new WorkspaceFolders([root1, root2]); locators.activate(folders); + const query = { searchLocations: { roots: [Uri.file('baz')] } }; - const iterators = locators.iterEnvs({ searchLocations: [Uri.file('baz')] }); + const iterators = locators.iterEnvs(query); const envs = await getEnvs(iterators); expect(envs).to.deep.equal([]); @@ -448,10 +454,10 @@ suite('WorkspaceLocators', () => { test('query has no searchLocation', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); - const env2 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env3 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env4 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env2 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env3 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env4 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); const expected: PythonEnvInfo[] = [env1, env2, env3, env4]; const loc1 = new SimpleLocator([env1]); const loc2 = new SimpleLocator([env2], { before: loc1.done }); @@ -473,14 +479,14 @@ suite('WorkspaceLocators', () => { test('iterate out of order', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const env2 = createLocatedEnv('foo/some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env6 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); - const env7 = createEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); - const env8 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env6 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env7 = createNamedEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); + const env8 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); const expected: PythonEnvInfo[] = [env5, env6, env1, env2, env3, env4, env7, env8]; const loc3 = new SimpleLocator([env5, env6]); const loc1 = new SimpleLocator([env1, env2], { before: loc3.done }); @@ -502,14 +508,14 @@ suite('WorkspaceLocators', () => { test('iterate intermingled', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo-x', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo-x', '3.8.1', PythonEnvKind.Venv); const env2 = createLocatedEnv('foo/some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env6 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); - const env7 = createEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); - const env8 = createEnv('foo-y', '3.5.12b1', PythonEnvKind.Venv); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env6 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env7 = createNamedEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); + const env8 = createNamedEnv('foo-y', '3.5.12b1', PythonEnvKind.Venv); const expected = [env3, env6, env1, env2, env8, env4, env5, env7]; const ordered = [env1, env2, env3, env4, env5, env6, env7, env8]; const deferreds = [ @@ -554,14 +560,14 @@ suite('WorkspaceLocators', () => { test('respects roots set during activation', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const env2 = createLocatedEnv('foo/some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env6 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); - const env7 = createEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); - const env8 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env6 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env7 = createNamedEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); + const env8 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); const expected: PythonEnvInfo[] = [env1, env2, env3, env4, env5, env6, env7, env8]; const loc1 = new SimpleLocator([env1, env2]); const loc2 = new SimpleLocator([env3, env4], { before: loc1.done }); @@ -586,14 +592,14 @@ suite('WorkspaceLocators', () => { test('respects added roots', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const env2 = createLocatedEnv('foo/some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env6 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); - const env7 = createEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); - const env8 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env6 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env7 = createNamedEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); + const env8 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); const expected: PythonEnvInfo[] = [env1, env2, env3, env4, env5, env6, env7, env8]; const loc1 = new SimpleLocator([env1, env2]); const loc2 = new SimpleLocator([env3, env4], { before: loc1.done }); @@ -620,14 +626,14 @@ suite('WorkspaceLocators', () => { test('ignores removed roots', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const env2 = createLocatedEnv('foo/some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env6 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); - const env7 = createEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); - const env8 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env6 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env7 = createNamedEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); + const env8 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); const expectedBefore = [env1, env2, env3, env4, env5, env6, env7, env8]; const expectedAfter = [env1, env2, env3, env4]; const loc1 = new SimpleLocator([env1, env2]); @@ -661,7 +667,7 @@ suite('WorkspaceLocators', () => { } test('no roots', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const loc1 = new SimpleLocator([env1]); const locators = new WorkspaceLocators([ () => [loc1], @@ -675,7 +681,7 @@ suite('WorkspaceLocators', () => { }); test('no factories', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const locators = new WorkspaceLocators([]); const folders = new WorkspaceFolders(['foo', 'bar']); locators.activate(folders); @@ -687,7 +693,7 @@ suite('WorkspaceLocators', () => { test('one locator, not resolved', async () => { const root1 = Uri.file('foo'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const loc1 = new SimpleLocator([env1], { resolve: null }); const locators = new WorkspaceLocators([ () => [loc1], @@ -702,7 +708,7 @@ suite('WorkspaceLocators', () => { test('one locator, resolved', async () => { const root1 = Uri.file('foo'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = env1; const loc1 = new SimpleLocator([env1]); const locators = new WorkspaceLocators([ @@ -718,7 +724,7 @@ suite('WorkspaceLocators', () => { test('one root, first locator resolves', async () => { const root1 = Uri.file('foo'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = env1; const seen: number[] = []; const loc1 = new SimpleLocator([env1], { resolve: getResolver(seen, 1) }); @@ -737,7 +743,7 @@ suite('WorkspaceLocators', () => { test('one root, second locator resolves', async () => { const root1 = Uri.file('foo'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = env1; const seen: number[] = []; const loc1 = new SimpleLocator([env1], { resolve: getResolver(seen, 1, false) }); @@ -756,7 +762,7 @@ suite('WorkspaceLocators', () => { test('one root, not resolved', async () => { const root1 = Uri.file('foo'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const seen: number[] = []; const loc1 = new SimpleLocator([env1], { resolve: getResolver(seen, 1, false) }); const loc2 = new SimpleLocator([], { resolve: getResolver(seen, 2, false) }); @@ -775,7 +781,7 @@ suite('WorkspaceLocators', () => { test('many roots, no searchLocation, second root matches', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = env1; const seen: number[] = []; const loc1 = new SimpleLocator([env1], { resolve: getResolver(seen, 1, false) }); @@ -795,7 +801,7 @@ suite('WorkspaceLocators', () => { test('many roots, searchLocation matches', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); env1.searchLocation = root2; const expected = env1; const seen: number[] = []; @@ -816,7 +822,7 @@ suite('WorkspaceLocators', () => { test('many roots, searchLocation does not match', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); env1.searchLocation = Uri.file('baz'); const expected = env1; const seen: number[] = []; diff --git a/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts index 6bbba7c7bef2..f6855a9e4617 100644 --- a/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts +++ b/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts @@ -2,32 +2,259 @@ // Licensed under the MIT License. import * as assert from 'assert'; +import { zip } from 'lodash'; import * as path from 'path'; import * as sinon from 'sinon'; +import { ExecutionResult } from '../../../../client/common/process/types'; import * as platformApis from '../../../../client/common/utils/platform'; -import * as storeApis from '../../../../client/pythonEnvironments/discovery/locators/services/windowsStoreLocator'; +import { + PythonEnvInfo, PythonEnvKind, PythonReleaseLevel, PythonVersion, +} from '../../../../client/pythonEnvironments/base/info'; +import { InterpreterInformation } from '../../../../client/pythonEnvironments/base/info/interpreter'; +import { parseVersion } from '../../../../client/pythonEnvironments/base/info/pythonVersion'; +import * as externalDep from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { getWindowsStorePythonExes, WindowsStoreLocator } from '../../../../client/pythonEnvironments/discovery/locators/services/windowsStoreLocator'; +import { getEnvs } from '../../base/common'; import { TEST_LAYOUT_ROOT } from '../../common/commonTestConstants'; -suite('Windows Store Utils', () => { - let getEnvVar: sinon.SinonStub; - const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); - const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); - setup(() => { - getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); - getEnvVar.withArgs('LOCALAPPDATA').returns(testLocalAppData); - }); - teardown(() => { - getEnvVar.restore(); +suite('Windows Store', () => { + suite('Utils', () => { + let getEnvVar: sinon.SinonStub; + const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); + + setup(() => { + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + getEnvVar.withArgs('LOCALAPPDATA').returns(testLocalAppData); + }); + + teardown(() => { + getEnvVar.restore(); + }); + + test('Store Python Interpreters', async () => { + const expected = [ + path.join(testStoreAppRoot, 'python.exe'), + path.join(testStoreAppRoot, 'python3.7.exe'), + path.join(testStoreAppRoot, 'python3.8.exe'), + path.join(testStoreAppRoot, 'python3.exe'), + ]; + + const actual = await getWindowsStorePythonExes(); + assert.deepEqual(actual, expected); + }); }); - test('Store Python Interpreters', async () => { - const expected = [ - path.join(testStoreAppRoot, 'python.exe'), - path.join(testStoreAppRoot, 'python3.7.exe'), - path.join(testStoreAppRoot, 'python3.8.exe'), - path.join(testStoreAppRoot, 'python3.exe'), - ]; - - const actual = await storeApis.getWindowsStorePythonExes(); - assert.deepEqual(actual, expected); + + suite('Locator', () => { + let stubShellExec: sinon.SinonStub; + let getEnvVar: sinon.SinonStub; + + const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); + const pathToData = new Map(); + + const python383data = { + versionInfo: [3, 8, 3, 'final', 0], + sysPrefix: 'path', + sysVersion: '3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]', + is64Bit: true, + }; + + const python379data = { + versionInfo: [3, 7, 9, 'final', 0], + sysPrefix: 'path', + sysVersion: '3.7.9 (tags/v3.7.9:13c94747c7, Aug 17 2020, 16:30:00) [MSC v.1900 64 bit (AMD64)]', + is64Bit: true, + }; + + pathToData.set(path.join(testStoreAppRoot, 'python.exe'), python383data); + pathToData.set(path.join(testStoreAppRoot, 'python3.exe'), python383data); + pathToData.set(path.join(testStoreAppRoot, 'python3.8.exe'), python383data); + pathToData.set(path.join(testStoreAppRoot, 'python3.7.exe'), python379data); + + function createExpectedInterpreterInfo( + executable: string, + sysVersion?: string, + sysPrefix?: string, + versionStr?:string, + ): InterpreterInformation { + let version:PythonVersion; + try { + version = parseVersion(versionStr ?? path.basename(executable)); + if (sysVersion) { + version.sysVersion = sysVersion; + } + } catch (e) { + version = { + major: 3, + minor: -1, + micro: -1, + release: { level: PythonReleaseLevel.Final, serial: -1 }, + sysVersion, + }; + } + return { + version, + arch: platformApis.Architecture.x64, + executable: { + filename: executable, + sysPrefix: sysPrefix ?? '', + ctime: -1, + mtime: -1, + }, + }; + } + + setup(() => { + stubShellExec = sinon.stub(externalDep, 'shellExecute'); + stubShellExec.callsFake((command:string) => { + if (command.indexOf('notpython.exe') > 0) { + return Promise.resolve>({ stdout: '' }); + } + if (command.indexOf('python3.7.exe') > 0) { + return Promise.resolve>({ stdout: JSON.stringify(python379data) }); + } + return Promise.resolve>({ stdout: JSON.stringify(python383data) }); + }); + + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + getEnvVar.withArgs('LOCALAPPDATA').returns(testLocalAppData); + }); + + teardown(() => { + stubShellExec.restore(); + getEnvVar.restore(); + }); + + function assertEnvEqual(actual:PythonEnvInfo | undefined, expected: PythonEnvInfo | undefined):void { + assert.notStrictEqual(actual, undefined); + assert.notStrictEqual(expected, undefined); + + if (actual) { + // ensure ctime and mtime are greater than -1 + assert.ok(actual?.executable.ctime > -1); + assert.ok(actual?.executable.mtime > -1); + + // No need to match these, so reset them + actual.executable.ctime = -1; + actual.executable.mtime = -1; + + assert.deepStrictEqual(actual, expected); + } + } + + test('iterEnvs()', async () => { + const expectedEnvs = [...pathToData.keys()] + .sort((a: string, b: string) => a.localeCompare(b)) + .map((k): PythonEnvInfo|undefined => { + const data = pathToData.get(k); + if (data) { + return { + + name: '', + location: '', + kind: PythonEnvKind.WindowsStore, + distro: { org: 'Microsoft' }, + ...createExpectedInterpreterInfo(k), + }; + } + return undefined; + }); + + const locator = new WindowsStoreLocator(); + const iterator = locator.iterEnvs(); + const actualEnvs = (await getEnvs(iterator)) + .sort((a, b) => a.executable.filename.localeCompare(b.executable.filename)); + + zip(actualEnvs, expectedEnvs).forEach((value) => { + const [actual, expected] = value; + assertEnvEqual(actual, expected); + }); + }); + + test('resolveEnv(string)', async () => { + const python38path = path.join(testStoreAppRoot, 'python3.8.exe'); + const expected = { + + name: '', + location: '', + kind: PythonEnvKind.WindowsStore, + distro: { org: 'Microsoft' }, + ...createExpectedInterpreterInfo(python38path), + }; + + const locator = new WindowsStoreLocator(); + const actual = await locator.resolveEnv(python38path); + + assertEnvEqual(actual, expected); + }); + + test('resolveEnv(PythonEnvInfo)', async () => { + const python38path = path.join(testStoreAppRoot, 'python3.8.exe'); + const expected = { + + name: '', + location: '', + kind: PythonEnvKind.WindowsStore, + distro: { org: 'Microsoft' }, + ...createExpectedInterpreterInfo(python38path), + }; + + // Partially filled in env info object + const input:PythonEnvInfo = { + name: '', + location: '', + kind: PythonEnvKind.WindowsStore, + distro: { org: 'Microsoft' }, + arch: platformApis.Architecture.x64, + executable: { + filename: python38path, + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + version: { + major: 3, + minor: -1, + micro: -1, + release: { level: PythonReleaseLevel.Final, serial: -1 }, + }, + }; + + const locator = new WindowsStoreLocator(); + const actual = await locator.resolveEnv(input); + + assertEnvEqual(actual, expected); + }); + test('resolveEnv(string): forbidden path', async () => { + const python38path = path.join(testLocalAppData, 'Program Files', 'WindowsApps', 'python3.8.exe'); + const expected = { + + name: '', + location: '', + kind: PythonEnvKind.WindowsStore, + distro: { org: 'Microsoft' }, + ...createExpectedInterpreterInfo(python38path), + }; + + const locator = new WindowsStoreLocator(); + const actual = await locator.resolveEnv(python38path); + + assertEnvEqual(actual, expected); + }); + test('resolveEnv(string): Non store python', async () => { + // Use a non store root path + const python38path = path.join(testLocalAppData, 'python3.8.exe'); + + const locator = new WindowsStoreLocator(); + const actual = await locator.resolveEnv(python38path); + + assert.deepStrictEqual(actual, undefined); + }); }); });