diff --git a/extensions/positron-python/.github/actions/lint/action.yml b/extensions/positron-python/.github/actions/lint/action.yml index 1e4fd0712f70..47924c108151 100644 --- a/extensions/positron-python/.github/actions/lint/action.yml +++ b/extensions/positron-python/.github/actions/lint/action.yml @@ -36,7 +36,7 @@ runs: shell: bash - name: Install Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.x' cache: 'pip' diff --git a/extensions/positron-python/.github/release_plan.md b/extensions/positron-python/.github/release_plan.md index b4ceef69abe4..71f8d8aa0955 100644 --- a/extensions/positron-python/.github/release_plan.md +++ b/extensions/positron-python/.github/release_plan.md @@ -80,6 +80,39 @@ NOTE: this PR should make all CI relating to `main` be passing again (such as th - [ ] Determine if a hotfix is needed. - [ ] Merge the release branch **`release/YYYY.minor`** back into `main`. (This step is only required if changes were merged into the release branch. If the only change made on the release branch is the version, this is not necessary. Overall you need to ensure you DO NOT overwrite the version on the `main` branch.) + +## Steps for Point Release (if necessary) +- [ ] checkout to `main` on your local machine and run `git fetch` to ensure your local is up to date with the remote repo. +- [ ] checkout to the `release/YYY.minor` and check to make sure all necessary changes for the point release have been cherry-picked into the release branch. If not, contact the owner of the changes to do so. +- [ ] Create a branch against **`release/YYYY.minor`** called **`release-[YYYY.minor.point]`**. +- [ ] Bump the point version number in the `package.json` to the next `YYYY.minor.point` +- [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(you should now see changes to the `package.json` and `package-lock.json` only relating to the new version number)_ . (🤖) +- [ ] Create a PR from this branch against `release/YYYY.minor` +- [ ] **Rebase** and merge this PR into the release branch +- [ ] Create a draft GitHub release for the release notes (🤖) ❄️ + - [ ] Create a new [GitHub release](https://github.com/microsoft/vscode-python/releases/new). + - [ ] Specify a new tag called `vYYYY.minor.point`. + - [ ] Have the `target` for the github release be your release branch called **`release/YYYY.minor`**. + - [ ] Create the release notes by specifying the previous tag as the previous version of stable, so the minor release **`vYYYY.minor`** for the last stable release and click `Generate release notes`. + - [ ] Check the generated notes to ensure that all PRs for the point release are included so users know these new changes. + - [ ] Click `Save draft`. +- [ ] Publish the point release + - [ ] Make sure CI is passing for **`release/YYYY.minor`** release branch (🤖). + - [ ] Run the [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299) pipeline on the **`release/YYYY.minor`** branch. + - [ ] Click `run pipeline`. + - [ ] for `branch/tag` select the release branch which is **`release/YYYY.minor`**. + - [ ] 🧍🧍 Get approval on the release on the [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299) and publish the release to the marketplace. 🎉 + - [ ] Take the Github release out of draft. + +## Steps for contributing to a point release +- [ ] Work with team to decide if point release is necessary +- [ ] Work with team or users to verify the fix is correct and solves the problem without creating any new ones +- [ ] Create PR/PRs and merge then each into main as usual +- [ ] Make sure to still mark if the change is "bug" or "no-changelog" +- [ ] Cherry-pick all PRs to the release branch and check that the changes are in before the package is bumped +- [ ] Notify the release champ that your changes are in so they can trigger a point-release + + ## Prep for the _next_ release - [ ] Create a new [release plan](https://raw.githubusercontent.com/microsoft/vscode-python/main/.github/release_plan.md). (🤖) diff --git a/extensions/positron-python/.github/workflows/build.yml b/extensions/positron-python/.github/workflows/build.yml index 08f2874a83a6..586fe619d5db 100644 --- a/extensions/positron-python/.github/workflows/build.yml +++ b/extensions/positron-python/.github/workflows/build.yml @@ -77,7 +77,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Use Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} @@ -98,7 +98,7 @@ jobs: python -m pip install --upgrade -r build/test-requirements.txt - name: Run Pyright - uses: jakebailey/pyright-action@v1 + uses: jakebailey/pyright-action@v2 with: version: 1.1.308 working-directory: 'pythonFiles' @@ -126,7 +126,7 @@ jobs: path: ${{ env.special-working-directory-relative }} - name: Use Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} @@ -177,8 +177,11 @@ jobs: - name: Compile run: npx gulp prePublishNonBundle + - name: Localization + run: npx @vscode/l10n-dev@latest export ./src + - name: Install Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} diff --git a/extensions/positron-python/.github/workflows/codeql-analysis.yml b/extensions/positron-python/.github/workflows/codeql-analysis.yml index 5b037d5a1d0b..d902a68878e0 100644 --- a/extensions/positron-python/.github/workflows/codeql-analysis.yml +++ b/extensions/positron-python/.github/workflows/codeql-analysis.yml @@ -40,7 +40,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -65,4 +65,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/extensions/positron-python/.github/workflows/community-feedback-auto-comment.yml b/extensions/positron-python/.github/workflows/community-feedback-auto-comment.yml index 1bb8ca9b10da..cf3c4f51fe61 100644 --- a/extensions/positron-python/.github/workflows/community-feedback-auto-comment.yml +++ b/extensions/positron-python/.github/workflows/community-feedback-auto-comment.yml @@ -12,7 +12,7 @@ jobs: issues: write steps: - name: Check For Existing Comment - uses: peter-evans/find-comment@v2 + uses: peter-evans/find-comment@v3 id: finder with: issue-number: ${{ github.event.issue.number }} @@ -21,7 +21,7 @@ jobs: - name: Add Community Feedback Comment if: steps.finder.outputs.comment-id == '' - uses: peter-evans/create-or-update-comment@v3 + uses: peter-evans/create-or-update-comment@v4 with: issue-number: ${{ github.event.issue.number }} body: | diff --git a/extensions/positron-python/.github/workflows/pr-check.yml b/extensions/positron-python/.github/workflows/pr-check.yml index 5fe523dc9cf6..2f00cd7a761a 100644 --- a/extensions/positron-python/.github/workflows/pr-check.yml +++ b/extensions/positron-python/.github/workflows/pr-check.yml @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Use Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} @@ -72,7 +72,7 @@ jobs: python -m pip install --upgrade -r build/test-requirements.txt - name: Run Pyright - uses: jakebailey/pyright-action@v1 + uses: jakebailey/pyright-action@v2 with: version: 1.1.308 working-directory: 'pythonFiles' @@ -100,7 +100,7 @@ jobs: path: ${{ env.special-working-directory-relative }} - name: Use Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} @@ -152,8 +152,11 @@ jobs: - name: Compile run: npx gulp prePublishNonBundle + - name: Localization + run: npx @vscode/l10n-dev@latest export ./src + - name: Use Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} @@ -361,8 +364,11 @@ jobs: - name: Compile run: npx gulp prePublishNonBundle + - name: Localization + run: npx @vscode/l10n-dev@latest export ./src + - name: Use Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} cache: 'pip' @@ -513,7 +519,7 @@ jobs: run: npm run test:cover:report - name: Upload HTML report - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ runner.os }}-coverage-report-html path: ./coverage diff --git a/extensions/positron-python/.github/workflows/triage-info-needed.yml b/extensions/positron-python/.github/workflows/triage-info-needed.yml index 24ad2ed2c480..1ded54ea3f59 100644 --- a/extensions/positron-python/.github/workflows/triage-info-needed.yml +++ b/extensions/positron-python/.github/workflows/triage-info-needed.yml @@ -7,6 +7,9 @@ on: env: TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd", "brettcannon","anthonykim1"]' +permissions: + issues: write + jobs: add_label: runs-on: ubuntu-latest diff --git a/extensions/positron-python/build/update_ext_version.py b/extensions/positron-python/build/update_ext_version.py index bfd7ac1e9996..6ac2b15bbf0f 100644 --- a/extensions/positron-python/build/update_ext_version.py +++ b/extensions/positron-python/build/update_ext_version.py @@ -70,10 +70,23 @@ def main(package_json: pathlib.Path, argv: Sequence[str]) -> None: major, minor, micro, suffix = parse_version(package["version"]) current_year = datetime.datetime.now().year - if int(major) != current_year: + current_month = datetime.datetime.now().month + int_major = int(major) + valid_major = ( + int_major + == current_year # Between JAN-DEC major version should be current year + or ( + int_major == current_year - 1 and current_month == 1 + ) # After new years the check is relaxed for JAN to allow releases of previous year DEC + or ( + int_major == current_year + 1 and current_month == 12 + ) # Before new years the check is relaxed for DEC to allow pre-releases of next year JAN + ) + if not valid_major: raise ValueError( f"Major version [{major}] must be the current year [{current_year}].", f"If changing major version after new year's, change to {current_year}.1.0", + f"Minor version must be updated based on release or pre-release channel.", ) if args.release and not is_even(minor): diff --git a/extensions/positron-python/gulpfile.js b/extensions/positron-python/gulpfile.js index 9da49fbb5ac0..b7c6c0ce235a 100644 --- a/extensions/positron-python/gulpfile.js +++ b/extensions/positron-python/gulpfile.js @@ -107,7 +107,7 @@ async function addExtensionPackDependencies() { // extension dependencies need not be installed during development const packageJsonContents = await fsExtra.readFile('package.json', 'utf-8'); const packageJson = JSON.parse(packageJsonContents); - packageJson.extensionPack = ['ms-python.vscode-pylance'].concat( + packageJson.extensionPack = ['ms-python.vscode-pylance', 'ms-python.debugpy'].concat( packageJson.extensionPack ? packageJson.extensionPack : [], ); // Remove potential duplicates. diff --git a/extensions/positron-python/package.json b/extensions/positron-python/package.json index 8c650bb9cedd..6fd37b70f3de 100644 --- a/extensions/positron-python/package.json +++ b/extensions/positron-python/package.json @@ -22,7 +22,6 @@ "quickPickSortByLabel", "testObserver", "quickPickItemTooltip", - "saveEditor", "terminalDataWriteEvent", "terminalExecuteCommandEvent" ], @@ -382,7 +381,7 @@ "category": "Python", "command": "python.execInConsole", "icon": "$(play)", - "title": "%python.command.python.execInConsole.title%" + "title": "%python.command.python.execInDedicatedTerminal.title%" }, { "category": "Python", @@ -483,9 +482,6 @@ "enum": [ "show", "hide" - ], - "tags": [ - "experimental" ] }, "python.createEnvironment.trigger": { @@ -1166,6 +1162,7 @@ } } }, + "deprecated": "%python.debugger.deprecatedMessage%", "configurationSnippets": [], "label": "Python", "languages": [ @@ -1175,7 +1172,8 @@ "variables": { "pickProcess": "python.pickLocalProcess" }, - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported", + "hiddenWhen": "true" } ], "grammars": [ @@ -1676,7 +1674,7 @@ "@types/xml2js": "0.4.9", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", - "@vscode/test-electron": "^2.3.4", + "@vscode/test-electron": "^2.3.8", "@vscode/vsce": "^2.18.0", "bent": "^7.3.12", "chai": "^4.1.2", diff --git a/extensions/positron-python/package.nls.json b/extensions/positron-python/package.nls.json index 932917a47624..3bddd13217df 100644 --- a/extensions/positron-python/package.nls.json +++ b/extensions/positron-python/package.nls.json @@ -34,6 +34,7 @@ "python.activeStateToolPath.description": "Path to the State Tool executable for ActiveState runtimes (version 0.36+).", "python.autoComplete.extraPaths.description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.", "python.condaPath.description": "Path to the conda executable to use for activation (version 4.4+).", + "python.debugger.deprecatedMessage": "This configuration will be deprecated soon. Please replace `python` with `debugpy` to use the new Python Debugger extension.", "python.defaultInterpreterPath.description": "Path to default Python to use when extension loads up for the first time, no longer used once an interpreter is selected for the workspace. See [here](https://aka.ms/AAfekmf) to understand when this is used", "python.diagnostics.sourceMapsEnabled.description": "Enable source map support for meaningful stack traces in error logs.", "python.envFile.description": "Absolute path to a file containing environment variable definitions.", diff --git a/extensions/positron-python/pythonFiles/pythonrc.py b/extensions/positron-python/pythonFiles/pythonrc.py index 2b23a5c99f85..1cb72b0ec344 100644 --- a/extensions/positron-python/pythonFiles/pythonrc.py +++ b/extensions/positron-python/pythonFiles/pythonrc.py @@ -1,55 +1,80 @@ -# import sys +import sys -# original_ps1 = ">>> " +if sys.platform != "win32": + import readline +original_ps1 = ">>> " -# class repl_hooks: -# def __init__(self): -# self.global_exit = None -# self.failure_flag = False -# self.original_excepthook = sys.excepthook -# self.original_displayhook = sys.displayhook -# sys.excepthook = self.my_excepthook -# sys.displayhook = self.my_displayhook -# def my_displayhook(self, value): -# if value is None: -# self.failure_flag = False +class repl_hooks: + def __init__(self): + self.global_exit = None + self.failure_flag = False + self.original_excepthook = sys.excepthook + self.original_displayhook = sys.displayhook + sys.excepthook = self.my_excepthook + sys.displayhook = self.my_displayhook -# self.original_displayhook(value) + def my_displayhook(self, value): + if value is None: + self.failure_flag = False -# def my_excepthook(self, type, value, traceback): -# self.global_exit = value -# self.failure_flag = True + self.original_displayhook(value) -# self.original_excepthook(type, value, traceback) + def my_excepthook(self, type, value, traceback): + self.global_exit = value + self.failure_flag = True + self.original_excepthook(type, value, traceback) -# class ps1: -# hooks = repl_hooks() -# sys.excepthook = hooks.my_excepthook -# sys.displayhook = hooks.my_displayhook -# # str will get called for every prompt with exit code to show success/failure -# def __str__(self): -# exit_code = 0 -# if self.hooks.failure_flag: -# exit_code = 1 -# else: -# exit_code = 0 +def get_last_command(): + # Get the last history item + last_command = "" + if sys.platform != "win32": + last_command = readline.get_history_item(readline.get_current_history_length()) -# # Guide following official VS Code doc for shell integration sequence: -# # result = "{command_finished}{prompt_started}{prompt}{command_start}{command_executed}".format( -# # command_finished="\x1b]633;D;" + str(exit_code) + "\x07", -# # prompt_started="\x1b]633;A\x07", -# # prompt=original_ps1, -# # command_start="\x1b]633;B\x07", -# # command_executed="\x1b]633;C\x07", -# # ) -# result = f"{chr(27)}]633;D;{exit_code}{chr(7)}{chr(27)}]633;A{chr(7)}{original_ps1}{chr(27)}]633;B{chr(7)}{chr(27)}]633;C{chr(7)}" + return last_command -# return result +class ps1: + hooks = repl_hooks() + sys.excepthook = hooks.my_excepthook + sys.displayhook = hooks.my_displayhook -# if sys.platform != "win32": -# sys.ps1 = ps1() + # str will get called for every prompt with exit code to show success/failure + def __str__(self): + exit_code = 0 + if self.hooks.failure_flag: + exit_code = 1 + else: + exit_code = 0 + + # Guide following official VS Code doc for shell integration sequence: + result = "" + # For non-windows allow recent_command history. + if sys.platform != "win32": + result = "{command_finished}{prompt_started}{prompt}{command_start}{command_executed}{command_line}".format( + command_finished="\x1b]633;D;" + str(exit_code) + "\x07", + prompt_started="\x1b]633;A\x07", + prompt=original_ps1, + command_start="\x1b]633;B\x07", + command_executed="\x1b]633;C\x07", + command_line="\x1b]633;E;" + str(get_last_command()) + "\x07", + ) + else: + result = "{command_finished}{prompt_started}{prompt}{command_start}{command_executed}".format( + command_finished="\x1b]633;D;" + str(exit_code) + "\x07", + prompt_started="\x1b]633;A\x07", + prompt=original_ps1, + command_start="\x1b]633;B\x07", + command_executed="\x1b]633;C\x07", + ) + + # result = f"{chr(27)}]633;D;{exit_code}{chr(7)}{chr(27)}]633;A{chr(7)}{original_ps1}{chr(27)}]633;B{chr(7)}{chr(27)}]633;C{chr(7)}" + + return result + + +if sys.platform != "win32": + sys.ps1 = ps1() diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/z_nested_folder_one/test_bottom_folder.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py rename to extensions/positron-python/pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/z_nested_folder_one/test_bottom_folder.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/test_multi_class_nest.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/test_multi_class_nest.py new file mode 100644 index 000000000000..209f9d51915b --- /dev/null +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/test_multi_class_nest.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +class TestFirstClass: + class TestSecondClass: + def test_second(self): # test_marker--test_second + assert 1 == 2 + + def test_first(self): # test_marker--test_first + assert 1 == 2 + + class TestSecondClass2: + def test_second2(self): # test_marker--test_second2 + assert 1 == 1 + + +def test_independent(): # test_marker--test_independent + assert 1 == 1 diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 31686d2b3b5d..d4e91f56b5fe 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -317,7 +317,7 @@ # └── test_top_folder.py # └── test_top_function_t # └── test_top_function_f -# └── nested_folder_one +# └── z_nested_folder_one # └── test_bottom_folder.py # └── test_bottom_function_t # └── test_bottom_function_f @@ -326,14 +326,14 @@ TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" ) -test_nested_folder_one_path = ( - TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" +test_z_nested_folder_one_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "z_nested_folder_one" ) test_bottom_folder_path = ( TEST_DATA_PATH / "dual_level_nested_folder" - / "nested_folder_one" + / "z_nested_folder_one" / "test_bottom_folder.py" ) @@ -392,10 +392,10 @@ ], }, { - "name": "nested_folder_one", - "path": os.fspath(test_nested_folder_one_path), + "name": "z_nested_folder_one", + "path": os.fspath(test_z_nested_folder_one_path), "type_": "folder", - "id_": os.fspath(test_nested_folder_one_path), + "id_": os.fspath(test_z_nested_folder_one_path), "children": [ { "name": "test_bottom_folder.py", @@ -412,11 +412,11 @@ ), "type_": "test", "id_": get_absolute_test_id( - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "dual_level_nested_folder/z_nested_folder_one/test_bottom_folder.py::test_bottom_function_t", test_bottom_folder_path, ), "runID": get_absolute_test_id( - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "dual_level_nested_folder/z_nested_folder_one/test_bottom_folder.py::test_bottom_function_t", test_bottom_folder_path, ), }, @@ -429,11 +429,11 @@ ), "type_": "test", "id_": get_absolute_test_id( - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + "dual_level_nested_folder/z_nested_folder_one/test_bottom_folder.py::test_bottom_function_f", test_bottom_folder_path, ), "runID": get_absolute_test_id( - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + "dual_level_nested_folder/z_nested_folder_one/test_bottom_folder.py::test_bottom_function_f", test_bottom_folder_path, ), }, @@ -886,3 +886,111 @@ ], "id_": os.fspath(tests_path), } +TEST_MULTI_CLASS_NEST_PATH = TEST_DATA_PATH / "test_multi_class_nest.py" + +nested_classes_expected_test_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "test_multi_class_nest.py", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "type_": "file", + "id_": str(TEST_MULTI_CLASS_NEST_PATH), + "children": [ + { + "name": "TestFirstClass", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "type_": "class", + "id_": "test_multi_class_nest.py::TestFirstClass", + "children": [ + { + "name": "TestSecondClass", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "type_": "class", + "id_": "test_multi_class_nest.py::TestFirstClass::TestSecondClass", + "children": [ + { + "name": "test_second", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "lineno": find_test_line_number( + "test_second", + str(TEST_MULTI_CLASS_NEST_PATH), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass::test_second", + TEST_MULTI_CLASS_NEST_PATH, + ), + "runID": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass::test_second", + TEST_MULTI_CLASS_NEST_PATH, + ), + } + ], + }, + { + "name": "test_first", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "lineno": find_test_line_number( + "test_first", str(TEST_MULTI_CLASS_NEST_PATH) + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::test_first", + TEST_MULTI_CLASS_NEST_PATH, + ), + "runID": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::test_first", + TEST_MULTI_CLASS_NEST_PATH, + ), + }, + { + "name": "TestSecondClass2", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "type_": "class", + "id_": "test_multi_class_nest.py::TestFirstClass::TestSecondClass2", + "children": [ + { + "name": "test_second2", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "lineno": find_test_line_number( + "test_second2", + str(TEST_MULTI_CLASS_NEST_PATH), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass2::test_second2", + TEST_MULTI_CLASS_NEST_PATH, + ), + "runID": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass2::test_second2", + TEST_MULTI_CLASS_NEST_PATH, + ), + } + ], + }, + ], + }, + { + "name": "test_independent", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "lineno": find_test_line_number( + "test_independent", str(TEST_MULTI_CLASS_NEST_PATH) + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::test_independent", + TEST_MULTI_CLASS_NEST_PATH, + ), + "runID": get_absolute_test_id( + "test_multi_class_nest.py::test_independent", + TEST_MULTI_CLASS_NEST_PATH, + ), + }, + ], + } + ], + "id_": str(TEST_DATA_PATH), +} diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index 44f3d3d0abce..cf8997a252d3 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -308,7 +308,7 @@ # └── test_top_folder.py # └── test_top_function_t: success # └── test_top_function_f: failure -# └── nested_folder_one +# └── z_nested_folder_one # └── test_bottom_folder.py # └── test_bottom_function_t: success # └── test_bottom_function_f: failure @@ -318,7 +318,7 @@ dual_level_nested_folder_bottom_path = ( TEST_DATA_PATH / "dual_level_nested_folder" - / "nested_folder_one" + / "z_nested_folder_one" / "test_bottom_folder.py" ) dual_level_nested_folder_execution_expected_output = { @@ -345,11 +345,11 @@ "subtest": None, }, get_absolute_test_id( - "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "z_nested_folder_one/test_bottom_folder.py::test_bottom_function_t", dual_level_nested_folder_bottom_path, ): { "test": get_absolute_test_id( - "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "z_nested_folder_one/test_bottom_folder.py::test_bottom_function_t", dual_level_nested_folder_bottom_path, ), "outcome": "success", @@ -358,11 +358,11 @@ "subtest": None, }, get_absolute_test_id( - "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + "z_nested_folder_one/test_bottom_folder.py::test_bottom_function_f", dual_level_nested_folder_bottom_path, ): { "test": get_absolute_test_id( - "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + "z_nested_folder_one/test_bottom_folder.py::test_bottom_function_f", dual_level_nested_folder_bottom_path, ), "outcome": "failure", @@ -479,7 +479,7 @@ dual_level_nested_folder_bottom_path = ( TEST_DATA_PATH / "dual_level_nested_folder" - / "nested_folder_one" + / "z_nested_folder_one" / "test_bottom_folder.py" ) unittest_folder_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_discovery.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/test_discovery.py index 674d92ac0545..2630ddef68b0 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/test_discovery.py @@ -123,6 +123,10 @@ def test_parameterized_error_collect(): @pytest.mark.parametrize( "file, expected_const", [ + ( + "test_multi_class_nest.py", + expected_discovery_test_output.nested_classes_expected_test_output, + ), ( "unittest_skiptest_file_level.py", expected_discovery_test_output.unittest_skip_file_level_expected_output, @@ -229,8 +233,7 @@ def test_pytest_config_file(): actual = runner_with_cwd( [ "--collect-only", - "-c", - "tests/pytest.ini", + "tests/", ], TEST_DATA_PATH / "root", ) diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py index dd32b61fa262..767d54a6cabe 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py @@ -193,8 +193,8 @@ def test_bad_id_error_execution(): [ "dual_level_nested_folder/test_top_folder.py::test_top_function_t", "dual_level_nested_folder/test_top_folder.py::test_top_function_f", - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + "dual_level_nested_folder/z_nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "dual_level_nested_folder/z_nested_folder_one/test_bottom_folder.py::test_bottom_function_f", ], expected_execution_test_output.dual_level_nested_folder_execution_expected_output, ), diff --git a/extensions/positron-python/pythonFiles/tests/test_shell_integration.py b/extensions/positron-python/pythonFiles/tests/test_shell_integration.py index 98e2b1f54116..896df416eced 100644 --- a/extensions/positron-python/pythonFiles/tests/test_shell_integration.py +++ b/extensions/positron-python/pythonFiles/tests/test_shell_integration.py @@ -1,48 +1,59 @@ -# import importlib -# from unittest.mock import Mock +import importlib +import sys +from unittest.mock import Mock +import pythonrc -# import pythonrc +def test_decoration_success(): + importlib.reload(pythonrc) + ps1 = pythonrc.ps1() -# def test_decoration_success(): -# importlib.reload(pythonrc) -# ps1 = pythonrc.ps1() + ps1.hooks.failure_flag = False + result = str(ps1) + if sys.platform != "win32": + assert ( + result + == "\x1b]633;D;0\x07\x1b]633;A\x07>>> \x1b]633;B\x07\x1b]633;C\x07\x1b]633;E;None\x07" + ) + else: + pass -# ps1.hooks.failure_flag = False -# result = str(ps1) -# assert result == "\x1b]633;D;0\x07\x1b]633;A\x07>>> \x1b]633;B\x07\x1b]633;C\x07" +def test_decoration_failure(): + importlib.reload(pythonrc) + ps1 = pythonrc.ps1() -# def test_decoration_failure(): -# importlib.reload(pythonrc) -# ps1 = pythonrc.ps1() + ps1.hooks.failure_flag = True + result = str(ps1) + if sys.platform != "win32": + assert ( + result + == "\x1b]633;D;1\x07\x1b]633;A\x07>>> \x1b]633;B\x07\x1b]633;C\x07\x1b]633;E;None\x07" + ) + else: + pass -# ps1.hooks.failure_flag = True -# result = str(ps1) -# assert result == "\x1b]633;D;1\x07\x1b]633;A\x07>>> \x1b]633;B\x07\x1b]633;C\x07" +def test_displayhook_call(): + importlib.reload(pythonrc) + pythonrc.ps1() + mock_displayhook = Mock() + hooks = pythonrc.repl_hooks() + hooks.original_displayhook = mock_displayhook -# def test_displayhook_call(): -# importlib.reload(pythonrc) -# pythonrc.ps1() -# mock_displayhook = Mock() + hooks.my_displayhook("mock_value") -# hooks = pythonrc.repl_hooks() -# hooks.original_displayhook = mock_displayhook + mock_displayhook.assert_called_once_with("mock_value") -# hooks.my_displayhook("mock_value") -# mock_displayhook.assert_called_once_with("mock_value") +def test_excepthook_call(): + importlib.reload(pythonrc) + pythonrc.ps1() + mock_excepthook = Mock() + hooks = pythonrc.repl_hooks() + hooks.original_excepthook = mock_excepthook -# def test_excepthook_call(): -# importlib.reload(pythonrc) -# pythonrc.ps1() -# mock_excepthook = Mock() - -# hooks = pythonrc.repl_hooks() -# hooks.original_excepthook = mock_excepthook - -# hooks.my_excepthook("mock_type", "mock_value", "mock_traceback") -# mock_excepthook.assert_called_once_with("mock_type", "mock_value", "mock_traceback") + hooks.my_excepthook("mock_type", "mock_value", "mock_traceback") + mock_excepthook.assert_called_once_with("mock_type", "mock_value", "mock_traceback") diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_complex_tree/__init__.py b/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_complex_tree/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/__init__.py b/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/__init__.py b/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/test_utils_complex_tree.py b/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/test_utils_complex_tree.py new file mode 100644 index 000000000000..8f57fb880ff1 --- /dev/null +++ b/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/test_utils_complex_tree.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + + +class TreeOne(unittest.TestCase): + def test_one(self): + assert True diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/expected_discovery_test_output.py b/extensions/positron-python/pythonFiles/tests/unittestadapter/expected_discovery_test_output.py index 3043ec158a2e..db509ebeca3c 100644 --- a/extensions/positron-python/pythonFiles/tests/unittestadapter/expected_discovery_test_output.py +++ b/extensions/positron-python/pythonFiles/tests/unittestadapter/expected_discovery_test_output.py @@ -2,8 +2,9 @@ # Licensed under the MIT License. import os -from unittestadapter.utils import TestNodeTypeEnum +from unittestadapter.pvsc_utils import TestNodeTypeEnum from .helpers import TEST_DATA_PATH +import pathlib skip_unittest_folder_discovery_output = { "path": os.fspath(TEST_DATA_PATH / "unittest_skip"), @@ -66,3 +67,87 @@ ], "id_": os.fspath(TEST_DATA_PATH / "unittest_skip"), } + +complex_tree_file_path = os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + "test_utils_complex_tree.py", + ) +) +complex_tree_expected_output = { + "name": "utils_complex_tree", + "type_": TestNodeTypeEnum.folder, + "path": os.fsdecode(pathlib.PurePath(TEST_DATA_PATH, "utils_complex_tree")), + "children": [ + { + "name": "test_outer_folder", + "type_": TestNodeTypeEnum.folder, + "path": os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, "utils_complex_tree", "test_outer_folder" + ) + ), + "children": [ + { + "name": "test_inner_folder", + "type_": TestNodeTypeEnum.folder, + "path": os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + ) + ), + "children": [ + { + "name": "test_utils_complex_tree.py", + "type_": TestNodeTypeEnum.file, + "path": complex_tree_file_path, + "children": [ + { + "name": "TreeOne", + "type_": TestNodeTypeEnum.class_, + "path": complex_tree_file_path, + "children": [ + { + "name": "test_one", + "type_": TestNodeTypeEnum.test, + "path": complex_tree_file_path, + "lineno": "7", + "id_": complex_tree_file_path + + "\\" + + "TreeOne" + + "\\" + + "test_one", + "runID": "utils_complex_tree.test_outer_folder.test_inner_folder.test_utils_complex_tree.TreeOne.test_one", + }, + ], + "id_": complex_tree_file_path + "\\" + "TreeOne", + } + ], + "id_": complex_tree_file_path, + } + ], + "id_": os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + ) + ), + }, + ], + "id_": os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, "utils_complex_tree", "test_outer_folder" + ) + ), + } + ], + "id_": os.fsdecode(pathlib.PurePath(TEST_DATA_PATH, "utils_complex_tree")), +} diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py b/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py index 7d7db772a4a4..4249ca4faef2 100644 --- a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py +++ b/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py @@ -7,7 +7,7 @@ import pytest from unittestadapter.discovery import discover_tests -from unittestadapter.utils import TestNodeTypeEnum, parse_unittest_args +from unittestadapter.pvsc_utils import TestNodeTypeEnum, parse_unittest_args from . import expected_discovery_test_output from .helpers import TEST_DATA_PATH, is_same_tree @@ -133,6 +133,71 @@ def test_simple_discovery() -> None: assert "error" not in actual +def test_simple_discovery_with_top_dir_calculated() -> None: + """The discover_tests function should return a dictionary with a "success" status, a uuid, no errors, and a test tree + if unittest discovery was performed successfully. + """ + start_dir = "." + pattern = "discovery_simple*" + file_path = os.fsdecode(pathlib.PurePath(TEST_DATA_PATH / "discovery_simple.py")) + + expected = { + "path": os.fsdecode(pathlib.PurePath(TEST_DATA_PATH)), + "type_": TestNodeTypeEnum.folder, + "name": ".data", + "children": [ + { + "name": "discovery_simple.py", + "type_": TestNodeTypeEnum.file, + "path": file_path, + "children": [ + { + "name": "DiscoverySimple", + "path": file_path, + "type_": TestNodeTypeEnum.class_, + "children": [ + { + "name": "test_one", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "14", + "id_": file_path + + "\\" + + "DiscoverySimple" + + "\\" + + "test_one", + }, + { + "name": "test_two", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "17", + "id_": file_path + + "\\" + + "DiscoverySimple" + + "\\" + + "test_two", + }, + ], + "id_": file_path + "\\" + "DiscoverySimple", + } + ], + "id_": file_path, + } + ], + "id_": os.fsdecode(pathlib.PurePath(TEST_DATA_PATH)), + } + + uuid = "some-uuid" + # Define the CWD to be the root of the test data folder. + os.chdir(os.fsdecode(pathlib.PurePath(TEST_DATA_PATH))) + actual = discover_tests(start_dir, pattern, None, uuid) + + assert actual["status"] == "success" + assert is_same_tree(actual.get("tests"), expected) + assert "error" not in actual + + def test_empty_discovery() -> None: """The discover_tests function should return a dictionary with a "success" status, a uuid, no errors, and no test tree if unittest discovery was performed successfully but no tests were found. @@ -231,3 +296,25 @@ def test_unit_skip() -> None: expected_discovery_test_output.skip_unittest_folder_discovery_output, ) assert "error" not in actual + + +def test_complex_tree() -> None: + """This test specifically tests when different start_dir and top_level_dir are provided.""" + start_dir = os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + ) + ) + pattern = "test_*.py" + top_level_dir = os.fsdecode(pathlib.PurePath(TEST_DATA_PATH, "utils_complex_tree")) + uuid = "some-uuid" + actual = discover_tests(start_dir, pattern, top_level_dir, uuid) + assert actual["status"] == "success" + assert "error" not in actual + assert is_same_tree( + actual.get("tests"), + expected_discovery_test_output.complex_tree_expected_output, + ) diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_utils.py b/extensions/positron-python/pythonFiles/tests/unittestadapter/test_utils.py index e262f877d52c..9fe2af8256cd 100644 --- a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_utils.py +++ b/extensions/positron-python/pythonFiles/tests/unittestadapter/test_utils.py @@ -7,7 +7,7 @@ import pytest -from unittestadapter.utils import ( +from unittestadapter.pvsc_utils import ( TestNode, TestNodeTypeEnum, build_test_tree, diff --git a/extensions/positron-python/pythonFiles/unittestadapter/discovery.py b/extensions/positron-python/pythonFiles/unittestadapter/discovery.py index e8f602a22fb3..db06004e02c9 100644 --- a/extensions/positron-python/pythonFiles/unittestadapter/discovery.py +++ b/extensions/positron-python/pythonFiles/unittestadapter/discovery.py @@ -17,7 +17,11 @@ from typing_extensions import Literal, NotRequired, TypedDict # If I use from utils then there will be an import error in test_discovery.py. -from unittestadapter.utils import TestNode, build_test_tree, parse_unittest_args +from unittestadapter.pvsc_utils import ( + TestNode, + build_test_tree, + parse_unittest_args, +) DEFAULT_PORT = 45454 @@ -86,7 +90,16 @@ def discover_tests( loader = unittest.TestLoader() suite = loader.discover(start_dir, pattern, top_level_dir) - tests, error = build_test_tree(suite, cwd) # test tree built succesfully here. + # If the top level directory is not provided, then use the start directory. + if top_level_dir is None: + top_level_dir = start_dir + + # Get abspath of top level directory for build_test_tree. + top_level_dir = os.path.abspath(top_level_dir) + + tests, error = build_test_tree( + suite, top_level_dir + ) # test tree built successfully here. except Exception: error.append(traceback.format_exc()) diff --git a/extensions/positron-python/pythonFiles/unittestadapter/execution.py b/extensions/positron-python/pythonFiles/unittestadapter/execution.py index 769d70afc0dd..22451c25bf1f 100644 --- a/extensions/positron-python/pythonFiles/unittestadapter/execution.py +++ b/extensions/positron-python/pythonFiles/unittestadapter/execution.py @@ -19,7 +19,7 @@ from testing_tools import process_json_util, socket_manager from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict -from unittestadapter.utils import parse_unittest_args +from unittestadapter.pvsc_utils import parse_unittest_args ErrorType = Union[ Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None] diff --git a/extensions/positron-python/pythonFiles/unittestadapter/utils.py b/extensions/positron-python/pythonFiles/unittestadapter/pvsc_utils.py similarity index 93% rename from extensions/positron-python/pythonFiles/unittestadapter/utils.py rename to extensions/positron-python/pythonFiles/unittestadapter/pvsc_utils.py index 2c5ebf09abc7..5632e69b09c7 100644 --- a/extensions/positron-python/pythonFiles/unittestadapter/utils.py +++ b/extensions/positron-python/pythonFiles/unittestadapter/pvsc_utils.py @@ -94,12 +94,13 @@ def build_test_node(path: str, name: str, type_: TestNodeTypeEnum) -> TestNode: def get_child_node( name: str, path: str, type_: TestNodeTypeEnum, root: TestNode ) -> TestNode: - """Find a child node in a test tree given its name and type. If the node doesn't exist, create it.""" + """Find a child node in a test tree given its name, type and path. If the node doesn't exist, create it. + Path is required to distinguish between nodes with the same name and type.""" try: result = next( node for node in root["children"] - if node["name"] == name and node["type_"] == type_ + if node["name"] == name and node["type_"] == type_ and node["path"] == path ) except StopIteration: result = build_test_node(path, name, type_) @@ -109,7 +110,7 @@ def get_child_node( def build_test_tree( - suite: unittest.TestSuite, test_directory: str + suite: unittest.TestSuite, top_level_directory: str ) -> Tuple[Union[TestNode, None], List[str]]: """Build a test tree from a unittest test suite. @@ -152,8 +153,10 @@ def build_test_tree( } """ error = [] - directory_path = pathlib.PurePath(test_directory) - root = build_test_node(test_directory, directory_path.name, TestNodeTypeEnum.folder) + directory_path = pathlib.PurePath(top_level_directory) + root = build_test_node( + top_level_directory, directory_path.name, TestNodeTypeEnum.folder + ) for test_case in get_test_case(suite): test_id = test_case.id() @@ -185,7 +188,7 @@ def build_test_tree( ) # Find/build file node. - path_components = [test_directory] + folders + [py_filename] + path_components = [top_level_directory] + folders + [py_filename] file_path = os.fsdecode(pathlib.PurePath("/".join(path_components))) current_node = get_child_node( py_filename, file_path, TestNodeTypeEnum.file, current_node diff --git a/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py b/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py index 306b360ad709..3b61156a59c3 100644 --- a/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py +++ b/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py @@ -391,25 +391,37 @@ def build_test_tree(session: pytest.Session) -> TestNode: for test_case in session.items: test_node = create_test_node(test_case) if isinstance(test_case.parent, pytest.Class): - try: - test_class_node = class_nodes_dict[test_case.parent.nodeid] - except KeyError: - test_class_node = create_class_node(test_case.parent) - class_nodes_dict[test_case.parent.nodeid] = test_class_node - test_class_node["children"].append(test_node) - if test_case.parent.parent: - parent_module = test_case.parent.parent + case_iter = test_case.parent + node_child_iter = test_node + test_class_node: Union[TestNode, None] = None + while isinstance(case_iter, pytest.Class): + # While the given node is a class, create a class and nest the previous node as a child. + try: + test_class_node = class_nodes_dict[case_iter.nodeid] + except KeyError: + test_class_node = create_class_node(case_iter) + class_nodes_dict[case_iter.nodeid] = test_class_node + test_class_node["children"].append(node_child_iter) + # Iterate up. + node_child_iter = test_class_node + case_iter = case_iter.parent + # Now the parent node is not a class node, it is a file node. + if case_iter: + parent_module = case_iter else: - ERRORS.append(f"Test class {test_case.parent} has no parent") + ERRORS.append(f"Test class {case_iter} has no parent") break - # Create a file node that has the class as a child. + # Create a file node that has the last class as a child. try: test_file_node: TestNode = file_nodes_dict[parent_module] except KeyError: test_file_node = create_file_node(parent_module) file_nodes_dict[parent_module] = test_file_node # Check if the class is already a child of the file node. - if test_class_node not in test_file_node["children"]: + if ( + test_class_node is not None + and test_class_node not in test_file_node["children"] + ): test_file_node["children"].append(test_class_node) elif hasattr(test_case, "callspec"): # This means it is a parameterized test. function_name: str = "" @@ -528,7 +540,7 @@ def create_session_node(session: pytest.Session) -> TestNode: """ node_path = get_node_path(session) return { - "name": session.name, + "name": node_path.name, "path": node_path, "type_": "folder", "children": [], diff --git a/extensions/positron-python/requirements.in b/extensions/positron-python/requirements.in index 22b599619ad3..16a49eec1d88 100644 --- a/extensions/positron-python/requirements.in +++ b/extensions/positron-python/requirements.in @@ -4,7 +4,7 @@ # 2) pip-compile --generate-hashes requirements.in # Unittest test adapter -typing-extensions==4.8.0 +typing-extensions==4.9.0 # Fallback env creator for debian microvenv diff --git a/extensions/positron-python/requirements.txt b/extensions/positron-python/requirements.txt index c24b0b48e391..838ab51d7e58 100644 --- a/extensions/positron-python/requirements.txt +++ b/extensions/positron-python/requirements.txt @@ -4,13 +4,13 @@ # # pip-compile --generate-hashes requirements.in # -importlib-metadata==6.7.0 \ - --hash=sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4 \ - --hash=sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5 +importlib-metadata==7.0.1 \ + --hash=sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e \ + --hash=sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc # via -r requirements.in -microvenv==2023.5 \ - --hash=sha256:128c0c8ab46e3bbd7b4c902c8a5d6333b694f9ebf871f123b473425cb6fbe19f \ - --hash=sha256:270977691d207d70308c4239221d2ffbbfd595fa1819d09680c75e8808b21254 +microvenv==2023.5.post1 \ + --hash=sha256:32c46afea874e300f69f1add0806eb0795fd02b5fb251092fba0b73c059a7d1f \ + --hash=sha256:fd79b3dfea7860e2e84c87dd0aa8a135075f7fa2284174842b7bdeb077a0d8ac # via -r requirements.in packaging==23.2 \ --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ @@ -20,9 +20,9 @@ tomli==2.0.1 \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f # via -r requirements.in -typing-extensions==4.8.0 \ - --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ - --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef +typing-extensions==4.9.0 \ + --hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \ + --hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd # via -r requirements.in zipp==3.15.0 \ --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ diff --git a/extensions/positron-python/src/client/application/diagnostics/serviceRegistry.ts b/extensions/positron-python/src/client/application/diagnostics/serviceRegistry.ts index 8d9b765939c9..acf460b88625 100644 --- a/extensions/positron-python/src/client/application/diagnostics/serviceRegistry.ts +++ b/extensions/positron-python/src/client/application/diagnostics/serviceRegistry.ts @@ -11,10 +11,6 @@ import { EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId, } from './checks/envPathVariable'; -import { - InvalidLaunchJsonDebuggerService, - InvalidLaunchJsonDebuggerServiceId, -} from './checks/invalidLaunchJsonDebugger'; import { InvalidPythonPathInDebuggerService, InvalidPythonPathInDebuggerServiceId, @@ -59,11 +55,6 @@ export function registerTypes(serviceManager: IServiceManager): void { EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId, ); - serviceManager.addSingleton( - IDiagnosticsService, - InvalidLaunchJsonDebuggerService, - InvalidLaunchJsonDebuggerServiceId, - ); serviceManager.addSingleton( IDiagnosticsService, InvalidPythonInterpreterService, diff --git a/extensions/positron-python/src/client/common/application/commands.ts b/extensions/positron-python/src/client/common/application/commands.ts index d0d6743d78a0..bcbf0d743d41 100644 --- a/extensions/positron-python/src/client/common/application/commands.ts +++ b/extensions/positron-python/src/client/common/application/commands.ts @@ -7,6 +7,7 @@ import { CancellationToken, Position, TextDocument, Uri } from 'vscode'; import { Commands as LSCommands } from '../../activation/commands'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from '../../tensorBoard/constants'; import { Channel, Commands, CommandSource } from '../constants'; +import { CreateEnvironmentOptions } from '../../pythonEnvironments/creation/proposed.createEnvApis'; export type CommandsWithoutArgs = keyof ICommandNameWithoutArgumentTypeMapping; @@ -56,6 +57,7 @@ export type AllCommands = keyof ICommandNameArgumentTypeMapping; * @extends {ICommandNameWithoutArgumentTypeMapping} */ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgumentTypeMapping { + [Commands.Create_Environment]: [CreateEnvironmentOptions]; ['vscode.openWith']: [Uri, string]; ['workbench.action.quickOpen']: [string]; ['workbench.action.openWalkthrough']: [string | { category: string; step: string }, boolean | undefined]; diff --git a/extensions/positron-python/src/client/common/constants.ts b/extensions/positron-python/src/client/common/constants.ts index 9ef4ccaa6417..7c9c31df1b96 100644 --- a/extensions/positron-python/src/client/common/constants.ts +++ b/extensions/positron-python/src/client/common/constants.ts @@ -77,12 +77,14 @@ export namespace Octicons { export const Test_Skip = '$(circle-slash)'; export const Downloading = '$(cloud-download)'; export const Installing = '$(desktop-download)'; + export const Search = '$(search)'; export const Search_Stop = '$(search-stop)'; export const Star = '$(star-full)'; export const Gear = '$(gear)'; export const Warning = '$(warning)'; export const Error = '$(error)'; export const Lightbulb = '$(lightbulb)'; + export const Folder = '$(folder)'; } /** diff --git a/extensions/positron-python/src/client/common/utils/localize.ts b/extensions/positron-python/src/client/common/utils/localize.ts index f09573614108..b997e168ce3e 100644 --- a/extensions/positron-python/src/client/common/utils/localize.ts +++ b/extensions/positron-python/src/client/common/utils/localize.ts @@ -259,6 +259,9 @@ export namespace InterpreterQuickPickList { }; export const refreshInterpreterList = l10n.t('Refresh Interpreter list'); export const refreshingInterpreterList = l10n.t('Refreshing Interpreter list...'); + export const create = { + label: l10n.t('Create Virtual Environment...'), + }; } export namespace OutputChannelNames { diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/debugConfigurationService.ts b/extensions/positron-python/src/client/debugger/extension/configuration/debugConfigurationService.ts index 80a1e3a8a8c4..9997fb4f0509 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/debugConfigurationService.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/debugConfigurationService.ts @@ -4,31 +4,13 @@ 'use strict'; import { inject, injectable, named } from 'inversify'; -import { cloneDeep } from 'lodash'; -import { CancellationToken, DebugConfiguration, QuickPickItem, WorkspaceFolder } from 'vscode'; -import { DebugConfigStrings } from '../../../common/utils/localize'; -import { - IMultiStepInputFactory, - InputStep, - IQuickPickParameters, - MultiStepInput, -} from '../../../common/utils/multiStepInput'; -import { AttachRequestArguments, DebugConfigurationArguments, LaunchRequestArguments } from '../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationService } from '../types'; -import { buildDjangoLaunchDebugConfiguration } from './providers/djangoLaunch'; -import { buildFastAPILaunchDebugConfiguration } from './providers/fastapiLaunch'; -import { buildFileLaunchDebugConfiguration } from './providers/fileLaunch'; -import { buildFlaskLaunchDebugConfiguration } from './providers/flaskLaunch'; -import { buildModuleLaunchConfiguration } from './providers/moduleLaunch'; -import { buildPidAttachConfiguration } from './providers/pidAttach'; -import { buildPyramidLaunchConfiguration } from './providers/pyramidLaunch'; -import { buildRemoteAttachConfiguration } from './providers/remoteAttach'; +import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode'; +import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; +import { IDebugConfigurationService } from '../types'; import { IDebugConfigurationResolver } from './types'; @injectable() export class PythonDebugConfigurationService implements IDebugConfigurationService { - private cacheDebugConfig: DebugConfiguration | undefined = undefined; - constructor( @inject(IDebugConfigurationResolver) @named('attach') @@ -36,26 +18,8 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi @inject(IDebugConfigurationResolver) @named('launch') private readonly launchResolver: IDebugConfigurationResolver, - @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, ) {} - public async provideDebugConfigurations( - folder: WorkspaceFolder | undefined, - token?: CancellationToken, - ): Promise { - const config: Partial = {}; - const state = { config, folder, token }; - - // Disabled until configuration issues are addressed by VS Code. See #4007 - const multiStep = this.multiStepFactory.create(); - await multiStep.run((input, s) => PythonDebugConfigurationService.pickDebugConfiguration(input, s), state); - - if (Object.keys(state.config).length !== 0) { - return [state.config as DebugConfiguration]; - } - return undefined; - } - public async resolveDebugConfiguration( folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, @@ -76,19 +40,7 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi ); } else { if (Object.keys(debugConfiguration).length === 0) { - if (this.cacheDebugConfig) { - debugConfiguration = cloneDeep(this.cacheDebugConfig); - } else { - const configs = await this.provideDebugConfigurations(folder, token); - if (configs === undefined) { - return undefined; - } - if (Array.isArray(configs) && configs.length === 1) { - // eslint-disable-next-line prefer-destructuring - debugConfiguration = configs[0]; - } - this.cacheDebugConfig = cloneDeep(debugConfiguration); - } + return undefined; } return this.launchResolver.resolveDebugConfiguration( folder, @@ -108,88 +60,4 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi } return debugConfiguration.request === 'attach' ? resolve(this.attachResolver) : resolve(this.launchResolver); } - - // eslint-disable-next-line consistent-return - protected static async pickDebugConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, - ): Promise | void> { - type DebugConfigurationQuickPickItemFunc = ( - input: MultiStepInput, - state: DebugConfigurationState, - ) => Promise>; - type DebugConfigurationQuickPickItem = QuickPickItem & { - type: DebugConfigurationType; - func: DebugConfigurationQuickPickItemFunc; - }; - const items: DebugConfigurationQuickPickItem[] = [ - { - func: buildFileLaunchDebugConfiguration, - label: DebugConfigStrings.file.selectConfiguration.label, - type: DebugConfigurationType.launchFile, - description: DebugConfigStrings.file.selectConfiguration.description, - }, - { - func: buildModuleLaunchConfiguration, - label: DebugConfigStrings.module.selectConfiguration.label, - type: DebugConfigurationType.launchModule, - description: DebugConfigStrings.module.selectConfiguration.description, - }, - { - func: buildRemoteAttachConfiguration, - label: DebugConfigStrings.attach.selectConfiguration.label, - type: DebugConfigurationType.remoteAttach, - description: DebugConfigStrings.attach.selectConfiguration.description, - }, - { - func: buildPidAttachConfiguration, - label: DebugConfigStrings.attachPid.selectConfiguration.label, - type: DebugConfigurationType.pidAttach, - description: DebugConfigStrings.attachPid.selectConfiguration.description, - }, - { - func: buildDjangoLaunchDebugConfiguration, - label: DebugConfigStrings.django.selectConfiguration.label, - type: DebugConfigurationType.launchDjango, - description: DebugConfigStrings.django.selectConfiguration.description, - }, - { - func: buildFastAPILaunchDebugConfiguration, - label: DebugConfigStrings.fastapi.selectConfiguration.label, - type: DebugConfigurationType.launchFastAPI, - description: DebugConfigStrings.fastapi.selectConfiguration.description, - }, - { - func: buildFlaskLaunchDebugConfiguration, - label: DebugConfigStrings.flask.selectConfiguration.label, - type: DebugConfigurationType.launchFlask, - description: DebugConfigStrings.flask.selectConfiguration.description, - }, - { - func: buildPyramidLaunchConfiguration, - label: DebugConfigStrings.pyramid.selectConfiguration.label, - type: DebugConfigurationType.launchPyramid, - description: DebugConfigStrings.pyramid.selectConfiguration.description, - }, - ]; - const debugConfigurations = new Map(); - for (const config of items) { - debugConfigurations.set(config.type, config.func); - } - - state.config = {}; - const pick = await input.showQuickPick< - DebugConfigurationQuickPickItem, - IQuickPickParameters - >({ - title: DebugConfigStrings.selectConfiguration.title, - placeholder: DebugConfigStrings.selectConfiguration.placeholder, - activeItem: items[0], - items, - }); - if (pick) { - const pickedDebugConfiguration = debugConfigurations.get(pick.type)!; - return pickedDebugConfiguration(input, state); - } - } } diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts b/extensions/positron-python/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts deleted file mode 100644 index e79f201d9367..000000000000 --- a/extensions/positron-python/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as fs from 'fs-extra'; -import { injectable } from 'inversify'; -import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode'; -import { IDynamicDebugConfigurationService } from '../types'; -import { DebuggerTypeName } from '../../constants'; -import { asyncFilter } from '../../../common/utils/arrayUtils'; -import { replaceAll } from '../../../common/stringUtils'; - -const workspaceFolderToken = '${workspaceFolder}'; - -@injectable() -export class DynamicPythonDebugConfigurationService implements IDynamicDebugConfigurationService { - // eslint-disable-next-line class-methods-use-this - public async provideDebugConfigurations( - folder: WorkspaceFolder, - _token?: CancellationToken, - ): Promise { - const providers = []; - - providers.push({ - name: 'Python: File', - type: DebuggerTypeName, - request: 'launch', - program: '${file}', - justMyCode: true, - }); - - const djangoManagePath = await DynamicPythonDebugConfigurationService.getDjangoPath(folder); - if (djangoManagePath) { - providers.push({ - name: 'Python: Django', - type: DebuggerTypeName, - request: 'launch', - program: `${workspaceFolderToken}${path.sep}${djangoManagePath}`, - args: ['runserver'], - django: true, - justMyCode: true, - }); - } - - const flaskPath = await DynamicPythonDebugConfigurationService.getFlaskPath(folder); - if (flaskPath) { - providers.push({ - name: 'Python: Flask', - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: path.relative(folder.uri.fsPath, flaskPath), - FLASK_DEBUG: '1', - }, - args: ['run', '--no-debugger', '--no-reload'], - jinja: true, - justMyCode: true, - }); - } - - let fastApiPath = await DynamicPythonDebugConfigurationService.getFastApiPath(folder); - if (fastApiPath) { - fastApiPath = replaceAll(path.relative(folder.uri.fsPath, fastApiPath), path.sep, '.').replace('.py', ''); - providers.push({ - name: 'Python: FastAPI', - type: DebuggerTypeName, - request: 'launch', - module: 'uvicorn', - args: [`${fastApiPath}:app`, '--reload'], - jinja: true, - justMyCode: true, - }); - } - - return providers; - } - - private static async getDjangoPath(folder: WorkspaceFolder) { - const regExpression = /execute_from_command_line\(/; - const possiblePaths = await DynamicPythonDebugConfigurationService.getPossiblePaths( - folder, - ['manage.py', '*/manage.py', 'app.py', '*/app.py'], - regExpression, - ); - return possiblePaths.length ? path.relative(folder.uri.fsPath, possiblePaths[0]) : null; - } - - private static async getFastApiPath(folder: WorkspaceFolder) { - const regExpression = /app\s*=\s*FastAPI\(/; - const fastApiPaths = await DynamicPythonDebugConfigurationService.getPossiblePaths( - folder, - ['main.py', 'app.py', '*/main.py', '*/app.py', '*/*/main.py', '*/*/app.py'], - regExpression, - ); - - return fastApiPaths.length ? fastApiPaths[0] : null; - } - - private static async getFlaskPath(folder: WorkspaceFolder) { - const regExpression = /app(?:lication)?\s*=\s*(?:flask\.)?Flask\(|def\s+(?:create|make)_app\(/; - const flaskPaths = await DynamicPythonDebugConfigurationService.getPossiblePaths( - folder, - ['__init__.py', 'app.py', 'wsgi.py', '*/__init__.py', '*/app.py', '*/wsgi.py'], - regExpression, - ); - - return flaskPaths.length ? flaskPaths[0] : null; - } - - private static async getPossiblePaths( - folder: WorkspaceFolder, - globPatterns: string[], - regex: RegExp, - ): Promise { - const foundPathsPromises = (await Promise.allSettled( - globPatterns.map( - async (pattern): Promise => - (await fs.pathExists(path.join(folder.uri.fsPath, pattern))) - ? [path.join(folder.uri.fsPath, pattern)] - : [], - ), - )) as { status: string; value: [] }[]; - const possiblePaths: string[] = []; - foundPathsPromises.forEach((result) => possiblePaths.push(...result.value)); - const finalPaths = await asyncFilter(possiblePaths, async (possiblePath) => - regex.exec((await fs.readFile(possiblePath)).toString()), - ); - - return finalPaths; - } -} diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/completionProvider.ts b/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/completionProvider.ts deleted file mode 100644 index c3b243fe9065..000000000000 --- a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/completionProvider.ts +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { getLocation } from 'jsonc-parser'; -import * as path from 'path'; -import { - CancellationToken, - CompletionItem, - CompletionItemKind, - CompletionItemProvider, - Position, - SnippetString, - TextDocument, -} from 'vscode'; -import { IExtensionSingleActivationService } from '../../../../activation/types'; -import { ILanguageService } from '../../../../common/application/types'; -import { IDisposableRegistry } from '../../../../common/types'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; - -const configurationNodeName = 'configurations'; -enum JsonLanguages { - json = 'json', - jsonWithComments = 'jsonc', -} - -@injectable() -export class LaunchJsonCompletionProvider implements CompletionItemProvider, IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - constructor( - @inject(ILanguageService) private readonly languageService: ILanguageService, - @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, - ) {} - - public async activate(): Promise { - this.disposableRegistry.push( - this.languageService.registerCompletionItemProvider({ language: JsonLanguages.json }, this), - ); - this.disposableRegistry.push( - this.languageService.registerCompletionItemProvider({ language: JsonLanguages.jsonWithComments }, this), - ); - } - - // eslint-disable-next-line class-methods-use-this - public async provideCompletionItems( - document: TextDocument, - position: Position, - token: CancellationToken, - ): Promise { - if (!LaunchJsonCompletionProvider.canProvideCompletions(document, position)) { - return []; - } - - return [ - { - command: { - command: 'python.SelectAndInsertDebugConfiguration', - title: DebugConfigStrings.launchJsonCompletions.description, - arguments: [document, position, token], - }, - documentation: DebugConfigStrings.launchJsonCompletions.description, - sortText: 'AAAA', - preselect: true, - kind: CompletionItemKind.Enum, - label: DebugConfigStrings.launchJsonCompletions.label, - insertText: new SnippetString(), - }, - ]; - } - - public static canProvideCompletions(document: TextDocument, position: Position): boolean { - if (path.basename(document.uri.fsPath) !== 'launch.json') { - return false; - } - const location = getLocation(document.getText(), document.offsetAt(position)); - // Cursor must be inside the configurations array and not in any nested items. - // Hence path[0] = array, path[1] = array element index. - return location.path[0] === configurationNodeName && location.path.length === 2; - } -} diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/updaterService.ts b/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/updaterService.ts deleted file mode 100644 index b95749040f3c..000000000000 --- a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/updaterService.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { IExtensionSingleActivationService } from '../../../../activation/types'; -import { IDisposableRegistry } from '../../../../common/types'; -import { registerCommand } from '../../../../common/vscodeApis/commandApis'; -import { IDebugConfigurationService } from '../../types'; -import { LaunchJsonUpdaterServiceHelper } from './updaterServiceHelper'; - -@injectable() -export class LaunchJsonUpdaterService implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - constructor( - @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, - @inject(IDebugConfigurationService) private readonly configurationProvider: IDebugConfigurationService, - ) {} - - public async activate(): Promise { - const handler = new LaunchJsonUpdaterServiceHelper(this.configurationProvider); - this.disposableRegistry.push( - registerCommand('python.SelectAndInsertDebugConfiguration', handler.selectAndInsertDebugConfig, handler), - ); - } -} diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/updaterServiceHelper.ts b/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/updaterServiceHelper.ts deleted file mode 100644 index bc0820fa188f..000000000000 --- a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/updaterServiceHelper.ts +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { createScanner, parse, SyntaxKind } from 'jsonc-parser'; -import { CancellationToken, DebugConfiguration, Position, Range, TextDocument, WorkspaceEdit } from 'vscode'; -import { noop } from '../../../../common/utils/misc'; -import { executeCommand } from '../../../../common/vscodeApis/commandApis'; -import { getActiveTextEditor } from '../../../../common/vscodeApis/windowApis'; -import { applyEdit, getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; -import { captureTelemetry } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { IDebugConfigurationService } from '../../types'; - -type PositionOfCursor = 'InsideEmptyArray' | 'BeforeItem' | 'AfterItem'; -type PositionOfComma = 'BeforeCursor'; - -export class LaunchJsonUpdaterServiceHelper { - constructor(private readonly configurationProvider: IDebugConfigurationService) {} - - @captureTelemetry(EventName.DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON) - public async selectAndInsertDebugConfig( - document: TextDocument, - position: Position, - token: CancellationToken, - ): Promise { - const activeTextEditor = getActiveTextEditor(); - if (activeTextEditor && activeTextEditor.document === document) { - const folder = getWorkspaceFolder(document.uri); - const configs = await this.configurationProvider.provideDebugConfigurations!(folder, token); - - if (!token.isCancellationRequested && Array.isArray(configs) && configs.length > 0) { - // Always use the first available debug configuration. - await LaunchJsonUpdaterServiceHelper.insertDebugConfiguration(document, position, configs[0]); - } - } - } - - /** - * Inserts the debug configuration into the document. - * Invokes the document formatter to ensure JSON is formatted nicely. - * @param {TextDocument} document - * @param {Position} position - * @param {DebugConfiguration} config - * @returns {Promise} - * @memberof LaunchJsonCompletionItemProvider - */ - public static async insertDebugConfiguration( - document: TextDocument, - position: Position, - config: DebugConfiguration, - ): Promise { - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document, - position, - ); - if (!cursorPosition) { - return; - } - const commaPosition = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document, position) - ? 'BeforeCursor' - : undefined; - const formattedJson = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, cursorPosition, commaPosition); - const workspaceEdit = new WorkspaceEdit(); - workspaceEdit.insert(document.uri, position, formattedJson); - await applyEdit(workspaceEdit); - executeCommand('editor.action.formatDocument').then(noop, noop); - } - - /** - * Gets the string representation of the debug config for insertion in the document. - * Adds necessary leading or trailing commas (remember the text is added into an array). - * @param {DebugConfiguration} config - * @param {PositionOfCursor} cursorPosition - * @param {PositionOfComma} [commaPosition] - * @returns - * @memberof LaunchJsonCompletionItemProvider - */ - public static getTextForInsertion( - config: DebugConfiguration, - cursorPosition: PositionOfCursor, - commaPosition?: PositionOfComma, - ): string { - const json = JSON.stringify(config); - if (cursorPosition === 'AfterItem') { - // If we already have a comma immediatley before the cursor, then no need of adding a comma. - return commaPosition === 'BeforeCursor' ? json : `,${json}`; - } - if (cursorPosition === 'BeforeItem') { - return `${json},`; - } - return json; - } - - public static getCursorPositionInConfigurationsArray( - document: TextDocument, - position: Position, - ): PositionOfCursor | undefined { - if (LaunchJsonUpdaterServiceHelper.isConfigurationArrayEmpty(document)) { - return 'InsideEmptyArray'; - } - const scanner = createScanner(document.getText(), true); - scanner.setPosition(document.offsetAt(position)); - const nextToken = scanner.scan(); - if (nextToken === SyntaxKind.CommaToken || nextToken === SyntaxKind.CloseBracketToken) { - return 'AfterItem'; - } - if (nextToken === SyntaxKind.OpenBraceToken) { - return 'BeforeItem'; - } - return undefined; - } - - public static isConfigurationArrayEmpty(document: TextDocument): boolean { - const configuration = parse(document.getText(), [], { allowTrailingComma: true, disallowComments: false }) as { - configurations: []; - }; - return ( - !configuration || !Array.isArray(configuration.configurations) || configuration.configurations.length === 0 - ); - } - - public static isCommaImmediatelyBeforeCursor(document: TextDocument, position: Position): boolean { - const line = document.lineAt(position.line); - // Get text from start of line until the cursor. - const currentLine = document.getText(new Range(line.range.start, position)); - if (currentLine.trim().endsWith(',')) { - return true; - } - // If there are other characters, then don't bother. - if (currentLine.trim().length !== 0) { - return false; - } - - // Keep walking backwards until we hit a non-comma character or a comm character. - let startLineNumber = position.line - 1; - while (startLineNumber > 0) { - const lineText = document.lineAt(startLineNumber).text; - if (lineText.trim().endsWith(',')) { - return true; - } - // If there are other characters, then don't bother. - if (lineText.trim().length !== 0) { - return false; - } - startLineNumber -= 1; - } - return false; - } -} diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/providers/djangoLaunch.ts b/extensions/positron-python/src/client/debugger/extension/configuration/providers/djangoLaunch.ts deleted file mode 100644 index 4e1513ccb1ea..000000000000 --- a/extensions/positron-python/src/client/debugger/extension/configuration/providers/djangoLaunch.ts +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as vscode from 'vscode'; -import * as path from 'path'; -import * as fs from 'fs-extra'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; -import { resolveVariables } from '../utils/common'; - -const workspaceFolderToken = '${workspaceFolder}'; - -export async function buildDjangoLaunchDebugConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const program = await getManagePyPath(state.folder); - let manuallyEnteredAValue: boolean | undefined; - const defaultProgram = `${workspaceFolderToken}${path.sep}manage.py`; - const config: Partial = { - name: DebugConfigStrings.django.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: program || defaultProgram, - args: ['runserver'], - django: true, - justMyCode: true, - }; - if (!program) { - const selectedProgram = await input.showInputBox({ - title: DebugConfigStrings.django.enterManagePyPath.title, - value: defaultProgram, - prompt: DebugConfigStrings.django.enterManagePyPath.prompt, - validate: (value) => validateManagePy(state.folder, defaultProgram, value), - }); - if (selectedProgram) { - manuallyEnteredAValue = true; - config.program = selectedProgram; - } - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchDjango, - autoDetectedDjangoManagePyPath: !!program, - manuallyEnteredAValue, - }); - - Object.assign(state.config, config); -} - -export async function validateManagePy( - folder: vscode.WorkspaceFolder | undefined, - defaultValue: string, - selected?: string, -): Promise { - const error = DebugConfigStrings.django.enterManagePyPath.invalid; - if (!selected || selected.trim().length === 0) { - return error; - } - const resolvedPath = resolveVariables(selected, undefined, folder); - if (resolvedPath) { - if (selected !== defaultValue && !(await fs.pathExists(resolvedPath))) { - return error; - } - if (!resolvedPath.trim().toLowerCase().endsWith('.py')) { - return error; - } - } - return undefined; -} - -export async function getManagePyPath(folder: vscode.WorkspaceFolder | undefined): Promise { - if (!folder) { - return undefined; - } - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'manage.py'); - if (await fs.pathExists(defaultLocationOfManagePy)) { - return `${workspaceFolderToken}${path.sep}manage.py`; - } - return undefined; -} diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts b/extensions/positron-python/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts deleted file mode 100644 index 38a9b7ccf1a2..000000000000 --- a/extensions/positron-python/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as fs from 'fs-extra'; -import { WorkspaceFolder } from 'vscode'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -export async function buildFastAPILaunchDebugConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const application = await getApplicationPath(state.folder); - let manuallyEnteredAValue: boolean | undefined; - const config: Partial = { - name: DebugConfigStrings.fastapi.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'uvicorn', - args: ['main:app', '--reload'], - jinja: true, - justMyCode: true, - }; - - if (!application && config.args) { - const selectedPath = await input.showInputBox({ - title: DebugConfigStrings.fastapi.enterAppPathOrNamePath.title, - value: 'main.py', - prompt: DebugConfigStrings.fastapi.enterAppPathOrNamePath.prompt, - validate: (value) => - Promise.resolve( - value && value.trim().length > 0 - ? undefined - : DebugConfigStrings.fastapi.enterAppPathOrNamePath.invalid, - ), - }); - if (selectedPath) { - manuallyEnteredAValue = true; - config.args[0] = `${path.basename(selectedPath, '.py').replace('/', '.')}:app`; - } - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchFastAPI, - autoDetectedFastAPIMainPyPath: !!application, - manuallyEnteredAValue, - }); - Object.assign(state.config, config); -} -export async function getApplicationPath(folder: WorkspaceFolder | undefined): Promise { - if (!folder) { - return undefined; - } - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'main.py'); - if (await fs.pathExists(defaultLocationOfManagePy)) { - return 'main.py'; - } - return undefined; -} diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/providers/fileLaunch.ts b/extensions/positron-python/src/client/debugger/extension/configuration/providers/fileLaunch.ts deleted file mode 100644 index edda7ed7e22d..000000000000 --- a/extensions/positron-python/src/client/debugger/extension/configuration/providers/fileLaunch.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -export async function buildFileLaunchDebugConfiguration( - _input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const config: Partial = { - name: DebugConfigStrings.file.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: '${file}', - console: 'integratedTerminal', - justMyCode: true, - }; - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchFile, - }); - Object.assign(state.config, config); -} diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/providers/flaskLaunch.ts b/extensions/positron-python/src/client/debugger/extension/configuration/providers/flaskLaunch.ts deleted file mode 100644 index d85258c800c6..000000000000 --- a/extensions/positron-python/src/client/debugger/extension/configuration/providers/flaskLaunch.ts +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as fs from 'fs-extra'; -import { WorkspaceFolder } from 'vscode'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -export async function buildFlaskLaunchDebugConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const application = await getApplicationPath(state.folder); - let manuallyEnteredAValue: boolean | undefined; - const config: Partial = { - name: DebugConfigStrings.flask.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: application || 'app.py', - FLASK_DEBUG: '1', - }, - args: ['run', '--no-debugger', '--no-reload'], - jinja: true, - justMyCode: true, - }; - - if (!application) { - const selectedApp = await input.showInputBox({ - title: DebugConfigStrings.flask.enterAppPathOrNamePath.title, - value: 'app.py', - prompt: DebugConfigStrings.flask.enterAppPathOrNamePath.prompt, - validate: (value) => - Promise.resolve( - value && value.trim().length > 0 - ? undefined - : DebugConfigStrings.flask.enterAppPathOrNamePath.invalid, - ), - }); - if (selectedApp) { - manuallyEnteredAValue = true; - config.env!.FLASK_APP = selectedApp; - } - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchFlask, - autoDetectedFlaskAppPyPath: !!application, - manuallyEnteredAValue, - }); - Object.assign(state.config, config); -} -export async function getApplicationPath(folder: WorkspaceFolder | undefined): Promise { - if (!folder) { - return undefined; - } - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'app.py'); - if (await fs.pathExists(defaultLocationOfManagePy)) { - return 'app.py'; - } - return undefined; -} diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/providers/moduleLaunch.ts b/extensions/positron-python/src/client/debugger/extension/configuration/providers/moduleLaunch.ts deleted file mode 100644 index 16787296ce7c..000000000000 --- a/extensions/positron-python/src/client/debugger/extension/configuration/providers/moduleLaunch.ts +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -export async function buildModuleLaunchConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - let manuallyEnteredAValue: boolean | undefined; - const config: Partial = { - name: DebugConfigStrings.module.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: DebugConfigStrings.module.snippet.default, - justMyCode: true, - }; - const selectedModule = await input.showInputBox({ - title: DebugConfigStrings.module.enterModule.title, - value: config.module || DebugConfigStrings.module.enterModule.default, - prompt: DebugConfigStrings.module.enterModule.prompt, - validate: (value) => - Promise.resolve( - value && value.trim().length > 0 ? undefined : DebugConfigStrings.module.enterModule.invalid, - ), - }); - if (selectedModule) { - manuallyEnteredAValue = true; - config.module = selectedModule; - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchModule, - manuallyEnteredAValue, - }); - Object.assign(state.config, config); -} diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/providers/pidAttach.ts b/extensions/positron-python/src/client/debugger/extension/configuration/providers/pidAttach.ts deleted file mode 100644 index fc0d66874470..000000000000 --- a/extensions/positron-python/src/client/debugger/extension/configuration/providers/pidAttach.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { AttachRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -export async function buildPidAttachConfiguration( - _input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const config: Partial = { - name: DebugConfigStrings.attachPid.snippet.name, - type: DebuggerTypeName, - request: 'attach', - processId: '${command:pickProcess}', - justMyCode: true, - }; - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.pidAttach, - }); - Object.assign(state.config, config); -} diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts b/extensions/positron-python/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts deleted file mode 100644 index 315e204e7bf8..000000000000 --- a/extensions/positron-python/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as fs from 'fs-extra'; -import { l10n, WorkspaceFolder } from 'vscode'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; -import { resolveVariables } from '../utils/common'; - -const workspaceFolderToken = '${workspaceFolder}'; - -export async function buildPyramidLaunchConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const iniPath = await getDevelopmentIniPath(state.folder); - const defaultIni = `${workspaceFolderToken}${path.sep}development.ini`; - let manuallyEnteredAValue: boolean | undefined; - - const config: Partial = { - name: DebugConfigStrings.pyramid.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'pyramid.scripts.pserve', - args: [iniPath || defaultIni], - pyramid: true, - jinja: true, - justMyCode: true, - }; - - if (!iniPath) { - const selectedIniPath = await input.showInputBox({ - title: DebugConfigStrings.pyramid.enterDevelopmentIniPath.title, - value: defaultIni, - prompt: l10n.t( - 'Enter the path to development.ini ({0} points to the root of the current workspace folder)', - workspaceFolderToken, - ), - validate: (value) => validateIniPath(state ? state.folder : undefined, defaultIni, value), - }); - if (selectedIniPath) { - manuallyEnteredAValue = true; - config.args = [selectedIniPath]; - } - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchPyramid, - autoDetectedPyramidIniPath: !!iniPath, - manuallyEnteredAValue, - }); - Object.assign(state.config, config); -} - -export async function validateIniPath( - folder: WorkspaceFolder | undefined, - defaultValue: string, - selected?: string, -): Promise { - if (!folder) { - return undefined; - } - const error = DebugConfigStrings.pyramid.enterDevelopmentIniPath.invalid; - if (!selected || selected.trim().length === 0) { - return error; - } - const resolvedPath = resolveVariables(selected, undefined, folder); - if (resolvedPath) { - if (selected !== defaultValue && !fs.pathExists(resolvedPath)) { - return error; - } - if (!resolvedPath.trim().toLowerCase().endsWith('.ini')) { - return error; - } - } - return undefined; -} - -export async function getDevelopmentIniPath(folder: WorkspaceFolder | undefined): Promise { - if (!folder) { - return undefined; - } - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'development.ini'); - if (await fs.pathExists(defaultLocationOfManagePy)) { - return `${workspaceFolderToken}${path.sep}development.ini`; - } - return undefined; -} diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/providers/remoteAttach.ts b/extensions/positron-python/src/client/debugger/extension/configuration/providers/remoteAttach.ts deleted file mode 100644 index a43c48b664af..000000000000 --- a/extensions/positron-python/src/client/debugger/extension/configuration/providers/remoteAttach.ts +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { InputStep, MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { AttachRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; -import { configurePort } from '../utils/configuration'; - -const defaultHost = 'localhost'; -const defaultPort = 5678; - -export async function buildRemoteAttachConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise | void> { - const config: Partial = { - name: DebugConfigStrings.attach.snippet.name, - type: DebuggerTypeName, - request: 'attach', - connect: { - host: defaultHost, - port: defaultPort, - }, - pathMappings: [ - { - localRoot: '${workspaceFolder}', - remoteRoot: '.', - }, - ], - justMyCode: true, - }; - - const connect = config.connect!; - connect.host = await input.showInputBox({ - title: DebugConfigStrings.attach.enterRemoteHost.title, - step: 1, - totalSteps: 2, - value: connect.host || defaultHost, - prompt: DebugConfigStrings.attach.enterRemoteHost.prompt, - validate: (value) => - Promise.resolve( - value && value.trim().length > 0 ? undefined : DebugConfigStrings.attach.enterRemoteHost.invalid, - ), - }); - if (!connect.host) { - connect.host = defaultHost; - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.remoteAttach, - manuallyEnteredAValue: connect.host !== defaultHost, - }); - Object.assign(state.config, config); - return (_) => configurePort(input, state.config); -} diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/utils/configuration.ts b/extensions/positron-python/src/client/debugger/extension/configuration/utils/configuration.ts deleted file mode 100644 index 37fb500dbfdd..000000000000 --- a/extensions/positron-python/src/client/debugger/extension/configuration/utils/configuration.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { AttachRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -const defaultPort = 5678; - -export async function configurePort( - input: MultiStepInput, - config: Partial, -): Promise { - const connect = config.connect || (config.connect = {}); - const port = await input.showInputBox({ - title: DebugConfigStrings.attach.enterRemotePort.title, - step: 2, - totalSteps: 2, - value: (connect.port || defaultPort).toString(), - prompt: DebugConfigStrings.attach.enterRemotePort.prompt, - validate: (value) => - Promise.resolve( - value && /^\d+$/.test(value.trim()) ? undefined : DebugConfigStrings.attach.enterRemotePort.invalid, - ), - }); - if (port && /^\d+$/.test(port.trim())) { - connect.port = parseInt(port, 10); - } - if (!connect.port) { - connect.port = defaultPort; - } - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.remoteAttach, - manuallyEnteredAValue: connect.port !== defaultPort, - }); -} diff --git a/extensions/positron-python/src/client/debugger/extension/helpers/protocolParser.ts b/extensions/positron-python/src/client/debugger/extension/helpers/protocolParser.ts deleted file mode 100644 index c0d1306a841b..000000000000 --- a/extensions/positron-python/src/client/debugger/extension/helpers/protocolParser.ts +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { EventEmitter } from 'events'; -import { injectable } from 'inversify'; -import { Readable } from 'stream'; -import { DebugProtocol } from 'vscode-debugprotocol'; -import { IProtocolParser } from '../types'; - -const PROTOCOL_START_INDENTIFIER = '\r\n\r\n'; - -type Listener = (...args: unknown[]) => void; - -/** - * Parsers the debugger Protocol messages and raises the following events: - * 1. 'data', message (for all protocol messages) - * 1. 'event_', message (for all protocol events) - * 1. 'request_', message (for all protocol requests) - * 1. 'response_', message (for all protocol responses) - * 1. '', message (for all protocol messages that are not events, requests nor responses) - * @export - * @class ProtocolParser - * @extends {EventEmitter} - * @implements {IProtocolParser} - */ -@injectable() -export class ProtocolParser implements IProtocolParser { - private rawData = Buffer.alloc(0); - - private contentLength = -1; - - private disposed = false; - - private stream?: Readable; - - private events: EventEmitter; - - constructor() { - this.events = new EventEmitter(); - } - - public dispose(): void { - if (this.stream) { - this.stream.removeListener('data', this.dataCallbackHandler); - this.stream = undefined; - } - } - - public connect(stream: Readable): void { - this.stream = stream; - stream.addListener('data', this.dataCallbackHandler); - } - - public on(event: string | symbol, listener: Listener): this { - this.events.on(event, listener); - return this; - } - - public once(event: string | symbol, listener: Listener): this { - this.events.once(event, listener); - return this; - } - - private dataCallbackHandler = (data: string | Buffer) => { - this.handleData(data as Buffer); - }; - - private dispatch(body: string): void { - const message = JSON.parse(body) as DebugProtocol.ProtocolMessage; - - switch (message.type) { - case 'event': { - const event = message as DebugProtocol.Event; - if (typeof event.event === 'string') { - this.events.emit(`${message.type}_${event.event}`, event); - } - break; - } - case 'request': { - const request = message as DebugProtocol.Request; - if (typeof request.command === 'string') { - this.events.emit(`${message.type}_${request.command}`, request); - } - break; - } - case 'response': { - const reponse = message as DebugProtocol.Response; - if (typeof reponse.command === 'string') { - this.events.emit(`${message.type}_${reponse.command}`, reponse); - } - break; - } - default: { - this.events.emit(`${message.type}`, message); - } - } - - this.events.emit('data', message); - } - - private handleData(data: Buffer): void { - if (this.disposed) { - return; - } - this.rawData = Buffer.concat([this.rawData, data]); - - // eslint-disable-next-line no-constant-condition - while (true) { - if (this.contentLength >= 0) { - if (this.rawData.length >= this.contentLength) { - const message = this.rawData.toString('utf8', 0, this.contentLength); - this.rawData = this.rawData.slice(this.contentLength); - this.contentLength = -1; - if (message.length > 0) { - this.dispatch(message); - } - // there may be more complete messages to process. - // eslint-disable-next-line no-continue - continue; - } - } else { - const idx = this.rawData.indexOf(PROTOCOL_START_INDENTIFIER); - if (idx !== -1) { - const header = this.rawData.toString('utf8', 0, idx); - const lines = header.split('\r\n'); - for (const line of lines) { - const pair = line.split(/: +/); - if (pair[0] === 'Content-Length') { - this.contentLength = +pair[1]; - } - } - this.rawData = this.rawData.slice(idx + PROTOCOL_START_INDENTIFIER.length); - // eslint-disable-next-line no-continue - continue; - } - } - break; - } - } -} diff --git a/extensions/positron-python/src/client/debugger/extension/serviceRegistry.ts b/extensions/positron-python/src/client/debugger/extension/serviceRegistry.ts index a8c5ae7bbfcc..7734e87124cd 100644 --- a/extensions/positron-python/src/client/debugger/extension/serviceRegistry.ts +++ b/extensions/positron-python/src/client/debugger/extension/serviceRegistry.ts @@ -13,10 +13,6 @@ import { OutdatedDebuggerPromptFactory } from './adapter/outdatedDebuggerPrompt' import { AttachProcessProviderFactory } from './attachQuickPick/factory'; import { IAttachProcessProviderFactory } from './attachQuickPick/types'; import { PythonDebugConfigurationService } from './configuration/debugConfigurationService'; -import { DynamicPythonDebugConfigurationService } from './configuration/dynamicdebugConfigurationService'; -import { LaunchJsonCompletionProvider } from './configuration/launch.json/completionProvider'; -import { InterpreterPathCommand } from './configuration/launch.json/interpreterPathCommand'; -import { LaunchJsonUpdaterService } from './configuration/launch.json/updaterService'; import { AttachConfigurationResolver } from './configuration/resolvers/attach'; import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService } from './configuration/resolvers/helper'; import { LaunchConfigurationResolver } from './configuration/resolvers/launch'; @@ -29,31 +25,14 @@ import { IDebugAdapterDescriptorFactory, IDebugConfigurationService, IDebugSessionLoggingFactory, - IDynamicDebugConfigurationService, IOutdatedDebuggerPromptFactory, } from './types'; export function registerTypes(serviceManager: IServiceManager): void { - serviceManager.addSingleton( - IExtensionSingleActivationService, - LaunchJsonCompletionProvider, - ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - InterpreterPathCommand, - ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - LaunchJsonUpdaterService, - ); serviceManager.addSingleton( IDebugConfigurationService, PythonDebugConfigurationService, ); - serviceManager.addSingleton( - IDynamicDebugConfigurationService, - DynamicPythonDebugConfigurationService, - ); serviceManager.addSingleton(IChildProcessAttachService, ChildProcessAttachService); serviceManager.addSingleton(IDebugSessionEventHandlers, ChildProcessAttachEventHandler); serviceManager.addSingleton>( diff --git a/extensions/positron-python/src/client/debugger/extension/types.ts b/extensions/positron-python/src/client/debugger/extension/types.ts index 2a304efae918..4a8f35e2b808 100644 --- a/extensions/positron-python/src/client/debugger/extension/types.ts +++ b/extensions/positron-python/src/client/debugger/extension/types.ts @@ -3,46 +3,11 @@ 'use strict'; -import { Readable } from 'stream'; -import { - CancellationToken, - DebugAdapterDescriptorFactory, - DebugAdapterTrackerFactory, - DebugConfigurationProvider, - Disposable, - WorkspaceFolder, -} from 'vscode'; - -import { DebugConfigurationArguments } from '../types'; +import { DebugAdapterDescriptorFactory, DebugAdapterTrackerFactory, DebugConfigurationProvider } from 'vscode'; export const IDebugConfigurationService = Symbol('IDebugConfigurationService'); export interface IDebugConfigurationService extends DebugConfigurationProvider {} -export const IDynamicDebugConfigurationService = Symbol('IDynamicDebugConfigurationService'); -export interface IDynamicDebugConfigurationService extends DebugConfigurationProvider {} - -export type DebugConfigurationState = { - config: Partial; - folder?: WorkspaceFolder; - token?: CancellationToken; -}; - -export enum DebugConfigurationType { - launchFile = 'launchFile', - remoteAttach = 'remoteAttach', - launchDjango = 'launchDjango', - launchFastAPI = 'launchFastAPI', - launchFlask = 'launchFlask', - launchModule = 'launchModule', - launchPyramid = 'launchPyramid', - pidAttach = 'pidAttach', -} - -export enum PythonPathSource { - launchJson = 'launch.json', - settingsJson = 'settings.json', -} - export const IDebugAdapterDescriptorFactory = Symbol('IDebugAdapterDescriptorFactory'); export interface IDebugAdapterDescriptorFactory extends DebugAdapterDescriptorFactory {} @@ -54,9 +19,7 @@ export const IOutdatedDebuggerPromptFactory = Symbol('IOutdatedDebuggerPromptFac export interface IOutdatedDebuggerPromptFactory extends DebugAdapterTrackerFactory {} -export const IProtocolParser = Symbol('IProtocolParser'); -export interface IProtocolParser extends Disposable { - connect(stream: Readable): void; - once(event: string | symbol, listener: (...args: unknown[]) => void): this; - on(event: string | symbol, listener: (...args: unknown[]) => void): this; +export enum PythonPathSource { + launchJson = 'launch.json', + settingsJson = 'settings.json', } diff --git a/extensions/positron-python/src/client/debugger/types.ts b/extensions/positron-python/src/client/debugger/types.ts index 60e82fb04418..3e884cf8f64f 100644 --- a/extensions/positron-python/src/client/debugger/types.ts +++ b/extensions/positron-python/src/client/debugger/types.ts @@ -61,6 +61,7 @@ interface ICommonDebugArguments { pathMappings?: PathMapping[]; clientOS?: 'windows' | 'unix'; } + interface IKnownAttachDebugArguments extends ICommonDebugArguments { workspaceFolder?: string; customDebugger?: boolean; diff --git a/extensions/positron-python/src/client/extensionActivation.ts b/extensions/positron-python/src/client/extensionActivation.ts index c4b663fdba6d..cd9f99d400db 100644 --- a/extensions/positron-python/src/client/extensionActivation.ts +++ b/extensions/positron-python/src/client/extensionActivation.ts @@ -3,7 +3,7 @@ 'use strict'; -import { debug, DebugConfigurationProvider, DebugConfigurationProviderTriggerKind, languages, window } from 'vscode'; +import { DebugConfigurationProvider, debug, languages, window } from 'vscode'; import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry'; import { IExtensionActivationManager } from './activation/types'; @@ -22,9 +22,8 @@ import { IPathUtils, } from './common/types'; import { noop } from './common/utils/misc'; -import { DebuggerTypeName } from './debugger/constants'; import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; -import { IDebugConfigurationService, IDynamicDebugConfigurationService } from './debugger/extension/types'; +import { IDebugConfigurationService } from './debugger/extension/types'; import { IInterpreterService } from './interpreter/contracts'; import { getLanguageConfiguration } from './language/languageConfiguration'; import { ReplProvider } from './providers/replProvider'; @@ -46,12 +45,12 @@ import { DebugService } from './common/application/debugService'; import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHandlerDispatcher'; import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; import { WorkspaceService } from './common/application/workspace'; -import { DynamicPythonDebugConfigurationService } from './debugger/extension/configuration/dynamicdebugConfigurationService'; import { IInterpreterQuickPick } from './interpreter/configuration/types'; import { registerAllCreateEnvironmentFeatures } from './pythonEnvironments/creation/registrations'; import { registerCreateEnvironmentTriggers } from './pythonEnvironments/creation/createEnvironmentTrigger'; import { initializePersistentStateForTriggers } from './common/persistentState'; import { logAndNotifyOnLegacySettings } from './logging/settingLogs'; +import { DebuggerTypeName } from './debugger/constants'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -151,7 +150,6 @@ async function activateLegacy(ext: ExtensionState): Promise { const handlers = serviceManager.getAll(IDebugSessionEventHandlers); const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); dispatcher.registerEventHandlers(); - const outputChannel = serviceManager.get(ILogOutputChannel); disposables.push(cmdManager.registerCommand(Commands.ViewOutput, () => outputChannel.show())); cmdManager.executeCommand('setContext', 'python.vscode.channel', applicationEnv.channel).then(noop, noop); @@ -166,22 +164,13 @@ async function activateLegacy(ext: ExtensionState): Promise { const terminalProvider = new TerminalProvider(serviceContainer); terminalProvider.initialize(window.activeTerminal).ignoreErrors(); - disposables.push(terminalProvider); serviceContainer .getAll(IDebugConfigurationService) .forEach((debugConfigProvider) => { disposables.push(debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfigProvider)); }); - - // register a dynamic configuration provider for 'python' debug type - disposables.push( - debug.registerDebugConfigurationProvider( - DebuggerTypeName, - serviceContainer.get(IDynamicDebugConfigurationService), - DebugConfigurationProviderTriggerKind.Dynamic, - ), - ); + disposables.push(terminalProvider); logAndNotifyOnLegacySettings(); registerCreateEnvironmentTriggers(disposables); diff --git a/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index 9b8ecec74f9f..5487f459a7a9 100644 --- a/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -81,8 +81,13 @@ export namespace EnvGroups { @injectable() export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implements IInterpreterQuickPick { + private readonly createEnvironmentSuggestion: QuickPickItem = { + label: `${Octicons.Add} ${InterpreterQuickPickList.create.label}`, + alwaysShow: true, + }; + private readonly manualEntrySuggestion: ISpecialQuickPickItem = { - label: `${Octicons.Add} ${InterpreterQuickPickList.enterPath.label}`, + label: `${Octicons.Folder} ${InterpreterQuickPickList.enterPath.label}`, alwaysShow: true, }; @@ -220,6 +225,13 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem } else if (selection.label === this.manualEntrySuggestion.label) { sendTelemetryEvent(EventName.SELECT_INTERPRETER_ENTER_OR_FIND); return this._enterOrBrowseInterpreterPath.bind(this); + } else if (selection.label === this.createEnvironmentSuggestion.label) { + this.commandManager + .executeCommand(Commands.Create_Environment, { + showBackButton: false, + selectEnvironment: true, + }) + .then(noop, noop); } else if (selection.label === this.noPythonInstalled.label) { this.commandManager.executeCommand(Commands.InstallPython).then(noop, noop); this.wasNoPythonInstalledItemClicked = true; @@ -237,7 +249,13 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem filter: ((i: PythonEnvironment) => boolean) | undefined, params?: InterpreterQuickPickParams, ): QuickPickType[] { - const suggestions: QuickPickType[] = [this.manualEntrySuggestion]; + const suggestions: QuickPickType[] = []; + if (params?.showCreateEnvironment) { + suggestions.push(this.createEnvironmentSuggestion, { label: '', kind: QuickPickItemKind.Separator }); + } + + suggestions.push(this.manualEntrySuggestion, { label: '', kind: QuickPickItemKind.Separator }); + const defaultInterpreterPathSuggestion = this.getDefaultInterpreterPathSuggestion(resource); if (defaultInterpreterPathSuggestion) { suggestions.push(defaultInterpreterPathSuggestion); @@ -450,7 +468,6 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem } const areItemsGrouped = items.find((item) => isSeparatorItem(item) && item.label === EnvGroups.Recommended); const recommended = cloneDeep(suggestion); - recommended.label = `${Octicons.Star} ${recommended.label}`; recommended.description = areItemsGrouped ? // No need to add a tag as "Recommended" group already exists. recommended.description @@ -553,7 +570,10 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem const wkspace = targetConfig[0].folderUri; const interpreterState: InterpreterStateArgs = { path: undefined, workspace: wkspace }; const multiStep = this.multiStepFactory.create(); - await multiStep.run((input, s) => this._pickInterpreter(input, s, undefined), interpreterState); + await multiStep.run( + (input, s) => this._pickInterpreter(input, s, undefined, { showCreateEnvironment: true }), + interpreterState, + ); if (interpreterState.path !== undefined) { // User may choose to have an empty string stored, so variable `interpreterState.path` may be diff --git a/extensions/positron-python/src/client/interpreter/configuration/types.ts b/extensions/positron-python/src/client/interpreter/configuration/types.ts index 2f3882e1246e..815de29045d3 100644 --- a/extensions/positron-python/src/client/interpreter/configuration/types.ts +++ b/extensions/positron-python/src/client/interpreter/configuration/types.ts @@ -80,6 +80,11 @@ export interface InterpreterQuickPickParams { * Specify `true` to show back button. */ showBackButton?: boolean; + + /** + * Show button to create a new environment. + */ + showCreateEnvironment?: boolean; } export const IInterpreterQuickPick = Symbol('IInterpreterQuickPick'); diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts b/extensions/positron-python/src/client/interpreter/interpreterPathCommand.ts similarity index 80% rename from extensions/positron-python/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts rename to extensions/positron-python/src/client/interpreter/interpreterPathCommand.ts index 21c8d0f1147b..8402374d50d6 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts +++ b/extensions/positron-python/src/client/interpreter/interpreterPathCommand.ts @@ -1,51 +1,51 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; -import { IExtensionSingleActivationService } from '../../../../activation/types'; -import { Commands } from '../../../../common/constants'; -import { IDisposable, IDisposableRegistry } from '../../../../common/types'; -import { registerCommand } from '../../../../common/vscodeApis/commandApis'; -import { IInterpreterService } from '../../../../interpreter/contracts'; - -@injectable() -export class InterpreterPathCommand implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - constructor( - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IDisposableRegistry) private readonly disposables: IDisposable[], - ) {} - - public async activate(): Promise { - this.disposables.push( - registerCommand(Commands.GetSelectedInterpreterPath, (args) => this._getSelectedInterpreterPath(args)), - ); - } - - public async _getSelectedInterpreterPath(args: { workspaceFolder: string } | string[]): Promise { - // If `launch.json` is launching this command, `args.workspaceFolder` carries the workspaceFolder - // If `tasks.json` is launching this command, `args[1]` carries the workspaceFolder - let workspaceFolder; - if ('workspaceFolder' in args) { - workspaceFolder = args.workspaceFolder; - } else if (args[1]) { - const [, second] = args; - workspaceFolder = second; - } else { - workspaceFolder = undefined; - } - - let workspaceFolderUri; - try { - workspaceFolderUri = workspaceFolder ? Uri.file(workspaceFolder) : undefined; - } catch (ex) { - workspaceFolderUri = undefined; - } - - return (await this.interpreterService.getActiveInterpreter(workspaceFolderUri))?.path ?? 'python'; - } -} +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../activation/types'; +import { Commands } from '../common/constants'; +import { IDisposable, IDisposableRegistry } from '../common/types'; +import { registerCommand } from '../common/vscodeApis/commandApis'; +import { IInterpreterService } from './contracts'; + +@injectable() +export class InterpreterPathCommand implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + constructor( + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IDisposableRegistry) private readonly disposables: IDisposable[], + ) {} + + public async activate(): Promise { + this.disposables.push( + registerCommand(Commands.GetSelectedInterpreterPath, (args) => this._getSelectedInterpreterPath(args)), + ); + } + + public async _getSelectedInterpreterPath(args: { workspaceFolder: string } | string[]): Promise { + // If `launch.json` is launching this command, `args.workspaceFolder` carries the workspaceFolder + // If `tasks.json` is launching this command, `args[1]` carries the workspaceFolder + let workspaceFolder; + if ('workspaceFolder' in args) { + workspaceFolder = args.workspaceFolder; + } else if (args[1]) { + const [, second] = args; + workspaceFolder = second; + } else { + workspaceFolder = undefined; + } + + let workspaceFolderUri; + try { + workspaceFolderUri = workspaceFolder ? Uri.file(workspaceFolder) : undefined; + } catch (ex) { + workspaceFolderUri = undefined; + } + + return (await this.interpreterService.getActiveInterpreter(workspaceFolderUri))?.path ?? 'python'; + } +} diff --git a/extensions/positron-python/src/client/interpreter/serviceRegistry.ts b/extensions/positron-python/src/client/interpreter/serviceRegistry.ts index 422776bd5e43..fa44038ec717 100644 --- a/extensions/positron-python/src/client/interpreter/serviceRegistry.ts +++ b/extensions/positron-python/src/client/interpreter/serviceRegistry.ts @@ -29,6 +29,7 @@ import { IActivatedEnvironmentLaunch, IInterpreterDisplay, IInterpreterHelper, I import { InterpreterDisplay } from './display'; import { InterpreterLocatorProgressStatubarHandler } from './display/progressDisplay'; import { InterpreterHelper } from './helpers'; +import { InterpreterPathCommand } from './interpreterPathCommand'; import { InterpreterService } from './interpreterService'; import { ActivatedEnvironmentLaunch } from './virtualEnvs/activatedEnvLaunch'; import { CondaInheritEnvPrompt } from './virtualEnvs/condaInheritEnvPrompt'; @@ -108,4 +109,8 @@ export function registerTypes(serviceManager: IServiceManager): void { IEnvironmentActivationService, EnvironmentActivationService, ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + InterpreterPathCommand, + ); } diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/info/env.ts b/extensions/positron-python/src/client/pythonEnvironments/base/info/env.ts index 12b3e519b944..aa2131678205 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/info/env.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/info/env.ts @@ -179,6 +179,11 @@ function buildEnvDisplayString(env: PythonEnvInfo, getAllDetails = false): strin const envSuffixParts: string[] = []; if (env.name && env.name !== '') { envSuffixParts.push(`'${env.name}'`); + } else if (env.location && env.location !== '') { + if (env.kind === PythonEnvKind.Conda) { + const condaEnvName = path.basename(env.location); + envSuffixParts.push(`'${condaEnvName}'`); + } } if (shouldDisplayKind) { const kindName = getKindDisplayName(env.kind); diff --git a/extensions/positron-python/src/client/telemetry/constants.ts b/extensions/positron-python/src/client/telemetry/constants.ts index 9e29ef808d0d..072537619224 100644 --- a/extensions/positron-python/src/client/telemetry/constants.ts +++ b/extensions/positron-python/src/client/telemetry/constants.ts @@ -44,8 +44,6 @@ export enum EventName { DEBUGGER = 'DEBUGGER', DEBUGGER_ATTACH_TO_CHILD_PROCESS = 'DEBUGGER.ATTACH_TO_CHILD_PROCESS', DEBUGGER_ATTACH_TO_LOCAL_PROCESS = 'DEBUGGER.ATTACH_TO_LOCAL_PROCESS', - DEBUGGER_CONFIGURATION_PROMPTS = 'DEBUGGER.CONFIGURATION.PROMPTS', - DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON = 'DEBUGGER.CONFIGURATION.PROMPTS.IN.LAUNCH.JSON', // Python testing specific telemetry UNITTEST_CONFIGURING = 'UNITTEST.CONFIGURING', diff --git a/extensions/positron-python/src/client/telemetry/index.ts b/extensions/positron-python/src/client/telemetry/index.ts index f9ed98eb3764..42a73fb06e07 100644 --- a/extensions/positron-python/src/client/telemetry/index.ts +++ b/extensions/positron-python/src/client/telemetry/index.ts @@ -11,7 +11,6 @@ import { AppinsightsKey, EXTENSION_ROOT_DIR, isTestExecution, isUnitTestExecutio import type { TerminalShellType } from '../common/terminal/types'; import { StopWatch } from '../common/utils/stopWatch'; import { isPromise } from '../common/utils/async'; -import { DebugConfigurationType } from '../debugger/extension/types'; import { ConsoleType, TriggerType } from '../debugger/types'; import { EnvironmentType, PythonEnvironment } from '../pythonEnvironments/info'; import { @@ -366,7 +365,6 @@ export interface IEventNamePropertyMapping { "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" }, "console" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" } - } */ [EventName.DEBUG_SESSION_ERROR]: { @@ -615,66 +613,6 @@ export interface IEventNamePropertyMapping { "debugger.attach_to_local_process" : { "owner": "paulacamargo25" } */ [EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS]: never | undefined; - /** - * Telemetry sent after building configuration for debugger - */ - /* __GDPR__ - "debugger.configuration.prompts" : { - "configurationtype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "autodetecteddjangomanagepypath" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "autodetectedpyramidinipath" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "autodetectedfastapimainpypath" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "autodetectedflaskapppypath" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "manuallyenteredavalue" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" } - } - */ - - [EventName.DEBUGGER_CONFIGURATION_PROMPTS]: { - /** - * The type of debug configuration to build configuration for - * - * @type {DebugConfigurationType} - */ - configurationType: DebugConfigurationType; - /** - * Carries `true` if we are able to auto-detect manage.py path for Django, `false` otherwise - * - * @type {boolean} - */ - autoDetectedDjangoManagePyPath?: boolean; - /** - * Carries `true` if we are able to auto-detect .ini file path for Pyramid, `false` otherwise - * - * @type {boolean} - */ - autoDetectedPyramidIniPath?: boolean; - /** - * Carries `true` if we are able to auto-detect main.py path for FastAPI, `false` otherwise - * - * @type {boolean} - */ - autoDetectedFastAPIMainPyPath?: boolean; - /** - * Carries `true` if we are able to auto-detect app.py path for Flask, `false` otherwise - * - * @type {boolean} - */ - autoDetectedFlaskAppPyPath?: boolean; - /** - * Carries `true` if user manually entered the required path for the app - * (path to `manage.py` for Django, path to `.ini` for Pyramid, path to `app.py` for Flask), `false` otherwise - * - * @type {boolean} - */ - manuallyEnteredAValue?: boolean; - }; - /** - * Telemetry event sent when providing completion provider in launch.json. It is sent just *after* inserting the completion. - */ - /* __GDPR__ - "debugger.configuration.prompts.in.launch.json" : { "owner": "paulacamargo25" } - */ - [EventName.DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON]: never | undefined; /** * Telemetry event sent with details of actions when invoking a diagnostic command */ diff --git a/extensions/positron-python/src/client/testing/testController/workspaceTestAdapter.ts b/extensions/positron-python/src/client/testing/testController/workspaceTestAdapter.ts index 35a5b7a24418..5fe69dfe3d69 100644 --- a/extensions/positron-python/src/client/testing/testController/workspaceTestAdapter.ts +++ b/extensions/positron-python/src/client/testing/testController/workspaceTestAdapter.ts @@ -47,6 +47,7 @@ export class WorkspaceTestAdapter { debugLauncher?: ITestDebugLauncher, ): Promise { if (this.executing) { + traceError('Test execution already in progress, not starting a new one.'); return this.executing.promise; } @@ -119,6 +120,7 @@ export class WorkspaceTestAdapter { // Discovery is expensive. If it is already running, use the existing promise. if (this.discovering) { + traceError('Test discovery already in progress, not starting a new one.'); return this.discovering.promise; } diff --git a/extensions/positron-python/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts b/extensions/positron-python/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts deleted file mode 100644 index d4eefd69dd5f..000000000000 --- a/extensions/positron-python/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts +++ /dev/null @@ -1,462 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as TypeMoq from 'typemoq'; -import { Uri, WorkspaceFolder } from 'vscode'; -import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; -import { - InvalidLaunchJsonDebuggerDiagnostic, - InvalidLaunchJsonDebuggerService, -} from '../../../../client/application/diagnostics/checks/invalidLaunchJsonDebugger'; -import { IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; -import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; -import { MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; -import { - IDiagnostic, - IDiagnosticHandlerService, - IDiagnosticsService, -} from '../../../../client/application/diagnostics/types'; -import { IWorkspaceService } from '../../../../client/common/application/types'; -import { IFileSystem } from '../../../../client/common/platform/types'; -import { Diagnostics } from '../../../../client/common/utils/localize'; -import { IServiceContainer } from '../../../../client/ioc/types'; - -suite('Application Diagnostics - Checks if launch.json is invalid', () => { - let serviceContainer: TypeMoq.IMock; - let diagnosticService: IDiagnosticsService; - let commandFactory: TypeMoq.IMock; - let fs: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let baseWorkspaceService: TypeMoq.IMock; - let messageHandler: TypeMoq.IMock>; - let workspaceFolder: WorkspaceFolder; - - setup(() => { - workspaceFolder = { uri: Uri.parse('full/path/to/workspace'), name: '', index: 0 }; - serviceContainer = TypeMoq.Mock.ofType(); - commandFactory = TypeMoq.Mock.ofType(); - fs = TypeMoq.Mock.ofType(); - messageHandler = TypeMoq.Mock.ofType>(); - workspaceService = TypeMoq.Mock.ofType(); - baseWorkspaceService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IWorkspaceService))) - .returns(() => baseWorkspaceService.object); - - diagnosticService = new (class extends InvalidLaunchJsonDebuggerService { - public _clear() { - while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { - BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); - } - } - public async fixLaunchJson(code: DiagnosticCodes) { - await super.fixLaunchJson(code); - } - })(serviceContainer.object, fs.object, [], workspaceService.object, messageHandler.object); - (diagnosticService as any)._clear(); - }); - - test('Can handle all InvalidLaunchJsonDebugger diagnostics', async () => { - for (const code of [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic, - ]) { - const diagnostic = TypeMoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => code) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(true, `Should be able to handle ${code}`); - diagnostic.verifyAll(); - } - }); - - test('Can not handle non-InvalidLaunchJsonDebugger diagnostics', async () => { - const diagnostic = TypeMoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => 'Something Else' as any) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(false, 'Invalid value'); - diagnostic.verifyAll(); - }); - - test('Should return empty diagnostics if there are no workspace folders', async () => { - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - workspaceService.verifyAll(); - }); - - test('Should return empty diagnostics if file launch.json does not exist', async () => { - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - workspaceService - .setup((w) => w.getWorkspaceFolder(undefined)) - .returns(() => undefined) - .verifiable(TypeMoq.Times.never()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return empty diagnostics if file launch.json does not contain strings "pythonExperimental" and "debugStdLib" ', async () => { - const fileContents = 'Hello I am launch.json, although I am not very jsony'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return InvalidDebuggerTypeDiagnostic if file launch.json contains string "pythonExperimental"', async () => { - const fileContents = 'Hello I am launch.json, I contain string "pythonExperimental"'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal( - [new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, undefined)], - 'Diagnostics returned are not as expected', - ); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return JustMyCodeDiagnostic if file launch.json contains string "debugStdLib"', async () => { - const fileContents = 'Hello I am launch.json, I contain string "debugStdLib"'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal( - [new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, undefined)], - 'Diagnostics returned are not as expected', - ); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return ConfigPythonPathDiagnostic if file launch.json contains string "{config:python.pythonPath}"', async () => { - const fileContents = 'Hello I am launch.json, I contain string {config:python.pythonPath}'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal( - [new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConfigPythonPathDiagnostic, undefined, false)], - 'Diagnostics returned are not as expected', - ); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return ConfigPythonPathDiagnostic if file launch.json contains string "{config:python.interpreterPath}"', async () => { - const fileContents = 'Hello I am launch.json, I contain string {config:python.interpreterPath}'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal( - [new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConfigPythonPathDiagnostic, undefined, false)], - 'Diagnostics returned are not as expected', - ); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return both diagnostics if file launch.json contains string "debugStdLib" and "pythonExperimental"', async () => { - const fileContents = 'Hello I am launch.json, I contain both "debugStdLib" and "pythonExperimental"'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal( - [ - new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, undefined), - new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, undefined), - ], - 'Diagnostics returned are not as expected', - ); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('All InvalidLaunchJsonDebugger diagnostics with `shouldShowPrompt` set to `true` should display a prompt with 2 buttons where clicking the first button will invoke a command', async () => { - for (const code of [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic, - ]) { - const diagnostic = TypeMoq.Mock.ofType(); - let options: MessageCommandPrompt | undefined; - diagnostic - .setup((d) => d.code) - .returns(() => code) - .verifiable(TypeMoq.Times.atLeastOnce()); - diagnostic - .setup((d) => d.shouldShowPrompt) - .returns(() => true) - .verifiable(TypeMoq.Times.atLeastOnce()); - messageHandler - .setup((m) => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .callback((_, opts: MessageCommandPrompt) => (options = opts)) - .verifiable(TypeMoq.Times.atLeastOnce()); - baseWorkspaceService - .setup((c) => c.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder) - .verifiable(TypeMoq.Times.atLeastOnce()); - - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - baseWorkspaceService.verifyAll(); - expect(options!.commandPrompts).to.be.lengthOf(2); - expect(options!.commandPrompts[0].prompt).to.be.equal(Diagnostics.yesUpdateLaunch); - expect(options!.commandPrompts[0].command).not.to.be.equal(undefined, 'Command not set'); - } - }); - - test('All InvalidLaunchJsonDebugger diagnostics with `shouldShowPrompt` set to `false` should directly fix launch.json', async () => { - for (const code of [DiagnosticCodes.ConfigPythonPathDiagnostic]) { - let called = false; - (diagnosticService as any).fixLaunchJson = () => { - called = true; - }; - const diagnostic = TypeMoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => code) - .verifiable(TypeMoq.Times.atLeastOnce()); - diagnostic - .setup((d) => d.shouldShowPrompt) - .returns(() => false) - .verifiable(TypeMoq.Times.atLeastOnce()); - messageHandler - .setup((m) => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .verifiable(TypeMoq.Times.never()); - baseWorkspaceService - .setup((c) => c.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder) - .verifiable(TypeMoq.Times.atLeastOnce()); - - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - baseWorkspaceService.verifyAll(); - expect(called).to.equal(true, ''); - } - }); - - test('All InvalidLaunchJsonDebugger diagnostics should display message twice if invoked twice', async () => { - for (const code of [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic, - ]) { - const diagnostic = TypeMoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => code) - .verifiable(TypeMoq.Times.atLeastOnce()); - diagnostic - .setup((d) => d.invokeHandler) - .returns(() => 'always') - .verifiable(TypeMoq.Times.atLeastOnce()); - messageHandler.reset(); - messageHandler - .setup((m) => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .verifiable(TypeMoq.Times.exactly(2)); - baseWorkspaceService - .setup((c) => c.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder) - .verifiable(TypeMoq.Times.never()); - - await diagnosticService.handle([diagnostic.object]); - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - baseWorkspaceService.verifyAll(); - } - }); - - const codes = [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic, - ]; - - codes.forEach((code) => { - test('Function fixLaunchJson() returns if there are no workspace folders', async () => { - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => undefined) - .verifiable(TypeMoq.Times.atLeastOnce()); - await (diagnosticService as any).fixLaunchJson(code); - workspaceService.verifyAll(); - }); - - test('Function fixLaunchJson() returns if file launch.json does not exist', async () => { - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve('')) - .verifiable(TypeMoq.Times.never()); - await (diagnosticService as any).fixLaunchJson(code); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - }); - - test('File launch.json is fixed correctly when code equals JustMyCodeDiagnostic', async () => { - const launchJson = '{"debugStdLib": true, "debugStdLib": false}'; - const correctedlaunchJson = '{"justMyCode": false, "justMyCode": true}'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(launchJson)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.JustMyCodeDiagnostic); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('File launch.json is fixed correctly when code equals InvalidDebuggerTypeDiagnostic', async () => { - const launchJson = '{"Python Experimental: task" "pythonExperimental"}'; - const correctedlaunchJson = '{"Python: task" "python"}'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(launchJson)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.InvalidDebuggerTypeDiagnostic); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('File launch.json is fixed correctly when code equals ConsoleTypeDiagnostic', async () => { - const launchJson = '{"console": "none"}'; - const correctedlaunchJson = '{"console": "internalConsole"}'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(launchJson)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.ConsoleTypeDiagnostic); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('File launch.json is fixed correctly when code equals ConfigPythonPathDiagnostic', async () => { - const launchJson = '"pythonPath": "{config:python.pythonPath}{config:python.interpreterPath}"'; - const correctedlaunchJson = '"python": "{command:python.interpreterPath}{command:python.interpreterPath}"'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(launchJson)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.ConfigPythonPathDiagnostic); - workspaceService.verifyAll(); - fs.verifyAll(); - }); -}); diff --git a/extensions/positron-python/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts b/extensions/positron-python/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts deleted file mode 100644 index 8c835003ffef..000000000000 --- a/extensions/positron-python/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts +++ /dev/null @@ -1,417 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import * as typemoq from 'typemoq'; -import { Uri } from 'vscode'; -import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; -import { InvalidPythonPathInDebuggerService } from '../../../../client/application/diagnostics/checks/invalidPythonPathInDebugger'; -import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; -import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; -import { - DiagnosticCommandPromptHandlerServiceId, - MessageCommandPrompt, -} from '../../../../client/application/diagnostics/promptHandler'; -import { - IDiagnostic, - IDiagnosticCommand, - IDiagnosticHandlerService, - IInvalidPythonPathInDebuggerService, -} from '../../../../client/application/diagnostics/types'; -import { CommandsWithoutArgs } from '../../../../client/common/application/commands'; -import { IDocumentManager, IWorkspaceService } from '../../../../client/common/application/types'; -import { IConfigurationService, IPythonSettings } from '../../../../client/common/types'; -import { PythonPathSource } from '../../../../client/debugger/extension/types'; -import { IInterpreterHelper } from '../../../../client/interpreter/contracts'; -import { IServiceContainer } from '../../../../client/ioc/types'; - -suite('Application Diagnostics - Checks Python Path in debugger', () => { - let diagnosticService: IInvalidPythonPathInDebuggerService; - let messageHandler: typemoq.IMock>; - let commandFactory: typemoq.IMock; - let configService: typemoq.IMock; - let helper: typemoq.IMock; - let workspaceService: typemoq.IMock; - let docMgr: typemoq.IMock; - setup(() => { - const serviceContainer = typemoq.Mock.ofType(); - messageHandler = typemoq.Mock.ofType>(); - serviceContainer - .setup((s) => - s.get( - typemoq.It.isValue(IDiagnosticHandlerService), - typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId), - ), - ) - .returns(() => messageHandler.object); - commandFactory = typemoq.Mock.ofType(); - docMgr = typemoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) - .returns(() => commandFactory.object); - configService = typemoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IConfigurationService))) - .returns(() => configService.object); - helper = typemoq.Mock.ofType(); - serviceContainer.setup((s) => s.get(typemoq.It.isValue(IInterpreterHelper))).returns(() => helper.object); - workspaceService = typemoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - - diagnosticService = new (class extends InvalidPythonPathInDebuggerService { - public _clear() { - while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { - BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); - } - } - })( - serviceContainer.object, - workspaceService.object, - commandFactory.object, - helper.object, - docMgr.object, - configService.object, - [], - messageHandler.object, - ); - (diagnosticService as any)._clear(); - }); - - test('Can handle InvalidPythonPathInDebugger diagnostics', async () => { - for (const code of [ - DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic, - DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic, - ]) { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => code) - .verifiable(typemoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(true, `Should be able to handle ${code}`); - diagnostic.verifyAll(); - } - }); - test('Can not handle non-InvalidPythonPathInDebugger diagnostics', async () => { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => 'Something Else' as any) - .verifiable(typemoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(false, 'Invalid value'); - diagnostic.verifyAll(); - }); - test('Should return empty diagnostics', async () => { - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - }); - test('InvalidPythonPathInDebuggerSettings diagnostic should display one option to with a command', async () => { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - const interpreterSelectionCommand = typemoq.Mock.ofType(); - commandFactory - .setup((f) => - f.createCommand( - typemoq.It.isAny(), - typemoq.It.isObjectWith>({ - type: 'executeVSCCommand', - }), - ), - ) - .returns(() => interpreterSelectionCommand.object) - .verifiable(typemoq.Times.once()); - messageHandler.setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())).verifiable(typemoq.Times.once()); - - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - }); - test('InvalidPythonPathInDebuggerSettings diagnostic should display message once if invoked twice', async () => { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - diagnostic - .setup((d) => d.invokeHandler) - .returns(() => 'default') - .verifiable(typemoq.Times.atLeastOnce()); - const interpreterSelectionCommand = typemoq.Mock.ofType(); - commandFactory - .setup((f) => - f.createCommand( - typemoq.It.isAny(), - typemoq.It.isObjectWith>({ - type: 'executeVSCCommand', - }), - ), - ) - .returns(() => interpreterSelectionCommand.object) - .verifiable(typemoq.Times.exactly(1)); - messageHandler - .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .verifiable(typemoq.Times.exactly(1)); - - await diagnosticService.handle([diagnostic.object]); - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - }); - test('InvalidPythonPathInDebuggerSettings diagnostic should display message twice if invoked twice', async () => { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - diagnostic - .setup((d) => d.invokeHandler) - .returns(() => 'always') - .verifiable(typemoq.Times.atLeastOnce()); - const interpreterSelectionCommand = typemoq.Mock.ofType(); - commandFactory - .setup((f) => - f.createCommand( - typemoq.It.isAny(), - typemoq.It.isObjectWith>({ - type: 'executeVSCCommand', - }), - ), - ) - .returns(() => interpreterSelectionCommand.object) - .verifiable(typemoq.Times.exactly(2)); - messageHandler - .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .verifiable(typemoq.Times.exactly(2)); - - await diagnosticService.handle([diagnostic.object]); - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - }); - test('InvalidPythonPathInDebuggerLaunch diagnostic should display one option to with a command', async () => { - const diagnostic = typemoq.Mock.ofType(); - let options: MessageCommandPrompt | undefined; - diagnostic - .setup((d) => d.code) - .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - messageHandler - .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .callback((_, opts: MessageCommandPrompt) => (options = opts)) - .verifiable(typemoq.Times.once()); - - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - expect(options!.commandPrompts).to.be.lengthOf(1); - expect(options!.commandPrompts[0].prompt).to.be.equal('Open launch.json'); - }); - test('Ensure we get python path from config when path = ${command:python.interpreterPath}', async () => { - const pythonPath = '${command:python.interpreterPath}'; - - const settings = typemoq.Mock.ofType(); - settings - .setup((s) => s.pythonPath) - .returns(() => 'p') - .verifiable(typemoq.Times.once()); - configService - .setup((c) => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.once()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue('p'))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath); - - settings.verifyAll(); - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure ${workspaceFolder} is not expanded when a resource is not passed', async () => { - const pythonPath = '${workspaceFolder}/venv/bin/python'; - - workspaceService - .setup((c) => c.getWorkspaceFolder(typemoq.It.isAny())) - .returns(() => undefined) - .verifiable(typemoq.Times.never()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isAny())) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - await diagnosticService.validatePythonPath(pythonPath); - - configService.verifyAll(); - helper.verifyAll(); - }); - test('Ensure ${workspaceFolder} is expanded', async () => { - const pythonPath = '${workspaceFolder}/venv/bin/python'; - - const workspaceFolder = { uri: Uri.parse('full/path/to/workspace'), name: '', index: 0 }; - const expectedPath = `${workspaceFolder.uri.fsPath}/venv/bin/python`; - - workspaceService - .setup((c) => c.getWorkspaceFolder(typemoq.It.isAny())) - .returns(() => workspaceFolder) - .verifiable(typemoq.Times.once()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue(expectedPath))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath( - pythonPath, - PythonPathSource.settingsJson, - Uri.parse('something'), - ); - - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure ${env:XYZ123} is expanded', async () => { - const pythonPath = '${env:XYZ123}/venv/bin/python'; - - process.env.XYZ123 = 'something/else'; - const expectedPath = `${process.env.XYZ123}/venv/bin/python`; - workspaceService - .setup((c) => c.getWorkspaceFolder(typemoq.It.isAny())) - .returns(() => undefined) - .verifiable(typemoq.Times.once()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue(expectedPath))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath); - - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure we get python path from config when path = undefined', async () => { - const pythonPath = undefined; - - const settings = typemoq.Mock.ofType(); - settings - .setup((s) => s.pythonPath) - .returns(() => 'p') - .verifiable(typemoq.Times.once()); - configService - .setup((c) => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.once()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue('p'))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath); - - settings.verifyAll(); - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure we do not get python path from config when path is provided', async () => { - const pythonPath = path.join('a', 'b'); - - const settings = typemoq.Mock.ofType(); - configService - .setup((c) => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.never()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath); - - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure InvalidPythonPathInDebuggerLaunch diagnostic is handled when path is invalid in launch.json', async () => { - const pythonPath = path.join('a', 'b'); - const settings = typemoq.Mock.ofType(); - configService - .setup((c) => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.never()); - let handleInvoked = false; - diagnosticService.handle = (diagnostics) => { - if ( - diagnostics.length !== 0 && - diagnostics[0].code === DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic - ) { - handleInvoked = true; - } - return Promise.resolve(); - }; - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath, PythonPathSource.launchJson); - - helper.verifyAll(); - expect(valid).to.be.equal(false, 'should be invalid'); - expect(handleInvoked).to.be.equal(true, 'should be invoked'); - }); - test('Ensure InvalidPythonPathInDebuggerSettings diagnostic is handled when path is invalid in settings.json', async () => { - const pythonPath = undefined; - const settings = typemoq.Mock.ofType(); - settings - .setup((s) => s.pythonPath) - .returns(() => 'p') - .verifiable(typemoq.Times.once()); - configService - .setup((c) => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.once()); - let handleInvoked = false; - diagnosticService.handle = (diagnostics) => { - if ( - diagnostics.length !== 0 && - diagnostics[0].code === DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic - ) { - handleInvoked = true; - } - return Promise.resolve(); - }; - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue('p'))) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath, PythonPathSource.settingsJson); - - helper.verifyAll(); - expect(valid).to.be.equal(false, 'should be invalid'); - expect(handleInvoked).to.be.equal(true, 'should be invoked'); - }); -}); diff --git a/extensions/positron-python/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts b/extensions/positron-python/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts index 2397743274c1..2eecf052e433 100644 --- a/extensions/positron-python/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts +++ b/extensions/positron-python/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts @@ -7,7 +7,6 @@ import { expect } from 'chai'; import * as typemoq from 'typemoq'; import { EventEmitter, Uri } from 'vscode'; import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; -import { InvalidLaunchJsonDebuggerDiagnostic } from '../../../../client/application/diagnostics/checks/invalidLaunchJsonDebugger'; import { DefaultShellDiagnostic, InvalidPythonInterpreterDiagnostic, @@ -586,39 +585,6 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { await diagnosticServiceMock.object.handle([diagnostic]); - messageHandler.verifyAll(); - commandFactory.verifyAll(); - }); - test('Getting command prompts for an unsupported diagnostic code should throw an error', async () => { - const diagnostic = new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, undefined); - const cmd = ({} as any) as IDiagnosticCommand; - - messageHandler - .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .callback((_d, p: MessageCommandPrompt) => p) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.never()); - commandFactory - .setup((f) => - f.createCommand( - typemoq.It.isAny(), - typemoq.It.isObjectWith>({ - type: 'executeVSCCommand', - }), - ), - ) - .returns(() => cmd) - .verifiable(typemoq.Times.never()); - - try { - await diagnosticService.handle([diagnostic]); - } catch (err) { - expect((err as Error).message).to.be.equal( - "Invalid diagnostic for 'InvalidPythonInterpreterService'", - 'Error message is different', - ); - } - messageHandler.verifyAll(); commandFactory.verifyAll(); }); diff --git a/extensions/positron-python/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts b/extensions/positron-python/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts index f177db5c2a32..1871a1b46874 100644 --- a/extensions/positron-python/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts +++ b/extensions/positron-python/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts @@ -155,7 +155,11 @@ suite('Set Interpreter Command', () => { } as PythonEnvironment, }; const expectedEnterInterpreterPathSuggestion = { - label: `${Octicons.Add} ${InterpreterQuickPickList.enterPath.label}`, + label: `${Octicons.Folder} ${InterpreterQuickPickList.enterPath.label}`, + alwaysShow: true, + }; + const expectedCreateEnvSuggestion = { + label: `${Octicons.Add} ${InterpreterQuickPickList.create.label}`, alwaysShow: true, }; const currentPythonPath = 'python'; @@ -233,10 +237,11 @@ suite('Set Interpreter Command', () => { const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; const multiStepInput = TypeMoq.Mock.ofType>(); const recommended = cloneDeep(item); - recommended.label = `${Octicons.Star} ${item.label}`; + recommended.label = item.label; recommended.description = interpreterPath; const suggestions = [ expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, defaultInterpreterPathSuggestion, { kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended }, recommended, @@ -278,11 +283,66 @@ suite('Set Interpreter Command', () => { assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); }); + test('Picker should show create env when set in options', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType>(); + const recommended = cloneDeep(item); + recommended.label = item.label; + recommended.description = interpreterPath; + const suggestions = [ + expectedCreateEnvSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, + expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, + defaultInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended }, + recommended, + ]; + const expectedParameters: IQuickPickParameters = { + placeholder: `Selected Interpreter: ${currentPythonPath}`, + items: suggestions, + matchOnDetail: true, + matchOnDescription: true, + title: InterpreterQuickPickList.browsePath.openButtonLabel, + sortByLabel: true, + keepScrollPosition: true, + }; + let actualParameters: IQuickPickParameters | undefined; + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + .callback((options) => { + actualParameters = options; + }) + .returns(() => Promise.resolve((undefined as unknown) as QuickPickItem)); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state, undefined, { + showCreateEnvironment: true, + }); + + expect(actualParameters).to.not.equal(undefined, 'Parameters not set'); + const refreshButtons = actualParameters!.customButtonSetups; + expect(refreshButtons).to.not.equal(undefined, 'Callback not set'); + delete actualParameters!.initialize; + delete actualParameters!.customButtonSetups; + delete actualParameters!.onChangeItem; + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, recommended); + } else { + assert(false, 'Not a function'); + } + delete actualParameters!.activeItem; + assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); + }); + test('Picker should be displayed with expected items if no interpreters are available', async () => { const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; const multiStepInput = TypeMoq.Mock.ofType>(); const suggestions = [ expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, defaultInterpreterPathSuggestion, noPythonInstalled, ]; @@ -436,10 +496,11 @@ suite('Set Interpreter Command', () => { .setup((i) => i.getRecommendedSuggestion(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => item); const recommended = cloneDeep(item); - recommended.label = `${Octicons.Star} ${item.label}`; + recommended.label = item.label; recommended.description = interpreterPath; const suggestions = [ expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, defaultInterpreterPathSuggestion, { kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended }, recommended, @@ -552,10 +613,11 @@ suite('Set Interpreter Command', () => { .setup((i) => i.getRecommendedSuggestion(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => item); const recommended = cloneDeep(item); - recommended.label = `${Octicons.Star} ${item.label}`; + recommended.label = item.label; recommended.description = interpreterPath; const suggestions = [ expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, defaultInterpreterPathSuggestion, { kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended }, recommended, @@ -641,7 +703,7 @@ suite('Set Interpreter Command', () => { const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; const multiStepInput = TypeMoq.Mock.ofType>(); const recommended = cloneDeep(item); - recommended.label = `${Octicons.Star} ${item.label}`; + recommended.label = item.label; recommended.description = interpreterPath; const separator = { label: EnvGroups.Recommended, kind: QuickPickItemKind.Separator }; @@ -652,7 +714,13 @@ suite('Set Interpreter Command', () => { alwaysShow: true, }; - const suggestions = [expectedEnterInterpreterPathSuggestion, defaultPathSuggestion, separator, recommended]; + const suggestions = [ + expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, + defaultPathSuggestion, + separator, + recommended, + ]; const expectedParameters: IQuickPickParameters = { placeholder: `Selected Interpreter: ${currentPythonPath}`, items: suggestions, @@ -786,7 +854,7 @@ suite('Set Interpreter Command', () => { await sleep(1); const recommended = cloneDeep(refreshedItem); - recommended.label = `${Octicons.Star} ${refreshedItem.label}`; + recommended.label = refreshedItem.label; recommended.description = `${interpreterPath} - ${Common.recommended}`; assert.deepStrictEqual( quickPick, diff --git a/extensions/positron-python/src/test/debugger/common/constants.ts b/extensions/positron-python/src/test/debugger/common/constants.ts deleted file mode 100644 index a9bcc64f1a24..000000000000 --- a/extensions/positron-python/src/test/debugger/common/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// Sometimes PTVSD can take a while for thread & other events to be reported. -export const DEBUGGER_TIMEOUT = 20000; diff --git a/extensions/positron-python/src/test/debugger/common/protocolparser.test.ts b/extensions/positron-python/src/test/debugger/common/protocolparser.test.ts deleted file mode 100644 index 117a58a7bc66..000000000000 --- a/extensions/positron-python/src/test/debugger/common/protocolparser.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { expect } from 'chai'; -import { PassThrough } from 'stream'; -import { createDeferred } from '../../../client/common/utils/async'; -import { ProtocolParser } from '../../../client/debugger/extension/helpers/protocolParser'; -import { sleep } from '../../common'; - -suite('Debugging - Protocol Parser', () => { - test('Test request, response and event messages', async () => { - const stream = new PassThrough(); - - const protocolParser = new ProtocolParser(); - protocolParser.connect(stream); - let messagesDetected = 0; - protocolParser.on('data', () => (messagesDetected += 1)); - const requestDetected = new Promise((resolve) => { - protocolParser.on('request_initialize', () => resolve(true)); - }); - const responseDetected = new Promise((resolve) => { - protocolParser.on('response_initialize', () => resolve(true)); - }); - const eventDetected = new Promise((resolve) => { - protocolParser.on('event_initialized', () => resolve(true)); - }); - - stream.write( - 'Content-Length: 289\r\n\r\n{"command":"initialize","arguments":{"clientID":"vscode","adapterID":"pythonExperiment","pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsVariableType":true,"supportsVariablePaging":true,"supportsRunInTerminalRequest":true,"locale":"en-us"},"type":"request","seq":1}', - ); - await expect(requestDetected).to.eventually.equal(true, 'request not parsed'); - - stream.write( - 'Content-Length: 265\r\n\r\n{"seq":1,"type":"response","request_seq":1,"command":"initialize","success":true,"body":{"supportsEvaluateForHovers":false,"supportsConditionalBreakpoints":true,"supportsConfigurationDoneRequest":true,"supportsFunctionBreakpoints":false,"supportsSetVariable":true}}', - ); - await expect(responseDetected).to.eventually.equal(true, 'response not parsed'); - - stream.write('Content-Length: 63\r\n\r\n{"type": "event", "seq": 1, "event": "initialized", "body": {}}'); - await expect(eventDetected).to.eventually.equal(true, 'event not parsed'); - - expect(messagesDetected).to.be.equal(3, 'incorrect number of protocol messages'); - }); - test('Ensure messages are not received after disposing the parser', async () => { - const stream = new PassThrough(); - - const protocolParser = new ProtocolParser(); - protocolParser.connect(stream); - let messagesDetected = 0; - protocolParser.on('data', () => (messagesDetected += 1)); - const requestDetected = new Promise((resolve) => { - protocolParser.on('request_initialize', () => resolve(true)); - }); - stream.write( - 'Content-Length: 289\r\n\r\n{"command":"initialize","arguments":{"clientID":"vscode","adapterID":"pythonExperiment","pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsVariableType":true,"supportsVariablePaging":true,"supportsRunInTerminalRequest":true,"locale":"en-us"},"type":"request","seq":1}', - ); - await expect(requestDetected).to.eventually.equal(true, 'request not parsed'); - - protocolParser.dispose(); - - const responseDetected = createDeferred(); - protocolParser.on('response_initialize', () => responseDetected.resolve(true)); - - stream.write( - 'Content-Length: 265\r\n\r\n{"seq":1,"type":"response","request_seq":1,"command":"initialize","success":true,"body":{"supportsEvaluateForHovers":false,"supportsConditionalBreakpoints":true,"supportsConfigurationDoneRequest":true,"supportsFunctionBreakpoints":false,"supportsSetVariable":true}}', - ); - // Wait for messages to go through and get parsed (unnecenssary, but add for testing edge cases). - await sleep(1000); - expect(responseDetected.completed).to.be.equal(false, 'Promise should not have resolved'); - }); -}); diff --git a/extensions/positron-python/src/test/debugger/extension/adapter/activator.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/adapter/activator.unit.test.ts deleted file mode 100644 index e8c6ef74fc2a..000000000000 --- a/extensions/positron-python/src/test/debugger/extension/adapter/activator.unit.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { IExtensionSingleActivationService } from '../../../../client/activation/types'; -import { CommandManager } from '../../../../client/common/application/commandManager'; -import { DebugService } from '../../../../client/common/application/debugService'; -import { ICommandManager, IDebugService } from '../../../../client/common/application/types'; -import { ConfigurationService } from '../../../../client/common/configuration/service'; -import { IConfigurationService, IDisposableRegistry, IPythonSettings } from '../../../../client/common/types'; -import { DebugAdapterActivator } from '../../../../client/debugger/extension/adapter/activator'; -import { DebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/adapter/factory'; -import { DebugSessionLoggingFactory } from '../../../../client/debugger/extension/adapter/logging'; -import { OutdatedDebuggerPromptFactory } from '../../../../client/debugger/extension/adapter/outdatedDebuggerPrompt'; -import { AttachProcessProviderFactory } from '../../../../client/debugger/extension/attachQuickPick/factory'; -import { IAttachProcessProviderFactory } from '../../../../client/debugger/extension/attachQuickPick/types'; -import { - IDebugAdapterDescriptorFactory, - IDebugSessionLoggingFactory, - IOutdatedDebuggerPromptFactory, -} from '../../../../client/debugger/extension/types'; -import { clearTelemetryReporter } from '../../../../client/telemetry'; -import { noop } from '../../../core'; - -suite('Debugging - Adapter Factory and logger Registration', () => { - let activator: IExtensionSingleActivationService; - let debugService: IDebugService; - let commandManager: ICommandManager; - let descriptorFactory: IDebugAdapterDescriptorFactory; - let loggingFactory: IDebugSessionLoggingFactory; - let debuggerPromptFactory: IOutdatedDebuggerPromptFactory; - let disposableRegistry: IDisposableRegistry; - let attachFactory: IAttachProcessProviderFactory; - let configService: IConfigurationService; - - setup(() => { - attachFactory = mock(AttachProcessProviderFactory); - - debugService = mock(DebugService); - when(debugService.onDidStartDebugSession).thenReturn(() => noop as any); - - commandManager = mock(CommandManager); - - configService = mock(ConfigurationService); - when(configService.getSettings(undefined)).thenReturn(({ - experiments: { enabled: true }, - } as any) as IPythonSettings); - - descriptorFactory = mock(DebugAdapterDescriptorFactory); - loggingFactory = mock(DebugSessionLoggingFactory); - debuggerPromptFactory = mock(OutdatedDebuggerPromptFactory); - disposableRegistry = []; - - activator = new DebugAdapterActivator( - instance(debugService), - instance(configService), - instance(commandManager), - instance(descriptorFactory), - instance(loggingFactory), - instance(debuggerPromptFactory), - disposableRegistry, - instance(attachFactory), - ); - }); - - teardown(() => { - clearTelemetryReporter(); - }); - - test('Register Debug adapter factory', async () => { - await activator.activate(); - - verify(debugService.registerDebugAdapterTrackerFactory('python', instance(loggingFactory))).once(); - verify(debugService.registerDebugAdapterTrackerFactory('python', instance(debuggerPromptFactory))).once(); - verify(debugService.registerDebugAdapterDescriptorFactory('python', instance(descriptorFactory))).once(); - }); - - test('Register a disposable item', async () => { - const disposable = { dispose: noop }; - when(debugService.registerDebugAdapterTrackerFactory(anything(), anything())).thenReturn(disposable); - when(debugService.registerDebugAdapterDescriptorFactory(anything(), anything())).thenReturn(disposable); - when(debugService.onDidStartDebugSession).thenReturn(() => disposable); - - await activator.activate(); - - assert.deepEqual(disposableRegistry, [disposable, disposable, disposable, disposable]); - }); -}); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts index 7c7977ab8480..ae13ad375371 100644 --- a/extensions/positron-python/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts @@ -6,36 +6,20 @@ import { expect } from 'chai'; import * as typemoq from 'typemoq'; import { DebugConfiguration, Uri } from 'vscode'; -import { IMultiStepInputFactory, MultiStepInput } from '../../../../client/common/utils/multiStepInput'; import { PythonDebugConfigurationService } from '../../../../client/debugger/extension/configuration/debugConfigurationService'; import { IDebugConfigurationResolver } from '../../../../client/debugger/extension/configuration/types'; -import { DebugConfigurationState } from '../../../../client/debugger/extension/types'; import { AttachRequestArguments, LaunchRequestArguments } from '../../../../client/debugger/types'; suite('Debugging - Configuration Service', () => { let attachResolver: typemoq.IMock>; let launchResolver: typemoq.IMock>; let configService: TestPythonDebugConfigurationService; - let multiStepFactory: typemoq.IMock; - class TestPythonDebugConfigurationService extends PythonDebugConfigurationService { - public static async pickDebugConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, - ) { - return PythonDebugConfigurationService.pickDebugConfiguration(input, state); - } - } + class TestPythonDebugConfigurationService extends PythonDebugConfigurationService {} setup(() => { attachResolver = typemoq.Mock.ofType>(); launchResolver = typemoq.Mock.ofType>(); - multiStepFactory = typemoq.Mock.ofType(); - - configService = new TestPythonDebugConfigurationService( - attachResolver.object, - launchResolver.object, - multiStepFactory.object, - ); + configService = new TestPythonDebugConfigurationService(attachResolver.object, launchResolver.object); }); test('Should use attach resolver when passing attach config', async () => { const config = ({ @@ -86,96 +70,4 @@ suite('Debugging - Configuration Service', () => { launchResolver.verifyAll(); }); }); - test('Picker should be displayed', async () => { - const state = ({ configs: [], folder: {}, token: undefined } as unknown) as DebugConfigurationState; - const multiStepInput = typemoq.Mock.ofType>(); - multiStepInput - .setup((i) => i.showQuickPick(typemoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.once()); - - await TestPythonDebugConfigurationService.pickDebugConfiguration(multiStepInput.object, state); - - multiStepInput.verifyAll(); - }); - test('Existing Configuration items must be removed before displaying picker', async () => { - const state = ({ configs: [1, 2, 3], folder: {}, token: undefined } as unknown) as DebugConfigurationState; - const multiStepInput = typemoq.Mock.ofType>(); - multiStepInput - .setup((i) => i.showQuickPick(typemoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.once()); - - await TestPythonDebugConfigurationService.pickDebugConfiguration(multiStepInput.object, state); - - multiStepInput.verifyAll(); - expect(Object.keys(state.config)).to.be.lengthOf(0); - }); - test('Ensure generated config is returned', async () => { - const expectedConfig = { yes: 'Updated' }; - const multiStepInput = { - run: (_: unknown, state: DebugConfiguration) => { - Object.assign(state.config, expectedConfig); - return Promise.resolve(); - }, - }; - multiStepFactory - .setup((f) => f.create()) - .returns(() => multiStepInput as MultiStepInput) - .verifiable(typemoq.Times.once()); - TestPythonDebugConfigurationService.pickDebugConfiguration = (_, state) => { - Object.assign(state.config, expectedConfig); - return Promise.resolve(); - }; - const config = await configService.provideDebugConfigurations!(({} as unknown) as undefined); - - multiStepFactory.verifyAll(); - expect(config).to.deep.equal([expectedConfig]); - }); - test('Ensure `undefined` is returned if QuickPick is cancelled', async () => { - const multiStepInput = { - run: (_: unknown, _state: DebugConfiguration) => Promise.resolve(), - }; - const folder = { name: '1', index: 0, uri: Uri.parse('1234') }; - multiStepFactory - .setup((f) => f.create()) - .returns(() => multiStepInput as MultiStepInput) - .verifiable(typemoq.Times.once()); - const config = await configService.resolveDebugConfiguration(folder, {} as DebugConfiguration); - - multiStepFactory.verifyAll(); - - expect(config).to.equal(undefined, `Config should be undefined`); - }); - test('Use cached debug configuration', async () => { - const folder = { name: '1', index: 0, uri: Uri.parse('1234') }; - const expectedConfig = { - name: 'File', - type: 'python', - request: 'launch', - program: '${file}', - console: 'integratedTerminal', - }; - const multiStepInput = { - run: (_: unknown, state: DebugConfiguration) => { - Object.assign(state.config, expectedConfig); - return Promise.resolve(); - }, - }; - multiStepFactory - .setup((f) => f.create()) - .returns(() => multiStepInput as MultiStepInput) - .verifiable(typemoq.Times.once()); // this should be called only once. - - launchResolver - .setup((a) => a.resolveDebugConfiguration(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) - .returns(() => Promise.resolve(expectedConfig as LaunchRequestArguments)) - .verifiable(typemoq.Times.exactly(2)); // this should be called twice with the same config. - - await configService.resolveDebugConfiguration(folder, {} as DebugConfiguration); - await configService.resolveDebugConfiguration(folder, {} as DebugConfiguration); - - multiStepFactory.verifyAll(); - launchResolver.verifyAll(); - }); }); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts deleted file mode 100644 index a850d50150ae..000000000000 --- a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { deepEqual, instance, mock, verify } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { - CancellationTokenSource, - CompletionItem, - CompletionItemKind, - Position, - SnippetString, - TextDocument, - Uri, -} from 'vscode'; -import { LanguageService } from '../../../../../client/common/application/languageService'; -import { ILanguageService } from '../../../../../client/common/application/types'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { LaunchJsonCompletionProvider } from '../../../../../client/debugger/extension/configuration/launch.json/completionProvider'; - -suite('Debugging - launch.json Completion Provider', () => { - let completionProvider: LaunchJsonCompletionProvider; - let languageService: ILanguageService; - - setup(() => { - languageService = mock(LanguageService); - completionProvider = new LaunchJsonCompletionProvider(instance(languageService), []); - }); - test('Activation will register the completion provider', async () => { - await completionProvider.activate(); - verify( - languageService.registerCompletionItemProvider(deepEqual({ language: 'json' }), completionProvider), - ).once(); - verify( - languageService.registerCompletionItemProvider(deepEqual({ language: 'jsonc' }), completionProvider), - ).once(); - }); - test('Cannot provide completions for non launch.json files', () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - document.setup((doc) => doc.uri).returns(() => Uri.file(__filename)); - assert.strictEqual(LaunchJsonCompletionProvider.canProvideCompletions(document.object, position), false); - - document.reset(); - document.setup((doc) => doc.uri).returns(() => Uri.file('settings.json')); - assert.strictEqual(LaunchJsonCompletionProvider.canProvideCompletions(document.object, position), false); - }); - function testCanProvideCompletions(position: Position, offset: number, json: string, expectedValue: boolean) { - const document = typemoq.Mock.ofType(); - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.uri).returns(() => Uri.file('launch.json')); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => offset); - const canProvideCompletions = LaunchJsonCompletionProvider.canProvideCompletions(document.object, position); - assert.strictEqual(canProvideCompletions, expectedValue); - } - test('Cannot provide completions when there is no configurations section in json', () => { - const position = new Position(0, 0); - const config = `{ - "version": "0.1.0" -}`; - testCanProvideCompletions(position, 1, config as string, false); - }); - test('Cannot provide completions when cursor position is not in configurations array', () => { - const position = new Position(0, 0); - const json = `{ - "version": "0.1.0", - "configurations": [] -}`; - testCanProvideCompletions(position, 10, json, false); - }); - test('Cannot provide completions when cursor position is in an empty configurations array', () => { - const position = new Position(0, 0); - const json = `{ - "version": "0.1.0", - "configurations": [ - # Cursor Position - ] -}`; - testCanProvideCompletions(position, json.indexOf('# Cursor Position'), json, true); - }); - test('No Completions for non launch.json', async () => { - const document = typemoq.Mock.ofType(); - document.setup((doc) => doc.uri).returns(() => Uri.file('settings.json')); - const { token } = new CancellationTokenSource(); - const position = new Position(0, 0); - - const completions = await completionProvider.provideCompletionItems(document.object, position, token); - - assert.strictEqual(completions.length, 0); - }); - test('No Completions for files ending with launch.json', async () => { - const document = typemoq.Mock.ofType(); - document.setup((doc) => doc.uri).returns(() => Uri.file('x-launch.json')); - const { token } = new CancellationTokenSource(); - const position = new Position(0, 0); - - const completions = await completionProvider.provideCompletionItems(document.object, position, token); - - assert.strictEqual(completions.length, 0); - }); - test('Get Completions', async () => { - const json = `{ - "version": "0.1.0", - "configurations": [ - # Cursor Position - ] -}`; - - const document = typemoq.Mock.ofType(); - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.uri).returns(() => Uri.file('launch.json')); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('# Cursor Position')); - const position = new Position(0, 0); - const { token } = new CancellationTokenSource(); - - const completions = await completionProvider.provideCompletionItems(document.object, position, token); - - assert.strictEqual(completions.length, 1); - - const expectedCompletionItem: CompletionItem = { - command: { - command: 'python.SelectAndInsertDebugConfiguration', - title: DebugConfigStrings.launchJsonCompletions.description, - arguments: [document.object, position, token], - }, - documentation: DebugConfigStrings.launchJsonCompletions.description, - sortText: 'AAAA', - preselect: true, - kind: CompletionItemKind.Enum, - label: DebugConfigStrings.launchJsonCompletions.label, - insertText: new SnippetString(), - }; - - assert.deepEqual(completions[0], expectedCompletionItem); - }); -}); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts deleted file mode 100644 index b2addd24267b..000000000000 --- a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { instance, mock, verify } from 'ts-mockito'; -import { CommandManager } from '../../../../../client/common/application/commandManager'; -import { ICommandManager } from '../../../../../client/common/application/types'; -import { PythonDebugConfigurationService } from '../../../../../client/debugger/extension/configuration/debugConfigurationService'; -import { LaunchJsonUpdaterService } from '../../../../../client/debugger/extension/configuration/launch.json/updaterService'; -import { LaunchJsonUpdaterServiceHelper } from '../../../../../client/debugger/extension/configuration/launch.json/updaterServiceHelper'; -import { IDebugConfigurationService } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - launch.json Updater Service', () => { - let helper: LaunchJsonUpdaterServiceHelper; - let commandManager: ICommandManager; - let debugConfigService: IDebugConfigurationService; - setup(() => { - commandManager = mock(CommandManager); - debugConfigService = mock(PythonDebugConfigurationService); - helper = new LaunchJsonUpdaterServiceHelper(instance(debugConfigService)); - }); - test('Activation will register the required commands', async () => { - const service = new LaunchJsonUpdaterService([], instance(debugConfigService)); - await service.activate(); - verify( - commandManager.registerCommand( - 'python.SelectAndInsertDebugConfiguration', - helper.selectAndInsertDebugConfig, - helper, - ), - ); - }); -}); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/updaterServerHelper.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/updaterServerHelper.unit.test.ts deleted file mode 100644 index 53118d68025e..000000000000 --- a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/updaterServerHelper.unit.test.ts +++ /dev/null @@ -1,496 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as sinon from 'sinon'; -import { instance, mock, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { - CancellationTokenSource, - DebugConfiguration, - Position, - Range, - TextDocument, - TextEditor, - TextLine, - Uri, -} from 'vscode'; -import { PythonDebugConfigurationService } from '../../../../../client/debugger/extension/configuration/debugConfigurationService'; -import { LaunchJsonUpdaterServiceHelper } from '../../../../../client/debugger/extension/configuration/launch.json/updaterServiceHelper'; -import { IDebugConfigurationService } from '../../../../../client/debugger/extension/types'; -import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; -import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; -import * as commandApis from '../../../../../client/common/vscodeApis/commandApis'; - -type LaunchJsonSchema = { - version: string; - configurations: DebugConfiguration[]; -}; - -suite('Debugging - launch.json Updater Service', () => { - let helper: LaunchJsonUpdaterServiceHelper; - let getWorkspaceFolderStub: sinon.SinonStub; - let getActiveTextEditorStub: sinon.SinonStub; - let applyEditStub: sinon.SinonStub; - let executeCommandStub: sinon.SinonStub; - let debugConfigService: IDebugConfigurationService; - - const sandbox = sinon.createSandbox(); - setup(() => { - getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); - getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); - applyEditStub = sinon.stub(workspaceApis, 'applyEdit'); - executeCommandStub = sinon.stub(commandApis, 'executeCommand'); - - debugConfigService = mock(PythonDebugConfigurationService); - sandbox.stub(LaunchJsonUpdaterServiceHelper, 'isCommaImmediatelyBeforeCursor').returns(false); - helper = new LaunchJsonUpdaterServiceHelper(instance(debugConfigService)); - }); - teardown(() => { - sandbox.restore(); - sinon.restore(); - }); - - test('Configuration Array is detected as being empty', async () => { - const document = typemoq.Mock.ofType(); - const config: LaunchJsonSchema = { - version: '', - configurations: [], - }; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); - - const isEmpty = LaunchJsonUpdaterServiceHelper.isConfigurationArrayEmpty(document.object); - assert.strictEqual(isEmpty, true); - }); - test('Configuration Array is not empty', async () => { - const document = typemoq.Mock.ofType(); - const config: LaunchJsonSchema = { - version: '', - configurations: [ - { - name: '', - request: 'launch', - type: 'python', - }, - ], - }; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); - - const isEmpty = LaunchJsonUpdaterServiceHelper.isConfigurationArrayEmpty(document.object); - assert.strictEqual(isEmpty, false); - }); - test('Cursor is not positioned in the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const config: LaunchJsonSchema = { - version: '', - configurations: [ - { - name: '', - request: 'launch', - type: 'python', - }, - ], - }; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => 10); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, undefined); - }); - test('Cursor is positioned in the empty configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - # Cursor Position - ] - }`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('#')); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, 'InsideEmptyArray'); - }); - test('Cursor is positioned before an item in the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - } - ] -}`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.lastIndexOf('{') - 1); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, 'BeforeItem'); - }); - test('Cursor is positioned before an item in the middle of the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - },{ - "name":"wow" - } - ] -}`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf(',{') + 1); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, 'BeforeItem'); - }); - test('Cursor is positioned after an item in the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - }] -}`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.lastIndexOf('}]') + 1); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, 'AfterItem'); - }); - test('Cursor is positioned after an item in the middle of the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - },{ - "name":"wow" - } - ] -}`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('},') + 1); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, 'AfterItem'); - }); - test('Text to be inserted must be prefixed with a comma', async () => { - const config = {} as DebugConfiguration; - const expectedText = `,${JSON.stringify(config)}`; - - const textToInsert = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, 'AfterItem'); - - assert.strictEqual(textToInsert, expectedText); - }); - test('Text to be inserted must not be prefixed with a comma (as a comma already exists)', async () => { - const config = {} as DebugConfiguration; - const expectedText = JSON.stringify(config); - - const textToInsert = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, 'AfterItem', 'BeforeCursor'); - - assert.strictEqual(textToInsert, expectedText); - }); - test('Text to be inserted must be suffixed with a comma', async () => { - const config = {} as DebugConfiguration; - const expectedText = `${JSON.stringify(config)},`; - - const textToInsert = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, 'BeforeItem'); - - assert.strictEqual(textToInsert, expectedText); - }); - test('Text to be inserted must not be prefixed nor suffixed with commas', async () => { - const config = {} as DebugConfiguration; - const expectedText = JSON.stringify(config); - - const textToInsert = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, 'InsideEmptyArray'); - - assert.strictEqual(textToInsert, expectedText); - }); - test('When inserting the debug config into the json file format the document', async () => { - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - },{ - "name":"wow" - } - ] -}`; - const config = {} as DebugConfiguration; - const document = typemoq.Mock.ofType(); - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('},') + 1); - applyEditStub.returns(undefined); - executeCommandStub.withArgs('editor.action.formatDocument').resolves(); - - await LaunchJsonUpdaterServiceHelper.insertDebugConfiguration(document.object, new Position(0, 0), config); - - sinon.assert.calledOnce(applyEditStub); - sinon.assert.calledOnce(executeCommandStub.withArgs('editor.action.formatDocument')); - }); - test('No changes to configuration if there is not active document', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const { token } = new CancellationTokenSource(); - getActiveTextEditorStub.returns(undefined); - let debugConfigInserted = false; - LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - sinon.assert.calledOnce(getActiveTextEditorStub); - sinon.assert.notCalled(getWorkspaceFolderStub); - assert.strictEqual(debugConfigInserted, false); - }); - test('No changes to configuration if the active document is not same as the document passed in', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const { token } = new CancellationTokenSource(); - const textEditor = typemoq.Mock.ofType(); - textEditor - .setup((t) => t.document) - .returns(() => ('x' as unknown) as TextDocument) - .verifiable(typemoq.Times.atLeastOnce()); - getActiveTextEditorStub.returns(textEditor.object); - let debugConfigInserted = false; - LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - sinon.assert.calledOnce(getActiveTextEditorStub); - sinon.assert.notCalled(getWorkspaceFolderStub); - textEditor.verifyAll(); - assert.strictEqual(debugConfigInserted, false); - }); - test('No changes to configuration if cancellation token has been cancelled', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const tokenSource = new CancellationTokenSource(); - tokenSource.cancel(); - const { token } = tokenSource; - const textEditor = typemoq.Mock.ofType(); - const docUri = Uri.file(__filename); - const folderUri = Uri.file('Folder Uri'); - const folder = { name: '', index: 0, uri: folderUri }; - document - .setup((doc) => doc.uri) - .returns(() => docUri) - .verifiable(typemoq.Times.atLeastOnce()); - textEditor - .setup((t) => t.document) - .returns(() => document.object) - .verifiable(typemoq.Times.atLeastOnce()); - getActiveTextEditorStub.returns(textEditor.object); - getWorkspaceFolderStub.returns(folder); - when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve(([''] as unknown) as void); - let debugConfigInserted = false; - LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - sinon.assert.calledOnce(getActiveTextEditorStub); - sinon.assert.calledOnce(getWorkspaceFolderStub); - textEditor.verifyAll(); - document.verifyAll(); - assert.strictEqual(debugConfigInserted, false); - }); - test('No changes to configuration if no configuration items are returned', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const tokenSource = new CancellationTokenSource(); - const { token } = tokenSource; - const textEditor = typemoq.Mock.ofType(); - const docUri = Uri.file(__filename); - const folderUri = Uri.file('Folder Uri'); - const folder = { name: '', index: 0, uri: folderUri }; - document - .setup((doc) => doc.uri) - .returns(() => docUri) - .verifiable(typemoq.Times.atLeastOnce()); - textEditor - .setup((t) => t.document) - .returns(() => document.object) - .verifiable(typemoq.Times.atLeastOnce()); - - getActiveTextEditorStub.returns(textEditor.object); - getWorkspaceFolderStub.returns(folder); - - when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve(([] as unknown) as void); - let debugConfigInserted = false; - LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - sinon.assert.calledOnce(getActiveTextEditorStub); - sinon.assert.calledOnce(getWorkspaceFolderStub.withArgs(docUri)); - textEditor.verifyAll(); - document.verifyAll(); - assert.strictEqual(debugConfigInserted, false); - }); - test('Changes are made to the configuration', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const tokenSource = new CancellationTokenSource(); - const { token } = tokenSource; - const textEditor = typemoq.Mock.ofType(); - const docUri = Uri.file(__filename); - const folderUri = Uri.file('Folder Uri'); - const folder = { name: '', index: 0, uri: folderUri }; - document - .setup((doc) => doc.uri) - .returns(() => docUri) - .verifiable(typemoq.Times.atLeastOnce()); - textEditor - .setup((t) => t.document) - .returns(() => document.object) - .verifiable(typemoq.Times.atLeastOnce()); - getActiveTextEditorStub.returns(textEditor.object); - getWorkspaceFolderStub.withArgs(docUri).returns(folder); - when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve(([ - 'config', - ] as unknown) as void); - let debugConfigInserted = false; - LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - sinon.assert.called(getActiveTextEditorStub); - sinon.assert.calledOnce(getWorkspaceFolderStub.withArgs(docUri)); - textEditor.verifyAll(); - document.verifyAll(); - assert.strictEqual(debugConfigInserted, true); - }); - test('If cursor is at the begining of line 1 then there is no comma before cursor', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(1, 0); - document - .setup((doc) => doc.lineAt(1)) - .returns(() => ({ range: new Range(1, 0, 1, 1) } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => '') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(!isBeforeCursor); - document.verifyAll(); - }); - test('If cursor is positioned after some text (not a comma) then detect this', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(2, 2); - document - .setup((doc) => doc.lineAt(2)) - .returns(() => ({ range: new Range(2, 0, 1, 5) } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => 'Hello') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(!isBeforeCursor); - document.verifyAll(); - }); - test('If cursor is positioned after a comma then detect this', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(2, 2); - document - .setup((doc) => doc.lineAt(2)) - .returns(() => ({ range: new Range(2, 0, 2, 3) } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => '}, ') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(isBeforeCursor); - document.verifyAll(); - }); - test('If cursor is positioned in an empty line and previous line ends with comma, then detect this', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(2, 2); - document - .setup((doc) => doc.lineAt(1)) - .returns(() => ({ range: new Range(1, 0, 1, 3), text: '}, ' } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.lineAt(2)) - .returns(() => ({ range: new Range(2, 0, 2, 3), text: ' ' } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => ' ') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(isBeforeCursor); - document.verifyAll(); - }); - test('If cursor is positioned in an empty line and previous line does not end with comma, then detect this', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(2, 2); - document - .setup((doc) => doc.lineAt(1)) - .returns(() => ({ range: new Range(1, 0, 1, 3), text: '} ' } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.lineAt(2)) - .returns(() => ({ range: new Range(2, 0, 2, 3), text: ' ' } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => ' ') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(!isBeforeCursor); - document.verifyAll(); - }); -}); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts deleted file mode 100644 index 8a5898611c82..000000000000 --- a/extensions/positron-python/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { Uri } from 'vscode'; -import { expect } from 'chai'; -import * as path from 'path'; -import * as fs from 'fs-extra'; -import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; -import { resolveVariables } from '../../../../../client/debugger/extension/configuration/utils/common'; -import * as djangoLaunch from '../../../../../client/debugger/extension/configuration/providers/djangoLaunch'; -import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; - -suite('Debugging - Configuration Provider Django', () => { - let pathExistsStub: sinon.SinonStub; - let pathSeparatorStub: sinon.SinonStub; - let workspaceStub: sinon.SinonStub; - let input: MultiStepInput; - - setup(() => { - input = mock>(MultiStepInput); - pathExistsStub = sinon.stub(fs, 'pathExists'); - pathSeparatorStub = sinon.stub(path, 'sep'); - workspaceStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); - }); - teardown(() => { - sinon.restore(); - }); - test("getManagePyPath should return undefined if file doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const managePyPath = path.join(folder.uri.fsPath, 'manage.py'); - pathExistsStub.withArgs(managePyPath).resolves(false); - const file = await djangoLaunch.getManagePyPath(folder); - - expect(file).to.be.equal(undefined, 'Should return undefined'); - }); - test('getManagePyPath should file path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const managePyPath = path.join(folder.uri.fsPath, 'manage.py'); - pathExistsStub.withArgs(managePyPath).resolves(true); - pathSeparatorStub.value('-'); - const file = await djangoLaunch.getManagePyPath(folder); - - expect(file).to.be.equal('${workspaceFolder}-manage.py'); - }); - test('Resolve variables (with resource)', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - workspaceStub.returns(folder); - const resolvedPath = resolveVariables('${workspaceFolder}/one.py', undefined, folder); - - expect(resolvedPath).to.be.equal(`${folder.uri.fsPath}/one.py`); - }); - test('Validation of path should return errors if path is undefined', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await djangoLaunch.validateManagePy(folder, ''); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if path is empty', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await djangoLaunch.validateManagePy(folder, '', ''); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is empty', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await djangoLaunch.validateManagePy(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test("Validation of path should return errors if resolved path doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz').resolves(false); - const error = await djangoLaunch.validateManagePy(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is non-python', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz.txt').resolves(true); - const error = await djangoLaunch.validateManagePy(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is python', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz.py').resolves(true); - const error = await djangoLaunch.validateManagePy(folder, '', 'xyz.py'); - - expect(error).to.be.equal(undefined, 'should not have errors'); - }); - test('Launch JSON with selected managepy path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - pathSeparatorStub.value('-'); - when(input.showInputBox(anything())).thenResolve('hello'); - await djangoLaunch.buildDjangoLaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.django.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: 'hello', - args: ['runserver'], - django: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with default managepy path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - const workspaceFolderToken = '${workspaceFolder}'; - const defaultProgram = `${workspaceFolderToken}-manage.py`; - pathSeparatorStub.value('-'); - when(input.showInputBox(anything())).thenResolve(); - await djangoLaunch.buildDjangoLaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.django.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: defaultProgram, - args: ['runserver'], - django: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts deleted file mode 100644 index 80ce37167024..000000000000 --- a/extensions/positron-python/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import * as fs from 'fs-extra'; -import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import * as fastApiLaunch from '../../../../../client/debugger/extension/configuration/providers/fastapiLaunch'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider FastAPI', () => { - let input: MultiStepInput; - let pathExistsStub: sinon.SinonStub; - - setup(() => { - input = mock>(MultiStepInput); - pathExistsStub = sinon.stub(fs, 'pathExists'); - }); - teardown(() => { - sinon.restore(); - }); - test("getApplicationPath should return undefined if file doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const appPyPath = path.join(folder.uri.fsPath, 'main.py'); - pathExistsStub.withArgs(appPyPath).resolves(false); - const file = await fastApiLaunch.getApplicationPath(folder); - - expect(file).to.be.equal(undefined, 'Should return undefined'); - }); - test('getApplicationPath should find path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const appPyPath = path.join(folder.uri.fsPath, 'main.py'); - pathExistsStub.withArgs(appPyPath).resolves(true); - const file = await fastApiLaunch.getApplicationPath(folder); - - expect(file).to.be.equal('main.py'); - }); - test('Launch JSON with valid python path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - await fastApiLaunch.buildFastAPILaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.fastapi.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'uvicorn', - args: ['main:app', '--reload'], - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with selected app path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - when(input.showInputBox(anything())).thenResolve('main'); - - await fastApiLaunch.buildFastAPILaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.fastapi.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'uvicorn', - args: ['main:app', '--reload'], - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts deleted file mode 100644 index f627c7558c51..000000000000 --- a/extensions/positron-python/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { buildFileLaunchDebugConfiguration } from '../../../../../client/debugger/extension/configuration/providers/fileLaunch'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider File', () => { - test('Launch JSON with default managepy path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - await buildFileLaunchDebugConfiguration( - (undefined as unknown) as MultiStepInput, - state, - ); - - const config = { - name: DebugConfigStrings.file.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: '${file}', - console: 'integratedTerminal', - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts deleted file mode 100644 index 08fb5259b282..000000000000 --- a/extensions/positron-python/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import * as fs from 'fs-extra'; -import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; -import * as flaskLaunch from '../../../../../client/debugger/extension/configuration/providers/flaskLaunch'; - -suite('Debugging - Configuration Provider Flask', () => { - let pathExistsStub: sinon.SinonStub; - let input: MultiStepInput; - setup(() => { - input = mock>(MultiStepInput); - pathExistsStub = sinon.stub(fs, 'pathExists'); - }); - teardown(() => { - sinon.restore(); - }); - test("getApplicationPath should return undefined if file doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const appPyPath = path.join(folder.uri.fsPath, 'app.py'); - pathExistsStub.withArgs(appPyPath).resolves(false); - const file = await flaskLaunch.getApplicationPath(folder); - - expect(file).to.be.equal(undefined, 'Should return undefined'); - }); - test('getApplicationPath should file path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const appPyPath = path.join(folder.uri.fsPath, 'app.py'); - pathExistsStub.withArgs(appPyPath).resolves(true); - const file = await flaskLaunch.getApplicationPath(folder); - - expect(file).to.be.equal('app.py'); - }); - test('Launch JSON with valid python path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - await flaskLaunch.buildFlaskLaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.flask.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: 'app.py', - FLASK_DEBUG: '1', - }, - args: ['run', '--no-debugger', '--no-reload'], - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with selected app path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - when(input.showInputBox(anything())).thenResolve('hello'); - - await flaskLaunch.buildFlaskLaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.flask.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: 'hello', - FLASK_DEBUG: '1', - }, - args: ['run', '--no-debugger', '--no-reload'], - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with default managepy path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - when(input.showInputBox(anything())).thenResolve(); - - await flaskLaunch.buildFlaskLaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.flask.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: 'app.py', - FLASK_DEBUG: '1', - }, - args: ['run', '--no-debugger', '--no-reload'], - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts deleted file mode 100644 index 2508db506ca2..000000000000 --- a/extensions/positron-python/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { buildModuleLaunchConfiguration } from '../../../../../client/debugger/extension/configuration/providers/moduleLaunch'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider Module', () => { - test('Launch JSON with default module name', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - const input = mock>(MultiStepInput); - - when(input.showInputBox(anything())).thenResolve(); - - await buildModuleLaunchConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.module.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: DebugConfigStrings.module.snippet.default, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with selected module name', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - const input = mock>(MultiStepInput); - - when(input.showInputBox(anything())).thenResolve('hello'); - - await buildModuleLaunchConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.module.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'hello', - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts deleted file mode 100644 index 8217e150aa01..000000000000 --- a/extensions/positron-python/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { buildPidAttachConfiguration } from '../../../../../client/debugger/extension/configuration/providers/pidAttach'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider File', () => { - test('Launch JSON with default process id', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - await buildPidAttachConfiguration((undefined as unknown) as MultiStepInput, state); - - const config = { - name: DebugConfigStrings.attachPid.snippet.name, - type: DebuggerTypeName, - request: 'attach', - processId: '${command:pickProcess}', - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts deleted file mode 100644 index 688215259a2f..000000000000 --- a/extensions/positron-python/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import * as fs from 'fs-extra'; -import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { resolveVariables } from '../../../../../client/debugger/extension/configuration/utils/common'; -import * as pyramidLaunch from '../../../../../client/debugger/extension/configuration/providers/pyramidLaunch'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; -import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; - -suite('Debugging - Configuration Provider Pyramid', () => { - let input: MultiStepInput; - let pathExistsStub: sinon.SinonStub; - let pathSeparatorStub: sinon.SinonStub; - let workspaceStub: sinon.SinonStub; - - setup(() => { - input = mock>(MultiStepInput); - pathExistsStub = sinon.stub(fs, 'pathExists'); - pathSeparatorStub = sinon.stub(path, 'sep'); - workspaceStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); - }); - teardown(() => { - sinon.restore(); - }); - test("getDevelopmentIniPath should return undefined if file doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const managePyPath = path.join(folder.uri.fsPath, 'development.ini'); - pathExistsStub.withArgs(managePyPath).resolves(false); - const file = await pyramidLaunch.getDevelopmentIniPath(folder); - - expect(file).to.be.equal(undefined, 'Should return undefined'); - }); - test('getDevelopmentIniPath should file path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const managePyPath = path.join(folder.uri.fsPath, 'development.ini'); - pathSeparatorStub.value('-'); - pathExistsStub.withArgs(managePyPath).resolves(true); - const file = await pyramidLaunch.getDevelopmentIniPath(folder); - - expect(file).to.be.equal('${workspaceFolder}-development.ini'); - }); - test('Resolve variables (with resource)', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - workspaceStub.returns(folder); - const resolvedPath = resolveVariables('${workspaceFolder}/one.py', undefined, folder); - - expect(resolvedPath).to.be.equal(`${folder.uri.fsPath}/one.py`); - }); - test('Validation of path should return errors if path is undefined', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await pyramidLaunch.validateIniPath(folder, ''); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if path is empty', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await pyramidLaunch.validateIniPath(folder, '', ''); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is empty', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await pyramidLaunch.validateIniPath(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test("Validation of path should return errors if resolved path doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz').resolves(false); - const error = await pyramidLaunch.validateIniPath(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is non-ini', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz.txt').resolves(true); - const error = await pyramidLaunch.validateIniPath(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should not return errors if resolved path is ini', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz.ini').resolves(true); - const error = await pyramidLaunch.validateIniPath(folder, '', 'xyz.ini'); - - expect(error).to.be.equal(undefined, 'should not have errors'); - }); - test('Launch JSON with valid ini path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - pathSeparatorStub.value('-'); - - await pyramidLaunch.buildPyramidLaunchConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.pyramid.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'pyramid.scripts.pserve', - args: ['${workspaceFolder}-development.ini'], - pyramid: true, - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with selected ini path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - pathSeparatorStub.value('-'); - when(input.showInputBox(anything())).thenResolve('hello'); - - await pyramidLaunch.buildPyramidLaunchConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.pyramid.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'pyramid.scripts.pserve', - args: ['hello'], - pyramid: true, - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with default ini path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - const workspaceFolderToken = '${workspaceFolder}'; - const defaultIni = `${workspaceFolderToken}-development.ini`; - - pathSeparatorStub.value('-'); - when(input.showInputBox(anything())).thenResolve(); - - await pyramidLaunch.buildPyramidLaunchConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.pyramid.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'pyramid.scripts.pserve', - args: [defaultIni], - pyramid: true, - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts deleted file mode 100644 index 323cda94a1eb..000000000000 --- a/extensions/positron-python/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import * as sinon from 'sinon'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import * as configuration from '../../../../../client/debugger/extension/configuration/utils/configuration'; -import * as remoteAttach from '../../../../../client/debugger/extension/configuration/providers/remoteAttach'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider Remote Attach', () => { - let input: MultiStepInput; - - setup(() => { - input = mock>(MultiStepInput); - }); - teardown(() => { - sinon.restore(); - }); - test('Configure port will display prompt', async () => { - when(input.showInputBox(anything())).thenResolve(); - - await configuration.configurePort(instance(input), {}); - - verify(input.showInputBox(anything())).once(); - }); - test('Configure port will default to 5678 if entered value is not a number', async () => { - const config: { connect?: { port?: number } } = {}; - when(input.showInputBox(anything())).thenResolve('xyz'); - - await configuration.configurePort(instance(input), config); - - verify(input.showInputBox(anything())).once(); - expect(config).to.be.deep.equal({ connect: { port: 5678 } }); - }); - test('Configure port will default to 5678', async () => { - const config: { connect?: { port?: number } } = {}; - when(input.showInputBox(anything())).thenResolve(); - - await configuration.configurePort(instance(input), config); - - verify(input.showInputBox(anything())).once(); - expect(config).to.be.deep.equal({ connect: { port: 5678 } }); - }); - test('Configure port will use user selected value', async () => { - const config: { connect?: { port?: number } } = {}; - when(input.showInputBox(anything())).thenResolve('1234'); - - await configuration.configurePort(instance(input), config); - - verify(input.showInputBox(anything())).once(); - expect(config).to.be.deep.equal({ connect: { port: 1234 } }); - }); - test('Launch JSON with default host name', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - let portConfigured = false; - when(input.showInputBox(anything())).thenResolve(); - - sinon.stub(configuration, 'configurePort').callsFake(async () => { - portConfigured = true; - }); - - const configurePort = await remoteAttach.buildRemoteAttachConfiguration(instance(input), state); - if (configurePort) { - await configurePort!(input, state); - } - - const config = { - name: DebugConfigStrings.attach.snippet.name, - type: DebuggerTypeName, - request: 'attach', - connect: { - host: 'localhost', - port: 5678, - }, - pathMappings: [ - { - localRoot: '${workspaceFolder}', - remoteRoot: '.', - }, - ], - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - expect(portConfigured).to.be.equal(true, 'Port not configured'); - }); - test('Launch JSON with user defined host name', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - let portConfigured = false; - when(input.showInputBox(anything())).thenResolve('Hello'); - sinon.stub(configuration, 'configurePort').callsFake(async (_, cfg) => { - portConfigured = true; - cfg.connect!.port = 9999; - }); - const configurePort = await remoteAttach.buildRemoteAttachConfiguration(instance(input), state); - if (configurePort) { - await configurePort(input, state); - } - - const config = { - name: DebugConfigStrings.attach.snippet.name, - type: DebuggerTypeName, - request: 'attach', - connect: { - host: 'Hello', - port: 9999, - }, - pathMappings: [ - { - localRoot: '${workspaceFolder}', - remoteRoot: '.', - }, - ], - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - expect(portConfigured).to.be.equal(true, 'Port not configured'); - }); -}); diff --git a/extensions/positron-python/src/test/debugger/extension/serviceRegistry.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/serviceRegistry.unit.test.ts index 43d81bbe1385..056d722c7e0e 100644 --- a/extensions/positron-python/src/test/debugger/extension/serviceRegistry.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/serviceRegistry.unit.test.ts @@ -11,10 +11,6 @@ import { DebugSessionLoggingFactory } from '../../../client/debugger/extension/a import { OutdatedDebuggerPromptFactory } from '../../../client/debugger/extension/adapter/outdatedDebuggerPrompt'; import { AttachProcessProviderFactory } from '../../../client/debugger/extension/attachQuickPick/factory'; import { IAttachProcessProviderFactory } from '../../../client/debugger/extension/attachQuickPick/types'; -import { PythonDebugConfigurationService } from '../../../client/debugger/extension/configuration/debugConfigurationService'; -import { LaunchJsonCompletionProvider } from '../../../client/debugger/extension/configuration/launch.json/completionProvider'; -import { InterpreterPathCommand } from '../../../client/debugger/extension/configuration/launch.json/interpreterPathCommand'; -import { LaunchJsonUpdaterService } from '../../../client/debugger/extension/configuration/launch.json/updaterService'; import { AttachConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/attach'; import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; import { IDebugConfigurationResolver } from '../../../client/debugger/extension/configuration/types'; @@ -25,7 +21,6 @@ import { IChildProcessAttachService, IDebugSessionEventHandlers } from '../../.. import { registerTypes } from '../../../client/debugger/extension/serviceRegistry'; import { IDebugAdapterDescriptorFactory, - IDebugConfigurationService, IDebugSessionLoggingFactory, IOutdatedDebuggerPromptFactory, } from '../../../client/debugger/extension/types'; @@ -35,43 +30,18 @@ import { IServiceManager } from '../../../client/ioc/types'; suite('Debugging - Service Registry', () => { let serviceManager: IServiceManager; - setup(() => { serviceManager = mock(ServiceManager); }); test('Registrations', () => { registerTypes(instance(serviceManager)); - verify( - serviceManager.addSingleton( - IExtensionSingleActivationService, - InterpreterPathCommand, - ), - ).once(); - verify( - serviceManager.addSingleton( - IDebugConfigurationService, - PythonDebugConfigurationService, - ), - ).once(); verify( serviceManager.addSingleton( IChildProcessAttachService, ChildProcessAttachService, ), ).once(); - verify( - serviceManager.addSingleton( - IExtensionSingleActivationService, - LaunchJsonCompletionProvider, - ), - ).once(); - verify( - serviceManager.addSingleton( - IExtensionSingleActivationService, - LaunchJsonUpdaterService, - ), - ).once(); verify( serviceManager.addSingleton( IExtensionSingleActivationService, diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts b/extensions/positron-python/src/test/interpreters/interpreterPathCommand.unit.test.ts similarity index 85% rename from extensions/positron-python/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts rename to extensions/positron-python/src/test/interpreters/interpreterPathCommand.unit.test.ts index 77077ad945fb..7001453100ec 100644 --- a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts +++ b/extensions/positron-python/src/test/interpreters/interpreterPathCommand.unit.test.ts @@ -8,11 +8,11 @@ import * as sinon from 'sinon'; import { anything, instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Uri } from 'vscode'; -import { IDisposable } from '../../../../../client/common/types'; -import * as commandApis from '../../../../../client/common/vscodeApis/commandApis'; -import { InterpreterPathCommand } from '../../../../../client/debugger/extension/configuration/launch.json/interpreterPathCommand'; -import { IInterpreterService } from '../../../../../client/interpreter/contracts'; -import { PythonEnvironment } from '../../../../../client/pythonEnvironments/info'; +import { IDisposable } from '../../client/common/types'; +import * as commandApis from '../../client/common/vscodeApis/commandApis'; +import { InterpreterPathCommand } from '../../client/interpreter/interpreterPathCommand'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; suite('Interpreter Path Command', () => { let interpreterService: IInterpreterService; diff --git a/extensions/positron-python/src/test/interpreters/serviceRegistry.unit.test.ts b/extensions/positron-python/src/test/interpreters/serviceRegistry.unit.test.ts index 00090eb4b6e9..bb488a49307d 100644 --- a/extensions/positron-python/src/test/interpreters/serviceRegistry.unit.test.ts +++ b/extensions/positron-python/src/test/interpreters/serviceRegistry.unit.test.ts @@ -43,6 +43,7 @@ import { ActivatedEnvironmentLaunch } from '../../client/interpreter/virtualEnvs import { CondaInheritEnvPrompt } from '../../client/interpreter/virtualEnvs/condaInheritEnvPrompt'; import { VirtualEnvironmentPrompt } from '../../client/interpreter/virtualEnvs/virtualEnvPrompt'; import { ServiceManager } from '../../client/ioc/serviceManager'; +import { InterpreterPathCommand } from '../../client/interpreter/interpreterPathCommand'; suite('Interpreters - Service Registry', () => { test('Registrations', () => { @@ -74,6 +75,7 @@ suite('Interpreters - Service Registry', () => { [EnvironmentActivationService, EnvironmentActivationService], [IEnvironmentActivationService, EnvironmentActivationService], + [IExtensionSingleActivationService, InterpreterPathCommand], [IExtensionActivationService, CondaInheritEnvPrompt], [IActivatedEnvironmentLaunch, ActivatedEnvironmentLaunch], ].forEach((mapping) => { diff --git a/extensions/positron-python/src/test/pythonEnvironments/base/info/env.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/base/info/env.unit.test.ts index bb67a4465f9e..20bff8d71249 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/base/info/env.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/base/info/env.unit.test.ts @@ -22,6 +22,9 @@ suite('Environment helpers', () => { version: parseVersionInfo('1.2.3')?.version, binDir: 'distroX/bin', }; + const locationConda1 = 'x/y/z/conda1'; + const locationConda2 = 'x/y/z/conda2'; + const kindConda = PythonEnvKind.Conda; function getEnv(info: { version?: string; arch?: Architecture; @@ -65,6 +68,10 @@ suite('Environment helpers', () => { "Python 3.8.1 64-bit ('my-env')", "Python 3.8.1 64-bit ('my-env')", ], + // conda env.name is empty. + [getEnv({ kind: kindConda }), 'Python', 'Python (conda)'], + [getEnv({ location: locationConda1, kind: kindConda }), "Python ('conda1')", "Python ('conda1': conda)"], + [getEnv({ location: locationConda2, kind: kindConda }), "Python ('conda2')", "Python ('conda2': conda)"], ]; return tests; } diff --git a/extensions/positron-python/yarn.lock b/extensions/positron-python/yarn.lock index 4a5af5ee0b67..92d29bc8f7ed 100644 --- a/extensions/positron-python/yarn.lock +++ b/extensions/positron-python/yarn.lock @@ -1472,10 +1472,10 @@ vscode-languageserver-protocol "^3.17.3-next.1" vscode-uri "^3.0.2" -"@vscode/test-electron@^2.3.4": - version "2.3.5" - resolved "https://registry.yarnpkg.com/@vscode/test-electron/-/test-electron-2.3.5.tgz#c472c5bdce1329aeb4762b8aa7a2cbe7aa783aac" - integrity sha512-lAW7nQ0HuPqJnGJrtCzEKZCICtRizeP6qNanyCrjmdCOAAWjX3ixiG8RVPwqsYPQBWLPgYuE12qQlwXsOR/2fQ== +"@vscode/test-electron@^2.3.8": + version "2.3.9" + resolved "https://registry.yarnpkg.com/@vscode/test-electron/-/test-electron-2.3.9.tgz#f61181392634b408411e4302aef6e1cd2dd41474" + integrity sha512-z3eiChaCQXMqBnk2aHHSEkobmC2VRalFQN0ApOAtydL172zXGxTwGrRtviT5HnUB+Q+G3vtEYFtuQkYqBzYgMA== dependencies: http-proxy-agent "^4.0.1" https-proxy-agent "^5.0.0"