From 54fa5278ef6a44280fe044b5a481e45af5880da6 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 31 Oct 2017 16:27:05 -0700 Subject: [PATCH] 1228 multi root master (#1) * fix #1280 handle env in shebang (#1290) * handle shebangs that resolve paths from env * oops * make test more specific * handle promise * fix #1282 use PYTHONIOENCODING variable (#1291) * fix #1270 debugger contribution changes (#1288) * add onDebug activation * use debug config provider for non workspace debugging * forgot to save file before commiting a merge * ability to opt out of telemetry using vscode settings (#1297) * Fix #1284 debugging parameterized tests (#1299) * fix #1298 remove vscode.startDebug command (#1300) * fix #1298 remove vscode.startDebug command * fix code review comments * added period as per code review comments * #1288 installer config and tests (#1302) * update to use latest api * config changes for multiroot workspace * linting support with multi roots * multi root support for formatters * determine workspace root path * revert change * support multiple configs per workspace folder * modify formatters to use resource specific settings * modified installer to pass resource for workspace resolution * null test in installer * canges to config settings to support multiroot workspace * changes to code refactoring to support workspace symbols * oops * modified to settings are resolved using document uri * unit tests for multi root support * fix unittests for multiroot * exclude files * add new line * config changes for multiroot workspace * new lines and enabled multi root linter tests * fix sys variables * added unit test to resolve ${workspaceRoot} in settings.json * fixed code review comments * fixed code review comments * fix #1276 Pre-commit hooks to ensure code complies with standards (#1277) * sanitize code * gulp file to check code hygiene * fix preLaunchTask in launch.json * added missing packages * enabled pre-commit using husky * enabled a few checks for precommit hook * fix tslint warnings when running tslint via gulp * exclude webpack building, else tries to pull in tests as well * improved checks for commits (strict) * added new lines * 1228 multi root workspace symbols (#1307) * update to use latest api * config changes for multiroot workspace * linting support with multi roots * multi root support for formatters * determine workspace root path * revert change * support multiple configs per workspace folder * modify formatters to use resource specific settings * modified installer to pass resource for workspace resolution * null test in installer * canges to config settings to support multiroot workspace * changes to code refactoring to support workspace symbols * oops * modified to settings are resolved using document uri * unit tests for multi root support * fix unittests for multiroot * exclude files * add new line * config changes for multiroot workspace * new lines and enabled multi root linter tests * fix sys variables * added unit test to resolve ${workspaceRoot} in settings.json * #1228 workspace symbols with multiroot support * fix test * added some data for workspace symbol tests * data for unit tests * fixed to add support for multit roots with unit tests * account for mutiroot files in sub directory * disable all but multiroot tests * fixed tests * include files for tests * Fixed travis tests for multi root workspace symbols (#1306) * added logging * more logging * yay * fixed * more fixes * fix tests * removed logging * enable all tests * uncommented * Added brackets around print statements (for p3) * Fixed travis unit tests (#1308) * update to use latest api * config changes for multiroot workspace * linting support with multi roots * multi root support for formatters * determine workspace root path * revert change * support multiple configs per workspace folder * modify formatters to use resource specific settings * modified installer to pass resource for workspace resolution * null test in installer * canges to config settings to support multiroot workspace * changes to code refactoring to support workspace symbols * oops * modified to settings are resolved using document uri * unit tests for multi root support * fix unittests for multiroot * exclude files * add new line * config changes for multiroot workspace * new lines and enabled multi root linter tests * fix sys variables * added unit test to resolve ${workspaceRoot} in settings.json * #1228 workspace symbols with multiroot support * fix test * added some data for workspace symbol tests * data for unit tests * fixed to add support for multit roots with unit tests * account for mutiroot files in sub directory * disable all but multiroot tests * fixed tests * fix tests * test where failing * properly determine root workspace * fix pytest unit test * delete files * add awaiter * use a path that works on multiple os * fixes * uncomment * invert * debug statements * use default workspace * reverted unwanted changes * oops * test unittests only * more logging * partial fixes to unit tests * run all tests * changes not to set paths for shebang tests * remove comments * update settings only if necessary * fix test * include files for tests * Fixed travis tests for multi root workspace symbols (#1306) * added logging * more logging * yay * fixed * more fixes * fix tests * removed logging * enable all tests * uncommented * Added brackets around print statements (for p3) * use resource when getting settings * fix #1315 unit tests need to wait for extension to activate (#1316) * fix #1314 allow for simultaneous language features (#1317) * #1228 support multi roots in language service (#1309) * update to use latest api * config changes for multiroot workspace * linting support with multi roots * multi root support for formatters * determine workspace root path * revert change * support multiple configs per workspace folder * modify formatters to use resource specific settings * modified installer to pass resource for workspace resolution * null test in installer * canges to config settings to support multiroot workspace * changes to code refactoring to support workspace symbols * oops * modified to settings are resolved using document uri * unit tests for multi root support * fix unittests for multiroot * exclude files * add new line * config changes for multiroot workspace * new lines and enabled multi root linter tests * fix sys variables * added unit test to resolve ${workspaceRoot} in settings.json * #1228 workspace symbols with multiroot support * fix test * added some data for workspace symbol tests * data for unit tests * fixed to add support for multit roots with unit tests * account for mutiroot files in sub directory * disable all but multiroot tests * fixed tests * fix tests * test where failing * properly determine root workspace * fix pytest unit test * delete files * add awaiter * use a path that works on multiple os * fixes * uncomment * invert * debug statements * use default workspace * reverted unwanted changes * oops * test unittests only * more logging * partial fixes to unit tests * run all tests * changes not to set paths for shebang tests * remove comments * update settings only if necessary * fix test * include files for tests * Fixed travis tests for multi root workspace symbols (#1306) * added logging * more logging * yay * fixed * more fixes * fix tests * removed logging * enable all tests * uncommented * Added brackets around print statements (for p3) * use resource when getting settings * support multiroot in language services * add additional tests for #1314 (#1318) * #1228 run all tests under multiroot (#1322) * modifications to fix tests to run under multi root setup * log errors * fix return type * fix linter messages * fix linter errors * changes to ensure code is formatted correctly * fixed comments * delete unwanted file * hide unwanted folders * fixes to linters to run on multiroot setup * udpate settings sequentially * log the output * show errors in deleting dir * removed prospector test, to be completed in #1319 * fixes to tests and sorting provider * fixed test for interpreter display * undo commenting of code * add new line * fix code review issues * ensure else is properly formatted * fix code review comments * fix #1304 preserve empty lines (#1329) * #1228 multiroot interpreter display (#1339) * modifications to fix tests to run under multi root setup * log errors * fix return type * fix linter messages * fix linter errors * changes to ensure code is formatted correctly * fixed comments * delete unwanted file * hide unwanted folders * fixes to linters to run on multiroot setup * udpate settings sequentially * log the output * show errors in deleting dir * removed prospector test, to be completed in #1319 * fixes to tests and sorting provider * fixed test for interpreter display * undo commenting of code * add new line * support multi root in interpreter display * fix linter * changed package version * disabled multiroot test * backwards compatible change * fix nose tests * revert change * enable test but disable it * multi root support in utils.ts * fixed #1328 * retries for flaky unit tests * retry beforeEach * common retry decorator * enable telemetry for extension loads * disable jupyter tests in multiroot tests * clean up python Path before and after testsclean up python Path before and after tests * rename test env variable * dispose cfg settings * dispose cfg settings * update comment * clean up * rearrange to ensurfe launching ext is first debug option * bug fix for display name * resolved code review comment * Fixed typp * 1228 multiroot interpreter ui changes (#1345) * fixes to unit tests and forgotten multiroot * globally retry all tests 3 times * refactor changing interpreters * added tests * fixed linter * removed redundant files * removed unwanted grep * remove blank line * fix 948 remove hardcoding of port number (#1353) * fix #1041 when debugging a test do not cancel it when re-discovering tests (#1354) * fix 1041 when debugging a test do not cancel it when re-discovering tests * create enum for creation of cancellation token * dispose correct cancellationToken * bug fix - in unit tests * bug fix - in unit tests * #1228 multiroot unit test runner (#1357) * fixes to unit tests and forgotten multiroot * globally retry all tests 3 times * refactor changing interpreters * added tests * fixed linter * removed redundant files * temp changes * more changes * lots of refactoring * adding support for multiroot workspaces * removed grep * copy changes for #948 and #1353 into multroot * replicate solution for #1041 and #1354 * #1041 create enum for creation of cancellation token * multiroot support for unit tests * remove empty line (linter warning) * delete pyc before making changes to py file * delete pyc file in teardown * merged multiroot master * pass uri of workspace when displaing prompt for configuration * pass uri to commands * fixed typos based on code review * prefix path with forward slash, as is done in the extension unit tests --- .editorconfig | 17 + .eslintrc | 15 + .gitignore | 5 +- .travis.yml | 1 + .vscode/launch.json | 64 +- .vscode/settings.json | 8 +- .vscode/tasks.json | 80 +- gulpfile.js | 260 ++++++ package.json | 39 +- pythonFiles/PythonTools/ipythonServer.py | 2 +- pythonFiles/completionServer.py | 2 +- src/client/common/configSettings.ts | 188 +++-- src/client/common/constants.ts | 2 +- src/client/common/contextKey.ts | 15 + src/client/common/editor.ts | 4 +- src/client/common/installer.ts | 248 ++++-- src/client/common/interpreterInfoCache.ts | 54 ++ src/client/common/logger.ts | 4 +- src/client/common/systemVariables.ts | 5 +- src/client/common/telemetry.ts | 10 +- src/client/common/utils.ts | 242 +++--- .../configProviders/simpleProvider.ts | 66 ++ src/client/debugger/index.ts | 1 + src/client/extension.ts | 141 ++-- src/client/formatters/baseFormatter.ts | 8 +- .../configuration/pythonPathUpdaterService.ts | 41 + .../pythonPathUpdaterServiceFactory.ts | 18 + .../services/globalUpdaterService.ts | 16 + .../services/workspaceFolderUpdaterService.ts | 23 + .../services/workspaceUpdaterService.ts | 23 + .../configuration/setInterpreterProvider.ts | 114 +++ src/client/interpreter/configuration/types.ts | 12 + src/client/interpreter/contracts.ts | 18 +- src/client/interpreter/display/index.ts | 43 +- .../display/shebangCodeLensProvider.ts | 71 ++ src/client/interpreter/helpers.ts | 18 + src/client/interpreter/index.ts | 81 +- src/client/interpreter/locators/index.ts | 109 ++- .../locators/services/KnownPathsService.ts | 25 +- .../locators/services/condaEnvFileService.ts | 19 +- .../locators/services/condaEnvService.ts | 67 +- .../locators/services/currentPathService.ts | 36 +- .../locators/services/virtualEnvService.ts | 43 +- .../services/windowsRegistryService.ts | 38 +- src/client/jedi/main.ts | 7 +- src/client/jedi/parsers/CompletionParser.ts | 8 +- src/client/jupyter/jupyter_client/main.ts | 2 +- .../languageServices/jediProxyFactory.ts | 38 + src/client/linters/baseLinter.ts | 10 +- .../linters/errorHandlers/invalidArgs.ts | 6 +- src/client/linters/errorHandlers/main.ts | 11 +- .../linters/errorHandlers/notInstalled.ts | 5 +- src/client/linters/errorHandlers/standard.ts | 15 +- src/client/linters/prospector.ts | 8 +- src/client/linters/pydocstyle.ts | 4 +- src/client/providers/completionProvider.ts | 18 +- src/client/providers/definitionProvider.ts | 14 +- .../providers/execInTerminalProvider.ts | 180 ++-- src/client/providers/hoverProvider.ts | 9 +- src/client/providers/importSortProvider.ts | 93 +- src/client/providers/jediProxy.ts | 796 +++++++++--------- .../providers/objectDefinitionProvider.ts | 12 +- src/client/providers/referenceProvider.ts | 9 +- src/client/providers/renameProvider.ts | 17 +- src/client/providers/replProvider.ts | 46 + .../providers/setInterpreterProvider.ts | 72 -- .../providers/shebangCodeLensProvider.ts | 57 -- src/client/providers/signatureProvider.ts | 13 +- .../providers/simpleRefactorProvider.ts | 24 +- src/client/providers/symbolProvider.ts | 11 +- src/client/providers/telemetry.ts | 71 -- src/client/refactor/proxy.ts | 8 +- src/client/singleFileDebug.ts | 51 -- src/client/sortImports.ts | 2 +- src/client/unittests/codeLenses/main.ts | 17 +- src/client/unittests/codeLenses/testFiles.ts | 213 ++--- .../unittests/common/baseTestManager.ts | 157 ++-- .../unittests/common/configSettingService.ts | 67 ++ src/client/unittests/common/constants.ts | 1 + src/client/unittests/common/contracts.ts | 89 -- src/client/unittests/common/debugLauncher.ts | 68 ++ src/client/unittests/common/runner.ts | 45 +- src/client/unittests/common/storageService.ts | 21 + .../common/testConfigurationManager.ts | 50 +- .../unittests/common/testManagerService.ts | 63 ++ .../common/testManagerServiceFactory.ts | 12 + .../unittests/common/testResultsService.ts | 110 +++ src/client/unittests/common/testUtils.ts | 324 ++----- .../common/testVisitors/flatteningVisitor.ts | 65 ++ .../testVisitors/folderGenerationVisitor.ts | 55 ++ .../common/testVisitors/resultResetVisitor.ts | 37 + src/client/unittests/common/types.ts | 152 ++++ .../common/workspaceTestManagerService.ts | 53 ++ src/client/unittests/common/xUnitParser.ts | 33 +- src/client/unittests/configuration.ts | 227 ++--- src/client/unittests/display/main.ts | 77 +- src/client/unittests/display/picker.ts | 85 +- src/client/unittests/main.ts | 389 +++++---- src/client/unittests/nosetest/collector.ts | 27 +- src/client/unittests/nosetest/main.ts | 29 +- src/client/unittests/nosetest/runner.ts | 95 +-- .../nosetest/testConfigurationManager.ts | 66 +- src/client/unittests/pytest/collector.ts | 35 +- src/client/unittests/pytest/main.ts | 28 +- src/client/unittests/pytest/runner.ts | 89 +- .../pytest/testConfigurationManager.ts | 76 +- src/client/unittests/unittest/collector.ts | 47 +- src/client/unittests/unittest/main.ts | 33 +- src/client/unittests/unittest/runner.ts | 130 +-- src/client/unittests/unittest/socketServer.ts | 25 +- .../unittest/testConfigurationManager.ts | 59 +- src/client/workspaceSymbols/generator.ts | 38 +- src/client/workspaceSymbols/main.ts | 98 ++- src/client/workspaceSymbols/parser.ts | 15 +- src/client/workspaceSymbols/provider.ts | 47 +- src/test/.vscode/settings.json | 25 + src/test/.vscode/tags | 721 ++++++++++++++++ src/test/autocomplete/base.test.ts | 11 +- src/test/autocomplete/pep484.test.ts | 8 +- src/test/autocomplete/pep526.test.ts | 8 +- src/test/common.ts | 112 +++ src/test/common/common.test.ts | 58 +- .../common/configSettings.multiroot.test.ts | 184 ++++ src/test/common/configSettings.test.ts | 13 +- src/test/common/installer.test.ts | 35 +- src/test/definitions/hover.test.ts | 3 +- src/test/definitions/parallel.test.ts | 40 + src/test/format/extension.format.test.ts | 58 +- .../format/extension.onTypeFormat.test.ts | 3 +- src/test/format/extension.sort.test.ts | 191 ++--- src/test/index.ts | 31 +- src/test/initialize.ts | 88 +- .../interpreters/condaEnvFileService.test.ts | 3 +- src/test/interpreters/condaEnvService.test.ts | 52 +- .../interpreters/display.multiroot.test.ts | 52 ++ src/test/interpreters/display.test.ts | 77 +- src/test/interpreters/mocks.ts | 19 +- .../pythonPathUpdater.multiroot.test.ts | 75 ++ .../interpreters/pythonPathUpdater.test.ts | 92 ++ .../windowsRegistryService.test.ts | 15 +- src/test/jupyter/jupyter.codeHelper.test.ts | 13 +- src/test/jupyter/jupyterClient.test.ts | 23 +- src/test/jupyter/jupyterKernel.test.ts | 15 +- src/test/jupyter/jupyterKernelManager.test.ts | 17 +- src/test/linters/base.test.ts | 68 -- src/test/linters/lint.multiroot.test.ts | 84 ++ src/test/linters/lint.test.ts | 352 ++++---- src/test/multiRootTest.ts | 9 + .../disableLinters/.vscode/tags | 19 + .../multiRootWkspc/disableLinters/file.py | 87 ++ src/test/multiRootWkspc/multi.code-workspace | 29 + .../parent/child/.vscode/settings.json | 3 + .../multiRootWkspc/parent/child/.vscode/tags | 24 + .../multiRootWkspc/parent/child/childFile.py | 13 + src/test/multiRootWkspc/parent/child/file.py | 87 ++ .../workspace1/.vscode/settings.json | 5 + .../multiRootWkspc/workspace1/.vscode/tags | 19 + src/test/multiRootWkspc/workspace1/file.py | 87 ++ .../workspace2/.vscode/settings.json | 4 + src/test/multiRootWkspc/workspace2/file.py | 87 ++ .../workspace2/workspace2.tags.file | 24 + .../workspace2/workspace2File.py | 13 + .../workspace3/.vscode/settings.json | 3 + src/test/multiRootWkspc/workspace3/file.py | 87 ++ .../workspace3/workspace3.tags.file | 19 + .../shebangCodeLenseProvider.test.ts | 94 ++- src/test/pythonFiles/linting/file.py | 44 +- src/test/pythonFiles/shebang/shebang.py | 2 +- src/test/pythonFiles/shebang/shebangEnv.py | 3 + .../pythonFiles/shebang/shebangInvalid.py | 3 + src/test/pythonFiles/symbolFiles/childFile.py | 13 + src/test/pythonFiles/symbolFiles/file.py | 87 ++ .../pythonFiles/symbolFiles/workspace2File.py | 13 + .../debuggerTest/tests/test_debugger_one.py | 8 + .../debuggerTest/tests/test_debugger_two.py | 8 + .../debuggerTest/tests/test_debugger_two.txt | 8 + .../tests/test_debugger_two.updated.txt | 14 + .../noseFiles/specific/tst_unittest_one.py | 15 + .../noseFiles/specific/tst_unittest_two.py | 18 + .../testFiles/noseFiles/test_root.py | 19 + .../testFiles/noseFiles/tests/test4.py | 13 + .../noseFiles/tests/test_unittest_one.py | 19 + .../noseFiles/tests/test_unittest_two.py | 32 + .../noseFiles/tests/unittest_three_test.py | 13 + .../standard/.cache/v/cache/lastfailed | 3 - .../extension.refactor.extract.method.test.ts | 24 +- .../extension.refactor.extract.var.test.ts | 26 +- src/test/standardTest.ts | 8 + src/test/unittests/debugger.test.ts | 197 +++++ src/test/unittests/mocks.ts | 40 + src/test/unittests/nosetest.test.ts | 184 ++-- src/test/unittests/pytest.test.ts | 106 ++- src/test/unittests/rediscover.test.ts | 108 +++ src/test/unittests/unittest.test.ts | 141 ++-- src/test/workspaceSymbols/common.ts | 8 + src/test/workspaceSymbols/multiroot.test.ts | 65 ++ src/test/workspaceSymbols/standard.test.ts | 74 ++ .../disableLinters/.vscode/tags | 19 + src/testMultiRootWkspc/disableLinters/file.py | 87 ++ src/testMultiRootWkspc/multi.code-workspace | 42 + .../parent/child/.vscode/settings.json | 3 + .../parent/child/.vscode/tags | 24 + .../parent/child/childFile.py | 13 + src/testMultiRootWkspc/parent/child/file.py | 87 ++ .../workspace1/.vscode/settings.json | 5 + .../workspace1/.vscode/tags | 19 + src/testMultiRootWkspc/workspace1/file.py | 87 ++ .../workspace2/.vscode/settings.json | 4 + src/testMultiRootWkspc/workspace2/file.py | 87 ++ .../workspace2/workspace2.tags.file | 24 + .../workspace2/workspace2File.py | 13 + .../workspace3/.vscode/settings.json | 3 + src/testMultiRootWkspc/workspace3/file.py | 87 ++ .../workspace3/workspace3.tags.file | 19 + tsconfig.json | 4 +- tsfmt.json | 17 + tslint.json | 43 +- typings.json | 6 - typings/globals/xml2js/index.d.ts | 96 --- typings/globals/xml2js/typings.json | 8 - typings/index.d.ts | 1 - typings/node.d.ts | 1 - typings/vscode-typings.d.ts | 1 - 223 files changed, 8577 insertions(+), 4058 deletions(-) create mode 100644 .editorconfig create mode 100644 .eslintrc create mode 100644 gulpfile.js create mode 100644 src/client/common/contextKey.ts create mode 100644 src/client/common/interpreterInfoCache.ts create mode 100644 src/client/debugger/configProviders/simpleProvider.ts create mode 100644 src/client/debugger/index.ts create mode 100644 src/client/interpreter/configuration/pythonPathUpdaterService.ts create mode 100644 src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts create mode 100644 src/client/interpreter/configuration/services/globalUpdaterService.ts create mode 100644 src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts create mode 100644 src/client/interpreter/configuration/services/workspaceUpdaterService.ts create mode 100644 src/client/interpreter/configuration/setInterpreterProvider.ts create mode 100644 src/client/interpreter/configuration/types.ts create mode 100644 src/client/interpreter/display/shebangCodeLensProvider.ts create mode 100644 src/client/languageServices/jediProxyFactory.ts create mode 100644 src/client/providers/replProvider.ts delete mode 100644 src/client/providers/setInterpreterProvider.ts delete mode 100644 src/client/providers/shebangCodeLensProvider.ts delete mode 100644 src/client/providers/telemetry.ts delete mode 100644 src/client/singleFileDebug.ts create mode 100644 src/client/unittests/common/configSettingService.ts create mode 100644 src/client/unittests/common/constants.ts delete mode 100644 src/client/unittests/common/contracts.ts create mode 100644 src/client/unittests/common/debugLauncher.ts create mode 100644 src/client/unittests/common/storageService.ts create mode 100644 src/client/unittests/common/testManagerService.ts create mode 100644 src/client/unittests/common/testManagerServiceFactory.ts create mode 100644 src/client/unittests/common/testResultsService.ts create mode 100644 src/client/unittests/common/testVisitors/flatteningVisitor.ts create mode 100644 src/client/unittests/common/testVisitors/folderGenerationVisitor.ts create mode 100644 src/client/unittests/common/testVisitors/resultResetVisitor.ts create mode 100644 src/client/unittests/common/types.ts create mode 100644 src/client/unittests/common/workspaceTestManagerService.ts create mode 100644 src/test/.vscode/settings.json create mode 100644 src/test/.vscode/tags create mode 100644 src/test/common.ts create mode 100644 src/test/common/configSettings.multiroot.test.ts create mode 100644 src/test/definitions/parallel.test.ts create mode 100644 src/test/interpreters/display.multiroot.test.ts create mode 100644 src/test/interpreters/pythonPathUpdater.multiroot.test.ts create mode 100644 src/test/interpreters/pythonPathUpdater.test.ts delete mode 100644 src/test/linters/base.test.ts create mode 100644 src/test/linters/lint.multiroot.test.ts create mode 100644 src/test/multiRootTest.ts create mode 100644 src/test/multiRootWkspc/disableLinters/.vscode/tags create mode 100644 src/test/multiRootWkspc/disableLinters/file.py create mode 100644 src/test/multiRootWkspc/multi.code-workspace create mode 100644 src/test/multiRootWkspc/parent/child/.vscode/settings.json create mode 100644 src/test/multiRootWkspc/parent/child/.vscode/tags create mode 100644 src/test/multiRootWkspc/parent/child/childFile.py create mode 100644 src/test/multiRootWkspc/parent/child/file.py create mode 100644 src/test/multiRootWkspc/workspace1/.vscode/settings.json create mode 100644 src/test/multiRootWkspc/workspace1/.vscode/tags create mode 100644 src/test/multiRootWkspc/workspace1/file.py create mode 100644 src/test/multiRootWkspc/workspace2/.vscode/settings.json create mode 100644 src/test/multiRootWkspc/workspace2/file.py create mode 100644 src/test/multiRootWkspc/workspace2/workspace2.tags.file create mode 100644 src/test/multiRootWkspc/workspace2/workspace2File.py create mode 100644 src/test/multiRootWkspc/workspace3/.vscode/settings.json create mode 100644 src/test/multiRootWkspc/workspace3/file.py create mode 100644 src/test/multiRootWkspc/workspace3/workspace3.tags.file create mode 100644 src/test/pythonFiles/shebang/shebangEnv.py create mode 100644 src/test/pythonFiles/shebang/shebangInvalid.py create mode 100644 src/test/pythonFiles/symbolFiles/childFile.py create mode 100644 src/test/pythonFiles/symbolFiles/file.py create mode 100644 src/test/pythonFiles/symbolFiles/workspace2File.py create mode 100644 src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_one.py create mode 100644 src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.py create mode 100644 src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.txt create mode 100644 src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.updated.txt create mode 100644 src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py create mode 100644 src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py create mode 100644 src/test/pythonFiles/testFiles/noseFiles/test_root.py create mode 100644 src/test/pythonFiles/testFiles/noseFiles/tests/test4.py create mode 100644 src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py create mode 100644 src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py create mode 100644 src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py delete mode 100644 src/test/pythonFiles/testFiles/standard/.cache/v/cache/lastfailed create mode 100644 src/test/standardTest.ts create mode 100644 src/test/unittests/debugger.test.ts create mode 100644 src/test/unittests/mocks.ts create mode 100644 src/test/unittests/rediscover.test.ts create mode 100644 src/test/workspaceSymbols/common.ts create mode 100644 src/test/workspaceSymbols/multiroot.test.ts create mode 100644 src/test/workspaceSymbols/standard.test.ts create mode 100644 src/testMultiRootWkspc/disableLinters/.vscode/tags create mode 100644 src/testMultiRootWkspc/disableLinters/file.py create mode 100644 src/testMultiRootWkspc/multi.code-workspace create mode 100644 src/testMultiRootWkspc/parent/child/.vscode/settings.json create mode 100644 src/testMultiRootWkspc/parent/child/.vscode/tags create mode 100644 src/testMultiRootWkspc/parent/child/childFile.py create mode 100644 src/testMultiRootWkspc/parent/child/file.py create mode 100644 src/testMultiRootWkspc/workspace1/.vscode/settings.json create mode 100644 src/testMultiRootWkspc/workspace1/.vscode/tags create mode 100644 src/testMultiRootWkspc/workspace1/file.py create mode 100644 src/testMultiRootWkspc/workspace2/.vscode/settings.json create mode 100644 src/testMultiRootWkspc/workspace2/file.py create mode 100644 src/testMultiRootWkspc/workspace2/workspace2.tags.file create mode 100644 src/testMultiRootWkspc/workspace2/workspace2File.py create mode 100644 src/testMultiRootWkspc/workspace3/.vscode/settings.json create mode 100644 src/testMultiRootWkspc/workspace3/file.py create mode 100644 src/testMultiRootWkspc/workspace3/workspace3.tags.file create mode 100644 tsfmt.json delete mode 100644 typings.json delete mode 100644 typings/globals/xml2js/index.d.ts delete mode 100644 typings/globals/xml2js/typings.json delete mode 100644 typings/index.d.ts delete mode 100644 typings/node.d.ts delete mode 100644 typings/vscode-typings.d.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000000..4f92a467d0ef --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Tab indentation +[*] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true + +# The indent size used in the `package.json` file cannot be changed +# https://github.com/npm/npm/pull/3180#issuecomment-16336516 +[{.travis.yml,npm-shrinkwrap.json,package.json}] +indent_style = space +indent_size = 4 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000000..7c2542a3c660 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,15 @@ + +{ + "env": { + "node": true, + "es6": true + }, + "rules": { + "no-console": 0, + "no-cond-assign": 0, + "no-unused-vars": 1, + "no-extra-semi": "warn", + "semi": "warn" + }, + "extends": "eslint:recommended" +} diff --git a/.gitignore b/.gitignore index 58ce4158ddbe..a5334358937a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,10 @@ out node_modules *.pyc -.vscode/.ropeproject/** -src/test/.vscode/** +**/.vscode/.ropeproject/** **/testFiles/**/.cache/** *.noseids .vscode-test __pycache__ +npm-debug.log +**/.mypy_cache/** diff --git a/.travis.yml b/.travis.yml index 45c73062ebf5..4631c982bbdb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,6 +48,7 @@ before_install: | pyenv install $PYTHON pyenv global $PYTHON fi + export TRAVIS_PYTHON_PATH=`which python` install: - pip install --upgrade -r requirements.txt - npm install diff --git a/.vscode/launch.json b/.vscode/launch.json index 284cb90bbaa5..3cc9f6c671c6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,25 +1,7 @@ // A launch configuration that compiles the extension and then opens it inside a new window { "version": "0.1.0", - "configurations": [{ - "name": "CompletionServer.ppy", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "pythonPath": "python", - "program": "${workspaceRoot}/pythonFiles/completionServer.py", - "cwd": "${workspaceRoot}", - "env": {}, - "args": [ - "123" - ], - "envFile": "${workspaceRoot}/.env", - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput" - ] - }, + "configurations": [ { "name": "Launch Extension", "type": "extensionHost", @@ -33,7 +15,7 @@ "outFiles": [ "${workspaceRoot}/out/**/*.js" ], - "preLaunchTask": "npm" + "preLaunchTask": "compile" }, { "name": "Launch Extension as debugServer", // https://code.visualstudio.com/docs/extensions/example-debuggers @@ -65,29 +47,33 @@ "outFiles": [ "${workspaceRoot}/out/**/*.js" ], - "preLaunchTask": "npm" + "preLaunchTask": "compile" }, { - "name": "Python", - "type": "python", + "name": "Launch Multiroot Tests", + "type": "extensionHost", "request": "launch", - "stopOnEntry": true, - "pythonPath": "python", - "program": "${file}", - "console": "integratedTerminal", - "args": [], - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput" + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceRoot}/src/testMultiRootWkspc/multi.code-workspace", + "--extensionDevelopmentPath=${workspaceRoot}", + "--extensionTestsPath=${workspaceRoot}/out/test" ], - "cwd": "${workspaceRoot}" + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [ + "${workspaceRoot}/out/**/*.js" + ], + "preLaunchTask": "compile" } ], - "compounds": [{ - "name": "Extension + Debugger", - "configurations": [ - "Launch Extension", "Launch Extension as debugServer" - ] - }] + "compounds": [ + { + "name": "Extension + Debugger", + "configurations": [ + "Launch Extension", + "Launch Extension as debugServer" + ] + } + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 2dc22387c78a..1884ba9a0255 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,13 @@ // Place your settings in this file to overwrite default and user settings. { "files.exclude": { - "out": false, // set this to true to hide the "out" folder with the compiled JS files + "out": true, // set this to true to hide the "out" folder with the compiled JS files "**/*.pyc": true, "**/__pycache__": true, - "node_modules": true + "node_modules": true, + ".vscode-test": true, + "**/.mypy_cache/**": true, + "**/.ropeproject/**": true }, "search.exclude": { "out": true // set this to false to include "out" folder in search results @@ -15,6 +18,5 @@ "python.formatting.formatOnSave": false, "python.unitTest.promptToConfigure": false, "python.workspaceSymbols.enabled": false, - "python.pythonPath": "/Users/donjayamanne/Projects/PythonEnvs/p27/bin/python", "python.formatting.provider": "yapf" } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3320d8498c8e..bfcb80a350f7 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,24 +5,66 @@ // ${fileDirname}: the current opened file's dirname // ${fileExtname}: the current opened file's extension // ${cwd}: the current working directory of the spawned process - // A task runner that calls a custom npm script that compiles the extension. { - "version": "0.1.0", - - // we want to run npm - "command": "npm", - - // the command is a shell script - "isShellCommand": true, - - // show the output window only if unrecognized errors occur. - "showOutput": "silent", - - // we run the custom script "compile" as defined in package.json - "args": ["run", "compile", "--loglevel", "silent"], - "isBackground": true, - - // use the standard tsc in watch mode problem matcher to find compile problems in the output. - "problemMatcher": "$tsc-watch" -} \ No newline at end of file + "version": "2.0.0", + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared" + }, + "tasks": [ + { + "label": "compile", + "type": "npm", + "script": "compile", + "group": "build", + "isBackground": true, + "problemMatcher": [ + "$tsc", + { + "base": "$tslint5", + "fileLocation": "relative" + } + ] + }, + { + "type": "npm", + "script": "lint", + "problemMatcher": { + "base": "$tslint5", + "fileLocation": "relative" + } + }, + { + "type": "npm", + "script": "watch", + "isBackground": true, + "problemMatcher": "$tsc-watch" + }, + { + "type": "gulp", + "task": "hygiene", + "problemMatcher": [ + "$tsc", + { + "base": "$tslint5", + "fileLocation": "relative" + } + ] + }, + { + "label": "lint-staged", + "type": "npm", + "script": "lint-staged", + "problemMatcher": [ + "$tsc", + { + "base": "$tslint5", + "fileLocation": "relative" + } + ] + } + ] +} diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 000000000000..052c3aa979fd --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,260 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +const gulp = require('gulp'); +const filter = require('gulp-filter'); +const es = require('event-stream'); +const tsfmt = require('typescript-formatter'); +const tslint = require('tslint'); +const relative = require('relative'); +const ts = require('gulp-typescript'); + +/** + * Hygiene works by creating cascading subsets of all our files and + * passing them through a sequence of checks. Here are the current subsets, + * named according to the checks performed on them. Each subset contains + * the following one, as described in mathematical notation: + * + * all ⊃ eol ⊇ indentation ⊃ copyright ⊃ typescript + */ + +const all = [ + 'src/**/*', + 'src/client/**/*', +]; + +const eolFilter = [ + '**', + '!.editorconfig', + '!.eslintrc', + '!.gitignore', + '!.gitmodules', + '!.jshintignore', + '!.jshintrc', + '!.npmrc', + '!.vscodeignore', + '!LICENSE', + '!webpack.config.js', + '!**/node_modules/**', + '!**/*.{svg,exe,png,bmp,scpt,bat,cmd,cur,ttf,woff,eot,txt,md,json,yml}', + '!out/**/*', + '!images/**/*', + '!docs/**/*', + '!.vscode/**/*', + '!pythonFiles/**/*', + '!resources/**/*', + '!snippets/**/*', + '!syntaxes/**/*', + '!**/typings/**/*', +]; + +const indentationFilter = [ + 'src/**/*.ts', + '!**/typings/**/*', +]; + +const tslintFilter = [ + 'src/**/*.ts', + 'test/**/*.ts', + '!webpack.config.js', + '!**/node_modules/**', + '!out/**/*', + '!images/**/*', + '!docs/**/*', + '!.vscode/**/*', + '!pythonFiles/**/*', + '!resources/**/*', + '!snippets/**/*', + '!syntaxes/**/*', + '!**/typings/**/*', +]; + +function reportFailures(failures) { + failures.forEach(failure => { + const name = failure.name || failure.fileName; + const position = failure.startPosition; + const line = position.lineAndCharacter ? position.lineAndCharacter.line : position.line; + const character = position.lineAndCharacter ? position.lineAndCharacter.character : position.character; + + // Output in format similar to tslint for the linter to pickup. + console.error(`ERROR: (${failure.ruleName}) ${relative(__dirname, name)}[${line + 1}, ${character + 1}]: ${failure.failure}`); + }); +} + +const hygiene = exports.hygiene = (some, options) => { + options = options || {}; + let errorCount = 0; + const eol = es.through(function (file) { + if (/\r\n?/g.test(file.contents.toString('utf8'))) { + console.error(file.relative + ': Bad EOL found'); + errorCount++; + } + + this.emit('data', file); + }); + + const indentation = es.through(function (file) { + file.contents + .toString('utf8') + .split(/\r\n|\r|\n/) + .forEach((line, i) => { + if (/^\s*$/.test(line)) { + // Empty or whitespace lines are OK. + } else if (/^(\s\s\s\s)+.*/.test(line)) { + // Good indent. + } else if (/^[\t]+.*/.test(line)) { + console.error(file.relative + '(' + (i + 1) + ',1): Bad whitespace indentation'); + errorCount++; + } + }); + + this.emit('data', file); + }); + + const formatting = es.map(function (file, cb) { + tsfmt.processString(file.path, file.contents.toString('utf8'), { + verify: true, + tsconfig: true, + tslint: true, + editorconfig: true, + tsfmt: true + }).then(result => { + if (result.error) { + console.error(result.message); + errorCount++; + } + cb(null, file); + + }, err => { + cb(err); + }); + }); + + const tsl = es.through(function (file) { + const configuration = tslint.Configuration.findConfiguration(null, '.'); + const options = { + formatter: 'json' + }; + const contents = file.contents.toString('utf8'); + const program = require('tslint').Linter.createProgram("./tsconfig.json"); + const linter = new tslint.Linter(options, program); + linter.lint(file.relative, contents, configuration.results); + const result = linter.getResult(); + if (result.failureCount > 0 || result.errorCount > 0) { + reportFailures(result.failures); + if (result.failureCount) { + errorCount += result.failureCount; + } + if (result.errorCount) { + errorCount += result.errorCount; + } + } + + this.emit('data', file); + }); + + const tsFiles = []; + const tscFilesTracker = es.through(function (file) { + tsFiles.push(file.path.replace(/\\/g, '/')); + tsFiles.push(file.path); + this.emit('data', file); + }); + + const tsc = function () { + function customReporter() { + return { + error: function (error) { + const fullFilename = error.fullFilename || ''; + const relativeFilename = error.relativeFilename || ''; + if (tsFiles.findIndex(file => fullFilename === file || relativeFilename === file) === -1) { + return; + } + errorCount += 1; + console.error(error.message); + }, + finish: function () { + // forget the summary. + } + }; + } + const tsProject = ts.createProject('tsconfig.json', { strict: true }); + const reporter = customReporter(); + return tsProject(reporter); + } + + const result = gulp.src(some || all, { + base: '.' + }) + .pipe(filter(f => !f.stat.isDirectory())) + .pipe(filter(eolFilter)) + .pipe(options.skipEOL ? es.through() : eol) + .pipe(filter(indentationFilter)) + .pipe(indentation); + + const typescript = result + .pipe(filter(tslintFilter)) + .pipe(formatting) + .pipe(tsl) + .pipe(tscFilesTracker) + .pipe(tsc()); + + return typescript + .pipe(es.through(null, function () { + if (errorCount > 0) { + this.emit('error', 'Hygiene failed with ' + errorCount + ' errors. Check \'gulpfile.js\'.'); + } else { + this.emit('end'); + } + })); +}; + +gulp.task('hygiene', () => hygiene()); + +// this allows us to run hygiene as a git pre-commit hook. +if (require.main === module) { + const cp = require('child_process'); + + process.on('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); + process.exit(1); + }); + + cp.exec('git config core.autocrlf', (err, out) => { + const skipEOL = out.trim() === 'true'; + + if (process.argv.length > 2) { + return hygiene(process.argv.slice(2), { + skipEOL: skipEOL + }).on('error', err => { + console.error(); + console.error(err); + process.exit(1); + }); + } + + cp.exec('git diff --cached --name-only', { + maxBuffer: 2000 * 1024 + }, (err, out) => { + if (err) { + console.error(); + console.error(err); + process.exit(1); + } + + const some = out + .split(/\r?\n/) + .filter(l => !!l); + hygiene(some, { + skipEOL: skipEOL + }).on('error', err => { + console.error(); + console.error(err); + process.exit(1); + }); + }); + }); +} diff --git a/package.json b/package.json index e19878709162..63f80480c85c 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.15.0" + "vscode": "^1.17.0" }, "recommendations": [ "donjayamanne.jupyter" @@ -33,7 +33,8 @@ "jupyter", "django", "debugger", - "unittest" + "unittest", + "multi-root ready" ], "categories": [ "Languages", @@ -44,8 +45,8 @@ "Other" ], "activationEvents": [ + "onDebug", "onLanguage:python", - "onCommand:python.python-debug.startSession", "onCommand:python.execInTerminal", "onCommand:python.sortImports", "onCommand:python.runtests", @@ -278,7 +279,6 @@ "languages": [ "python" ], - "startSessionCommand": "python.python-debug.startSession", "enableBreakpointsFor": { "languageIds": [ "python", @@ -1594,7 +1594,10 @@ "vscode:prepublish": "tsc -p ./ && webpack", "compile": "webpack && tsc -watch -p ./", "postinstall": "node ./node_modules/vscode/bin/install", - "test": "node ./node_modules/vscode/bin/test" + "test": "node ./out/test/standardTest.js && node ./out/test/multiRootTest.js", + "precommit": "node gulpfile.js", + "lint-staged": "node gulpfile.js", + "lint": "tslint src/**/*.ts -t verbose" }, "dependencies": { "anser": "^1.1.0", @@ -1624,13 +1627,15 @@ "vscode-languageserver": "^3.1.0", "winreg": "^1.2.4", "xml2js": "^0.4.17", - "vscode": "^1.0.3" + "vscode": "^1.1.5" }, "devDependencies": { + "@types/chai": "^4.0.4", + "@types/chai-as-promised": "^7.1.0", "@types/fs-extra": "^4.0.2", "@types/jquery": "^1.10.31", "@types/lodash": "^4.14.74", - "@types/mocha": "^2.2.32", + "@types/mocha": "^2.2.43", "@types/node": "^6.0.40", "@types/rx": "^2.5.33", "@types/semver": "^5.4.0", @@ -1639,16 +1644,28 @@ "@types/socket.io-client": "^1.4.27", "@types/uuid": "^3.3.27", "@types/winreg": "^1.2.30", + "@types/xml2js": "^0.4.0", "babel-core": "^6.14.0", "babel-loader": "^6.2.5", "babel-preset-es2015": "^6.14.0", + "chai": "^4.1.2", + "chai-as-promised": "^7.1.1", + "event-stream": "^3.3.4", + "gulp": "^3.9.1", + "gulp-filter": "^5.0.1", + "gulp-typescript": "^3.2.2", + "husky": "^0.14.3", "ignore-loader": "^0.1.1", - "mocha": "^3.2.0", + "mocha": "^2.3.3", + "relative": "^3.0.2", "retyped-diff-match-patch-tsd-ambient": "^1.0.0-0", "sinon": "^2.3.6", - "ts-loader": "^0.8.2", - "tslint": "^3.15.1", - "typescript": "^2.3.2", + "ts-loader": "^2.3.4", + "tslint": "^5.7.0", + "tslint-eslint-rules": "^4.1.1", + "tslint-microsoft-contrib": "^5.0.1", + "typescript": "^2.5.2", + "typescript-formatter": "^6.0.0", "webpack": "^1.13.2" } } diff --git a/pythonFiles/PythonTools/ipythonServer.py b/pythonFiles/PythonTools/ipythonServer.py index 99ff5dc7ac6c..0aac8c30bfb4 100644 --- a/pythonFiles/PythonTools/ipythonServer.py +++ b/pythonFiles/PythonTools/ipythonServer.py @@ -55,7 +55,7 @@ from queue import Empty, Queue # Python 3 DEBUG = os.environ.get('DEBUG_DJAYAMANNE_IPYTHON', '0') == '1' -TEST = os.environ.get('PYTHON_DONJAYAMANNE_TEST', '0') == '1' +TEST = os.environ.get('VSC_PYTHON_CI_TEST', '0') == '1' # The great "support IPython 2, 3, 4" strat begins if not TEST: diff --git a/pythonFiles/completionServer.py b/pythonFiles/completionServer.py index f364f1fddce3..2e2934724841 100644 --- a/pythonFiles/completionServer.py +++ b/pythonFiles/completionServer.py @@ -55,7 +55,7 @@ from queue import Empty, Queue # Python 3 DEBUG = os.environ.get('DEBUG_DJAYAMANNE_IPYTHON', '0') == '1' -TEST = os.environ.get('PYTHON_DONJAYAMANNE_TEST', '0') == '1' +TEST = os.environ.get('VSC_PYTHON_CI_TEST', '0') == '1' def _debug_write(out): if DEBUG: diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 32ef9ec060fb..797e644d3b3f 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -1,11 +1,14 @@ 'use strict'; -import * as vscode from 'vscode'; -import * as path from 'path'; import * as child_process from 'child_process'; +import { EventEmitter } from 'events'; +import * as path from 'path'; +import * as vscode from 'vscode'; import { Uri } from 'vscode'; +import { InterpreterInfoCache } from './interpreterInfoCache'; import { SystemVariables } from './systemVariables'; -import { EventEmitter } from 'events'; + +// tslint:disable-next-line:no-require-imports no-var-requires const untildify = require('untildify'); export const IS_WINDOWS = /^win/.test(process.platform); @@ -56,6 +59,7 @@ export interface IPep8CategorySeverity { W: vscode.DiagnosticSeverity; E: vscode.DiagnosticSeverity; } +// tslint:disable-next-line:interface-name export interface Flake8CategorySeverity { F: vscode.DiagnosticSeverity; E: vscode.DiagnosticSeverity; @@ -125,18 +129,39 @@ export interface ITerminalSettings { executeInFileDir: boolean; launchArgs: string[]; } +// tslint:disable-next-line:interface-name export interface JupyterSettings { appendResults: boolean; defaultKernel: string; startupCode: string[]; } -const IS_TEST_EXECUTION = process.env['PYTHON_DONJAYAMANNE_TEST'] === '1'; +// tslint:disable-next-line:no-string-literal +const IS_TEST_EXECUTION = process.env['VSC_PYTHON_CI_TEST'] === '1'; +// tslint:disable-next-line:completed-docs export class PythonSettings extends EventEmitter implements IPythonSettings { private static pythonSettings: Map = new Map(); - private workspaceRoot?: vscode.Uri; + + public jediPath: string; + public envFile: string; + public disablePromptForFeatures: string[]; + public venvPath: string; + public devOptions: string[]; + public linting: ILintingSettings; + public formatting: IFormattingSettings; + public autoComplete: IAutoCompeteSettings; + public unitTest: IUnitTestSettings; + public terminal: ITerminalSettings; + public jupyter: JupyterSettings; + public sortImports: ISortImportSettings; + public workspaceSymbols: IWorkspaceSymbolSettings; + + private workspaceRoot: vscode.Uri; private disposables: vscode.Disposable[] = []; + // tslint:disable-next-line:variable-name + private _pythonPath: string; + constructor(workspaceFolder?: Uri) { super(); this.workspaceRoot = workspaceFolder ? workspaceFolder : vscode.Uri.file(__dirname); @@ -146,9 +171,10 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { this.initializeSettings(); } + // tslint:disable-next-line:function-name public static getInstance(resource?: Uri): PythonSettings { const workspaceFolder = resource ? vscode.workspace.getWorkspaceFolder(resource) : undefined; - let workspaceFolderUri: Uri = workspaceFolder ? workspaceFolder.uri : undefined; + let workspaceFolderUri: Uri | undefined = workspaceFolder ? workspaceFolder.uri : undefined; if (!workspaceFolderUri && Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0) { workspaceFolderUri = vscode.workspace.workspaceFolders[0].uri; } @@ -157,44 +183,69 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { const settings = new PythonSettings(workspaceFolderUri); PythonSettings.pythonSettings.set(workspaceFolderKey, settings); } - return PythonSettings.pythonSettings.get(workspaceFolderKey); + // tslint:disable-next-line:no-non-null-assertion + return PythonSettings.pythonSettings.get(workspaceFolderKey)!; + } + // tslint:disable-next-line:function-name + public static dispose() { + if (!IS_TEST_EXECUTION) { + throw new Error('Dispose can only be called from unit tests'); + } + // tslint:disable-next-line:no-void-expression + PythonSettings.pythonSettings.forEach(item => item.dispose()); + PythonSettings.pythonSettings.clear(); + } + public dispose() { + // tslint:disable-next-line:no-unsafe-any + this.disposables.forEach(disposable => disposable.dispose()); + this.disposables = []; + InterpreterInfoCache.clear(); } + + // tslint:disable-next-line:cyclomatic-complexity max-func-body-length private initializeSettings() { - const systemVariables: SystemVariables = new SystemVariables(); - const workspaceRoot = (IS_TEST_EXECUTION || !this.workspaceRoot) ? __dirname : this.workspaceRoot.fsPath; + InterpreterInfoCache.clear(); + const workspaceRoot = this.workspaceRoot.fsPath; + const systemVariables: SystemVariables = new SystemVariables(this.workspaceRoot ? this.workspaceRoot.fsPath : undefined); const pythonSettings = vscode.workspace.getConfiguration('python', this.workspaceRoot); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion this.pythonPath = systemVariables.resolveAny(pythonSettings.get('pythonPath'))!; - this.pythonPath = getAbsolutePath(this.pythonPath, IS_TEST_EXECUTION ? __dirname : workspaceRoot); + this.pythonPath = getAbsolutePath(this.pythonPath, workspaceRoot); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion this.venvPath = systemVariables.resolveAny(pythonSettings.get('venvPath'))!; + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion this.jediPath = systemVariables.resolveAny(pythonSettings.get('jediPath'))!; if (typeof this.jediPath === 'string' && this.jediPath.length > 0) { - this.jediPath = getAbsolutePath(systemVariables.resolveAny(this.jediPath), IS_TEST_EXECUTION ? __dirname : workspaceRoot); - } - else { + this.jediPath = getAbsolutePath(systemVariables.resolveAny(this.jediPath), workspaceRoot); + } else { this.jediPath = ''; } + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion this.envFile = systemVariables.resolveAny(pythonSettings.get('envFile'))!; + // tslint:disable-next-line:no-any + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion no-any this.devOptions = systemVariables.resolveAny(pythonSettings.get('devOptions'))!; this.devOptions = Array.isArray(this.devOptions) ? this.devOptions : []; - let lintingSettings = systemVariables.resolveAny(pythonSettings.get('linting'))!; + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const lintingSettings = systemVariables.resolveAny(pythonSettings.get('linting'))!; + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion this.disablePromptForFeatures = pythonSettings.get('disablePromptForFeatures')!; this.disablePromptForFeatures = Array.isArray(this.disablePromptForFeatures) ? this.disablePromptForFeatures : []; if (this.linting) { Object.assign(this.linting, lintingSettings); - } - else { + } else { this.linting = lintingSettings; } - let sortImportSettings = systemVariables.resolveAny(pythonSettings.get('sortImports'))!; + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const sortImportSettings = systemVariables.resolveAny(pythonSettings.get('sortImports'))!; if (this.sortImports) { Object.assign(this.sortImports, sortImportSettings); - } - else { + } else { this.sortImports = sortImportSettings; } - // Support for travis + // Support for travis. this.sortImports = this.sortImports ? this.sortImports : { path: '', args: [] }; - // Support for travis + // Support for travis. this.linting = this.linting ? this.linting : { enabled: false, enabledWithoutWorkspace: false, @@ -236,14 +287,14 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { this.linting.pydocstylePath = getAbsolutePath(systemVariables.resolveAny(this.linting.pydocstylePath), workspaceRoot); this.linting.mypyPath = getAbsolutePath(systemVariables.resolveAny(this.linting.mypyPath), workspaceRoot); - let formattingSettings = systemVariables.resolveAny(pythonSettings.get('formatting'))!; + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const formattingSettings = systemVariables.resolveAny(pythonSettings.get('formatting'))!; if (this.formatting) { Object.assign(this.formatting, formattingSettings); - } - else { + } else { this.formatting = formattingSettings; } - // Support for travis + // Support for travis. this.formatting = this.formatting ? this.formatting : { autopep8Args: [], autopep8Path: 'autopep8', outputWindow: 'python', @@ -254,45 +305,46 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { this.formatting.autopep8Path = getAbsolutePath(systemVariables.resolveAny(this.formatting.autopep8Path), workspaceRoot); this.formatting.yapfPath = getAbsolutePath(systemVariables.resolveAny(this.formatting.yapfPath), workspaceRoot); - let autoCompleteSettings = systemVariables.resolveAny(pythonSettings.get('autoComplete'))!; + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const autoCompleteSettings = systemVariables.resolveAny(pythonSettings.get('autoComplete'))!; if (this.autoComplete) { Object.assign(this.autoComplete, autoCompleteSettings); - } - else { + } else { this.autoComplete = autoCompleteSettings; } - // Support for travis + // Support for travis. this.autoComplete = this.autoComplete ? this.autoComplete : { extraPaths: [], addBrackets: false, preloadModules: [] }; - let workspaceSymbolsSettings = systemVariables.resolveAny(pythonSettings.get('workspaceSymbols'))!; + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const workspaceSymbolsSettings = systemVariables.resolveAny(pythonSettings.get('workspaceSymbols'))!; if (this.workspaceSymbols) { Object.assign(this.workspaceSymbols, workspaceSymbolsSettings); - } - else { + } else { this.workspaceSymbols = workspaceSymbolsSettings; } - // Support for travis + // Support for travis. this.workspaceSymbols = this.workspaceSymbols ? this.workspaceSymbols : { ctagsPath: 'ctags', enabled: true, exclusionPatterns: [], rebuildOnFileSave: true, rebuildOnStart: true, - tagFilePath: path.join(workspaceRoot, "tags") + tagFilePath: path.join(workspaceRoot, 'tags') }; this.workspaceSymbols.tagFilePath = getAbsolutePath(systemVariables.resolveAny(this.workspaceSymbols.tagFilePath), workspaceRoot); - let unitTestSettings = systemVariables.resolveAny(pythonSettings.get('unitTest'))!; + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const unitTestSettings = systemVariables.resolveAny(pythonSettings.get('unitTest'))!; if (this.unitTest) { Object.assign(this.unitTest, unitTestSettings); - } - else { + } else { this.unitTest = unitTestSettings; if (IS_TEST_EXECUTION && !this.unitTest) { + // tslint:disable-next-line:prefer-type-cast this.unitTest = { nosetestArgs: [], pyTestArgs: [], unittestArgs: [], promptToConfigure: true, debugPort: 3000, @@ -302,7 +354,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { } } - // Support for travis + // Support for travis. this.unitTest = this.unitTest ? this.unitTest : { promptToConfigure: true, debugPort: 3000, @@ -317,18 +369,19 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { this.unitTest.cwd = getAbsolutePath(systemVariables.resolveAny(this.unitTest.cwd), workspaceRoot); } - // Resolve any variables found in the test arguments + // Resolve any variables found in the test arguments. this.unitTest.nosetestArgs = this.unitTest.nosetestArgs.map(arg => systemVariables.resolveAny(arg)); this.unitTest.pyTestArgs = this.unitTest.pyTestArgs.map(arg => systemVariables.resolveAny(arg)); this.unitTest.unittestArgs = this.unitTest.unittestArgs.map(arg => systemVariables.resolveAny(arg)); - let terminalSettings = systemVariables.resolveAny(pythonSettings.get('terminal'))!; + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const terminalSettings = systemVariables.resolveAny(pythonSettings.get('terminal'))!; if (this.terminal) { Object.assign(this.terminal, terminalSettings); - } - else { + } else { this.terminal = terminalSettings; if (IS_TEST_EXECUTION && !this.terminal) { + // tslint:disable-next-line:prefer-type-cast this.terminal = {} as ITerminalSettings; } } @@ -338,16 +391,18 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { launchArgs: [] }; + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion this.jupyter = pythonSettings.get('jupyter')!; - // Support for travis + // Support for travis. this.jupyter = this.jupyter ? this.jupyter : { appendResults: true, defaultKernel: '', startupCode: [] }; - this.emit('change'); + // If workspace config changes, then we could have a cascading effect of on change events. + // Let's defer the change notification. + setTimeout(() => this.emit('change'), 1); } - private _pythonPath: string; public get pythonPath(): string { return this._pythonPath; } @@ -355,32 +410,19 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { if (this._pythonPath === value) { return; } - // Add support for specifying just the directory where the python executable will be located - // E.g. virtual directory name + // Add support for specifying just the directory where the python executable will be located. + // E.g. virtual directory name. try { this._pythonPath = getPythonExecutable(value); - } - catch (ex) { + } catch (ex) { this._pythonPath = value; } } - public jediPath: string; - public envFile: string; - public disablePromptForFeatures: string[]; - public venvPath: string; - public devOptions: string[]; - public linting: ILintingSettings; - public formatting: IFormattingSettings; - public autoComplete: IAutoCompeteSettings; - public unitTest: IUnitTestSettings; - public terminal: ITerminalSettings; - public jupyter: JupyterSettings; - public sortImports: ISortImportSettings; - public workspaceSymbols: IWorkspaceSymbolSettings; } function getAbsolutePath(pathToCheck: string, rootDir: string): string { - pathToCheck = untildify(pathToCheck); + // tslint:disable-next-line:prefer-type-cast no-unsafe-any + pathToCheck = untildify(pathToCheck) as string; if (IS_TEST_EXECUTION && !pathToCheck) { return rootDir; } if (pathToCheck.indexOf(path.sep) === -1) { return pathToCheck; @@ -389,9 +431,10 @@ function getAbsolutePath(pathToCheck: string, rootDir: string): string { } function getPythonExecutable(pythonPath: string): string { - pythonPath = untildify(pythonPath); + // tslint:disable-next-line:prefer-type-cast no-unsafe-any + pythonPath = untildify(pythonPath) as string; - // If only 'python' + // If only 'python'. if (pythonPath === 'python' || pythonPath.indexOf(path.sep) === -1 || path.basename(pythonPath) === path.dirname(pythonPath)) { @@ -401,21 +444,21 @@ function getPythonExecutable(pythonPath: string): string { if (isValidPythonPath(pythonPath)) { return pythonPath; } - // Keep python right on top, for backwards compatibility + // Keep python right on top, for backwards compatibility. + // tslint:disable-next-line:variable-name const KnownPythonExecutables = ['python', 'python4', 'python3.6', 'python3.5', 'python3', 'python2.7', 'python2']; for (let executableName of KnownPythonExecutables) { - // Suffix with 'python' for linux and 'osx', and 'python.exe' for 'windows' + // Suffix with 'python' for linux and 'osx', and 'python.exe' for 'windows'. if (IS_WINDOWS) { - executableName = executableName + '.exe'; + executableName = `${executableName}.exe`; if (isValidPythonPath(path.join(pythonPath, executableName))) { return path.join(pythonPath, executableName); } if (isValidPythonPath(path.join(pythonPath, 'scripts', executableName))) { return path.join(pythonPath, 'scripts', executableName); } - } - else { + } else { if (isValidPythonPath(path.join(pythonPath, executableName))) { return path.join(pythonPath, executableName); } @@ -430,10 +473,9 @@ function getPythonExecutable(pythonPath: string): string { function isValidPythonPath(pythonPath: string): boolean { try { - let output = child_process.execFileSync(pythonPath, ['-c', 'print(1234)'], { encoding: 'utf8' }); + const output = child_process.execFileSync(pythonPath, ['-c', 'print(1234)'], { encoding: 'utf8' }); return output.startsWith('1234'); - } - catch (ex) { + } catch (ex) { return false; } } diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index 91f6e5af31b4..5ab6c470e79d 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -96,4 +96,4 @@ export namespace Documentation { export const Home = '/docs/workspaceSymbols/'; export const InstallOnWindows = '/docs/workspaceSymbols/#Install-Windows'; } -} \ No newline at end of file +} diff --git a/src/client/common/contextKey.ts b/src/client/common/contextKey.ts new file mode 100644 index 000000000000..87fe57f07d34 --- /dev/null +++ b/src/client/common/contextKey.ts @@ -0,0 +1,15 @@ +import { commands } from 'vscode'; + +export class ContextKey { + private lastValue: boolean; + + constructor(private name: string) { } + + public async set(value: boolean): Promise { + if (this.lastValue === value) { + return; + } + this.lastValue = value; + await commands.executeCommand('setContext', this.name, this.lastValue); + } +} diff --git a/src/client/common/editor.ts b/src/client/common/editor.ts index 9d23d38ff678..7da7a2657e8b 100644 --- a/src/client/common/editor.ts +++ b/src/client/common/editor.ts @@ -75,7 +75,7 @@ export function getTextEditsFromPatch(before: string, patch: string): TextEdit[] return textEdits; } -export function getWorkspaceEditsFromPatch(filePatches: string[]): WorkspaceEdit { +export function getWorkspaceEditsFromPatch(filePatches: string[], workspaceRoot?:string): WorkspaceEdit { const workspaceEdit = new WorkspaceEdit(); filePatches.forEach(patch => { const indexOfAtAt = patch.indexOf('@@'); @@ -101,7 +101,7 @@ export function getWorkspaceEditsFromPatch(filePatches: string[]): WorkspaceEdit } let fileName = fileNameLines[0].substring(fileNameLines[0].indexOf(' a') + 3).trim(); - fileName = path.isAbsolute(fileName) ? fileName : path.resolve(vscode.workspace.rootPath, fileName); + fileName = workspaceRoot && !path.isAbsolute(fileName) ? path.resolve(workspaceRoot, fileName) : fileName; if (!fs.existsSync(fileName)) { return; } diff --git a/src/client/common/installer.ts b/src/client/common/installer.ts index 1b3991047944..c78d6bf410be 100644 --- a/src/client/common/installer.ts +++ b/src/client/common/installer.ts @@ -1,27 +1,29 @@ +import * as os from 'os'; import * as vscode from 'vscode'; +import { commands, ConfigurationTarget, Disposable, OutputChannel, Terminal, Uri, window, workspace } from 'vscode'; import * as settings from './configSettings'; -import * as os from 'os'; import { isNotInstalledError } from './helpers'; +import { error } from './logger'; import { execPythonFile, getFullyQualifiedPythonInterpreterPath } from './utils'; -import { Documentation } from './constants'; export enum Product { - pytest, - nosetest, - pylint, - flake8, - pep8, - pylama, - prospector, - pydocstyle, - yapf, - autopep8, - mypy, - unittest, - ctags, - rope + pytest = 1, + nosetest = 2, + pylint = 3, + flake8 = 4, + pep8 = 5, + pylama = 6, + prospector = 7, + pydocstyle = 8, + yapf = 9, + autopep8 = 10, + mypy = 11, + unittest = 12, + ctags = 13, + rope = 14 } +// tslint:disable-next-line:variable-name const ProductInstallScripts = new Map(); ProductInstallScripts.set(Product.autopep8, ['-m', 'pip', 'install', 'autopep8']); ProductInstallScripts.set(Product.flake8, ['-m', 'pip', 'install', 'flake8']); @@ -36,6 +38,7 @@ ProductInstallScripts.set(Product.pytest, ['-m', 'pip', 'install', '-U', 'pytest ProductInstallScripts.set(Product.yapf, ['-m', 'pip', 'install', 'yapf']); ProductInstallScripts.set(Product.rope, ['-m', 'pip', 'install', 'rope']); +// tslint:disable-next-line:variable-name const ProductUninstallScripts = new Map(); ProductUninstallScripts.set(Product.autopep8, ['-m', 'pip', 'uninstall', 'autopep8', '--yes']); ProductUninstallScripts.set(Product.flake8, ['-m', 'pip', 'uninstall', 'flake8', '--yes']); @@ -50,6 +53,7 @@ ProductUninstallScripts.set(Product.pytest, ['-m', 'pip', 'uninstall', 'pytest', ProductUninstallScripts.set(Product.yapf, ['-m', 'pip', 'uninstall', 'yapf', '--yes']); ProductUninstallScripts.set(Product.rope, ['-m', 'pip', 'uninstall', 'rope', '--yes']); +// tslint:disable-next-line:variable-name export const ProductExecutableAndArgs = new Map(); ProductExecutableAndArgs.set(Product.mypy, { executable: 'python', args: ['-m', 'mypy'] }); ProductExecutableAndArgs.set(Product.nosetest, { executable: 'python', args: ['-m', 'nose'] }); @@ -76,6 +80,7 @@ switch (os.platform()) { } } +// tslint:disable-next-line:variable-name export const Linters: Product[] = [ Product.flake8, Product.pep8, @@ -86,6 +91,7 @@ export const Linters: Product[] = [ Product.pydocstyle ]; +// tslint:disable-next-line:variable-name const ProductNames = new Map(); ProductNames.set(Product.autopep8, 'autopep8'); ProductNames.set(Product.flake8, 'flake8'); @@ -100,6 +106,7 @@ ProductNames.set(Product.pytest, 'py.test'); ProductNames.set(Product.yapf, 'yapf'); ProductNames.set(Product.rope, 'rope'); +// tslint:disable-next-line:variable-name export const SettingToDisableProduct = new Map(); SettingToDisableProduct.set(Product.flake8, 'linting.flake8Enabled'); SettingToDisableProduct.set(Product.mypy, 'linting.mypyEnabled'); @@ -111,6 +118,10 @@ SettingToDisableProduct.set(Product.pydocstyle, 'linting.pydocstyleEnabled'); SettingToDisableProduct.set(Product.pylint, 'linting.pylintEnabled'); SettingToDisableProduct.set(Product.pytest, 'unitTest.pyTestEnabled'); +// tslint:disable-next-line:variable-name +const ProductInstallationPrompt = new Map(); +ProductInstallationPrompt.set(Product.ctags, 'Install CTags to enable Python workspace symbols'); + enum ProductType { Linter, Formatter, @@ -119,6 +130,7 @@ enum ProductType { WorkspaceSymbols } +// tslint:disable-next-line:variable-name const ProductTypeNames = new Map(); ProductTypeNames.set(ProductType.Formatter, 'Formatter'); ProductTypeNames.set(ProductType.Linter, 'Linter'); @@ -126,6 +138,7 @@ ProductTypeNames.set(ProductType.RefactoringLibrary, 'Refactoring library'); ProductTypeNames.set(ProductType.TestFramework, 'Test Framework'); ProductTypeNames.set(ProductType.WorkspaceSymbols, 'Workspace Symbols'); +// tslint:disable-next-line:variable-name const ProductTypes = new Map(); ProductTypes.set(Product.flake8, ProductType.Linter); ProductTypes.set(Product.mypy, ProductType.Linter); @@ -142,6 +155,11 @@ ProductTypes.set(Product.autopep8, ProductType.Formatter); ProductTypes.set(Product.yapf, ProductType.Formatter); ProductTypes.set(Product.rope, ProductType.RefactoringLibrary); +export enum InstallerResponse { + Installed, + Disabled, + Ignore +} export class Installer implements vscode.Disposable { private static terminal: vscode.Terminal | undefined | null; private disposables: vscode.Disposable[] = []; @@ -155,30 +173,37 @@ export class Installer implements vscode.Disposable { public dispose() { this.disposables.forEach(d => d.dispose()); } - public shouldDisplayPrompt(product: Product) { + private shouldDisplayPrompt(product: Product) { + // tslint:disable-next-line:no-non-null-assertion const productName = ProductNames.get(product)!; - return settings.PythonSettings.getInstance().disablePromptForFeatures.indexOf(productName) === -1; + const pythonConfig = workspace.getConfiguration('python'); + // tslint:disable-next-line:prefer-type-cast + const disablePromptForFeatures = pythonConfig.get('disablePromptForFeatures', [] as string[]); + return disablePromptForFeatures.indexOf(productName) === -1; } - async promptToInstall(product: Product) { + // tslint:disable-next-line:member-ordering + public async promptToInstall(product: Product, resource?: Uri): Promise { + // tslint:disable-next-line:no-non-null-assertion const productType = ProductTypes.get(product)!; - const productTypeName = ProductTypeNames.get(productType); + // tslint:disable-next-line:no-non-null-assertion + const productTypeName = ProductTypeNames.get(productType)!; + // tslint:disable-next-line:no-non-null-assertion const productName = ProductNames.get(product)!; if (!this.shouldDisplayPrompt(product)) { const message = `${productTypeName} '${productName}' not installed.`; if (this.outputChannel) { this.outputChannel.appendLine(message); - } - else { + } else { console.warn(message); } - return; + return InstallerResponse.Ignore; } - const installOption = 'Install ' + productName; - const disableOption = 'Disable ' + productTypeName; - const dontShowAgain = `Don't show this prompt again`; + const installOption = ProductInstallationPrompt.has(product) ? ProductInstallationPrompt.get(product) : `Install ${productName}`; + const disableOption = `Disable ${productTypeName}`; + const dontShowAgain = 'Don\'t show this prompt again'; const alternateFormatter = product === Product.autopep8 ? 'yapf' : 'autopep8'; const useOtherFormatter = `Use '${alternateFormatter}' formatter`; const options = []; @@ -189,122 +214,165 @@ export class Installer implements vscode.Disposable { if (SettingToDisableProduct.has(product)) { options.push(...[disableOption, dontShowAgain]); } - return vscode.window.showErrorMessage(`${productTypeName} ${productName} is not installed`, ...options).then(item => { - switch (item) { - case installOption: { - return this.install(product); - } - case disableOption: { - if (Linters.indexOf(product) >= 0) { - return disableLinter(product); - } - else { - const pythonConfig = vscode.workspace.getConfiguration('python'); - const settingToDisable = SettingToDisableProduct.get(product)!; - return pythonConfig.update(settingToDisable, false); - } - } - case useOtherFormatter: { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('formatting.provider', alternateFormatter); - } - case dontShowAgain: { - const pythonConfig = vscode.workspace.getConfiguration('python'); - const features = pythonConfig.get('disablePromptForFeatures', [] as string[]); - features.push(productName); - return pythonConfig.update('disablePromptForFeatures', features, true); - } - case 'Help': { - return Promise.resolve(); + const item = await window.showErrorMessage(`${productTypeName} ${productName} is not installed`, ...options); + switch (item) { + case installOption: { + return this.install(product, resource); + } + case disableOption: { + if (Linters.indexOf(product) >= 0) { + return this.disableLinter(product, resource).then(() => InstallerResponse.Disabled); + } else { + // tslint:disable-next-line:no-non-null-assertion + const settingToDisable = SettingToDisableProduct.get(product)!; + return this.updateSetting(settingToDisable, false, resource).then(() => InstallerResponse.Disabled); } } - }); + case useOtherFormatter: { + return this.updateSetting('formatting.provider', alternateFormatter, resource) + .then(() => InstallerResponse.Installed); + } + case dontShowAgain: { + const pythonConfig = workspace.getConfiguration('python'); + // tslint:disable-next-line:prefer-type-cast + const features = pythonConfig.get('disablePromptForFeatures', [] as string[]); + features.push(productName); + return pythonConfig.update('disablePromptForFeatures', features, true).then(() => InstallerResponse.Ignore); + } + default: { + throw new Error('Invalid selection'); + } + } } - - install(product: Product): Promise { + // tslint:disable-next-line:member-ordering + public async install(product: Product, resource?: Uri): Promise { if (!this.outputChannel && !Installer.terminal) { - Installer.terminal = vscode.window.createTerminal('Python Installer'); + Installer.terminal = window.createTerminal('Python Installer'); } - if (product === Product.ctags && os.platform() === 'win32') { - vscode.commands.executeCommand('python.displayHelp', Documentation.Workspace.InstallOnWindows); - return Promise.resolve(); + if (product === Product.ctags && settings.IS_WINDOWS) { + if (this.outputChannel) { + this.outputChannel.appendLine('Install Universal Ctags Win32 to enable support for Workspace Symbols'); + this.outputChannel.appendLine('Download the CTags binary from the Universal CTags site.'); + this.outputChannel.appendLine('Option 1: Extract ctags.exe from the downloaded zip to any folder within your PATH so that Visual Studio Code can run it.'); + this.outputChannel.appendLine('Option 2: Extract to any folder and add the path to this folder to the command setting.'); + this.outputChannel.appendLine('Option 3: Extract to any folder and define that path in the python.workspaceSymbols.ctagsPath setting of your user settings file (settings.json).'); + this.outputChannel.show(); + } else { + window.showInformationMessage('Install Universal Ctags and set it in your path or define the path in your python.workspaceSymbols.ctagsPath settings'); + } + return InstallerResponse.Ignore; } + // tslint:disable-next-line:no-non-null-assertion let installArgs = ProductInstallScripts.get(product)!; - let pipIndex = installArgs.indexOf('pip'); + const pipIndex = installArgs.indexOf('pip'); if (pipIndex > 0) { installArgs = installArgs.slice(); - let proxy = vscode.workspace.getConfiguration('http').get('proxy', ''); + const proxy = vscode.workspace.getConfiguration('http').get('proxy', ''); if (proxy.length > 0) { installArgs.splice(2, 0, proxy); installArgs.splice(2, 0, '--proxy'); } } + // tslint:disable-next-line:no-any + let installationPromise: Promise; if (this.outputChannel && installArgs[0] === '-m') { // Errors are just displayed to the user this.outputChannel.show(); - return execPythonFile(settings.PythonSettings.getInstance().pythonPath, installArgs, vscode.workspace.rootPath!, true, (data) => { - this.outputChannel!.append(data); - }); - } - else { + installationPromise = execPythonFile(resource, settings.PythonSettings.getInstance(resource).pythonPath, + // tslint:disable-next-line:no-non-null-assertion + installArgs, getCwdForInstallScript(resource), true, (data) => { this.outputChannel!.append(data); }); + } else { // When using terminal get the fully qualitified path // Cuz people may launch vs code from terminal when they have activated the appropriate virtual env // Problem is terminal doesn't use the currently activated virtual env // Must have something to do with the process being launched in the terminal - return getFullyQualifiedPythonInterpreterPath() + installationPromise = getFullyQualifiedPythonInterpreterPath(resource) .then(pythonPath => { let installScript = installArgs.join(' '); if (installArgs[0] === '-m') { if (pythonPath.indexOf(' ') >= 0) { installScript = `"${pythonPath}" ${installScript}`; - } - else { + } else { installScript = `${pythonPath} ${installScript}`; } } + // tslint:disable-next-line:no-non-null-assertion Installer.terminal!.sendText(installScript); + // tslint:disable-next-line:no-non-null-assertion Installer.terminal!.show(false); }); } + + return installationPromise + .then(() => this.isInstalled(product)) + .then(isInstalled => isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore); } - isInstalled(product: Product): Promise { - return isProductInstalled(product); + // tslint:disable-next-line:member-ordering + public isInstalled(product: Product, resource?: Uri): Promise { + return isProductInstalled(product, resource); } - uninstall(product: Product): Promise { - return uninstallproduct(product); + // tslint:disable-next-line:member-ordering no-any + public uninstall(product: Product, resource?: Uri): Promise { + return uninstallproduct(product, resource); + } + // tslint:disable-next-line:member-ordering + public disableLinter(product: Product, resource: Uri) { + if (resource && !workspace.getWorkspaceFolder(resource)) { + // tslint:disable-next-line:no-non-null-assertion + const settingToDisable = SettingToDisableProduct.get(product)!; + const pythonConfig = workspace.getConfiguration('python', resource); + return pythonConfig.update(settingToDisable, false, ConfigurationTarget.Workspace); + } else { + const pythonConfig = workspace.getConfiguration('python'); + return pythonConfig.update('linting.enabledWithoutWorkspace', false, true); + } + } + // tslint:disable-next-line:no-any + private updateSetting(setting: string, value: any, resource?: Uri) { + if (resource && !workspace.getWorkspaceFolder(resource)) { + const pythonConfig = workspace.getConfiguration('python', resource); + return pythonConfig.update(setting, value, ConfigurationTarget.Workspace); + } else { + const pythonConfig = workspace.getConfiguration('python'); + return pythonConfig.update(setting, value, true); + } } } -export function disableLinter(product: Product, global?: boolean) { - const pythonConfig = vscode.workspace.getConfiguration('python'); - const settingToDisable = SettingToDisableProduct.get(product)!; - if (vscode.workspace.rootPath) { - return pythonConfig.update(settingToDisable, false, global); +function getCwdForInstallScript(resource?: Uri) { + const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; + if (workspaceFolder) { + return workspaceFolder.uri.fsPath; } - else { - return pythonConfig.update('linting.enabledWithoutWorkspace', false, true); + if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + return workspace.workspaceFolders[0].uri.fsPath; } + return __dirname; } -async function isProductInstalled(product: Product): Promise { +async function isProductInstalled(product: Product, resource?: Uri): Promise { if (!ProductExecutableAndArgs.has(product)) { return; } + // tslint:disable-next-line:no-non-null-assertion const prodExec = ProductExecutableAndArgs.get(product)!; - return execPythonFile(prodExec.executable, prodExec.args.concat(['--version']), vscode.workspace.rootPath!, false) - .then(() => { - return true; - }).catch(reason => { - return !isNotInstalledError(reason); - }); + const cwd = getCwdForInstallScript(resource); + return execPythonFile(resource, prodExec.executable, prodExec.args.concat(['--version']), cwd, false) + .then(() => true) + .catch(reason => !isNotInstalledError(reason)); } -function uninstallproduct(product: Product): Promise { +// tslint:disable-next-line:no-any +function uninstallproduct(product: Product, resource?: Uri): Promise { + if (!ProductUninstallScripts.has(product)) { + return Promise.resolve(); + } + // tslint:disable-next-line:no-non-null-assertion const uninstallArgs = ProductUninstallScripts.get(product)!; - return execPythonFile('python', uninstallArgs, vscode.workspace.rootPath!, false); + return execPythonFile(resource, 'python', uninstallArgs, getCwdForInstallScript(resource), false); } diff --git a/src/client/common/interpreterInfoCache.ts b/src/client/common/interpreterInfoCache.ts new file mode 100644 index 000000000000..f23f6ef7563b --- /dev/null +++ b/src/client/common/interpreterInfoCache.ts @@ -0,0 +1,54 @@ +import { Uri, workspace } from 'vscode'; + +type InterpreterCache = { + pythonInterpreterDirectory?: string; + pythonInterpreterPath?: string; + pythonSettingsPath?: string; + // tslint:disable-next-line:no-any + customEnvVariables?: any; +}; + +const cache = new Map(); + +// tslint:disable-next-line:no-stateless-class +export class InterpreterInfoCache { + // tslint:disable-next-line:function-name + public static clear(): void { + cache.clear(); + } + // tslint:disable-next-line:function-name + public static get(resource?: Uri) { + const cacheKey = InterpreterInfoCache.getCacheKey(resource) || ''; + return cache.has(cacheKey) ? cache.get(cacheKey) : {}; + } + // tslint:disable-next-line:function-name + public static setPaths(resource?: Uri, pythonSettingsPath?: string, pythonInterpreterPath?: string, pythonInterpreterDirectory?: string) { + InterpreterInfoCache.setCacheData('pythonInterpreterDirectory', resource, pythonInterpreterDirectory); + InterpreterInfoCache.setCacheData('pythonInterpreterPath', resource, pythonInterpreterPath); + InterpreterInfoCache.setCacheData('pythonSettingsPath', resource, pythonSettingsPath); + } + + // tslint:disable-next-line:no-any function-name + public static setCustomEnvVariables(resource?: Uri, envVars?: any) { + // tslint:disable-next-line:no-any + InterpreterInfoCache.setCacheData('customEnvVariables', resource, envVars); + } + // tslint:disable-next-line:no-any function-name + private static setCacheData(property: keyof InterpreterCache, resource?: Uri, value?: any) { + const cacheKey = InterpreterInfoCache.getCacheKey(resource) || ''; + // tslint:disable-next-line:prefer-type-cast + const data = cache.has(cacheKey) ? cache.get(cacheKey) : {} as InterpreterCache; + data[property] = value; + cache.set(cacheKey, data); + } + private static getCacheKey(resource?: Uri): string { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return ''; + } + if (!resource || workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri.fsPath; + } + const folder = workspace.getWorkspaceFolder(resource); + return folder ? folder.uri.fsPath : ''; + } +} diff --git a/src/client/common/logger.ts b/src/client/common/logger.ts index ac372199e258..076e1a008964 100644 --- a/src/client/common/logger.ts +++ b/src/client/common/logger.ts @@ -22,7 +22,9 @@ class Logger { Logger.writeLine(category, message); } static writeLine(category: string = "log", line: any) { - console[category](line); + if (process.env['VSC_PYTHON_CI_TEST'] !== '1') { + console[category](line); + } if (outChannel) { outChannel.appendLine(line); } diff --git a/src/client/common/systemVariables.ts b/src/client/common/systemVariables.ts index f0df0218c2d9..444d782d6eea 100644 --- a/src/client/common/systemVariables.ts +++ b/src/client/common/systemVariables.ts @@ -133,11 +133,10 @@ export abstract class AbstractSystemVariables implements ISystemVariables { export class SystemVariables extends AbstractSystemVariables { private _workspaceRoot: string; private _workspaceRootFolderName: string; - private _execPath: string; - constructor() { + constructor(workspaceRoot?: string) { super(); - this._workspaceRoot = typeof vscode.workspace.rootPath === 'string' ? vscode.workspace.rootPath : __dirname; + this._workspaceRoot = typeof workspaceRoot === 'string' ? workspaceRoot : __dirname; this._workspaceRootFolderName = Path.basename(this._workspaceRoot); Object.keys(process.env).forEach(key => { this[`env:${key}`] = this[`env.${key}`] = process.env[key]; diff --git a/src/client/common/telemetry.ts b/src/client/common/telemetry.ts index 34cc0e72b73e..64f05c58f6d6 100644 --- a/src/client/common/telemetry.ts +++ b/src/client/common/telemetry.ts @@ -1,4 +1,4 @@ -import { extensions } from "vscode"; +import { extensions, workspace } from "vscode"; import TelemetryReporter from "vscode-extension-telemetry"; // Borrowed from omnisharpServer.ts (omnisharp-vscode) @@ -55,7 +55,7 @@ const extension = extensions.getExtension(extensionId); const extensionVersion = extension.packageJSON.version; const aiKey = "fce7a3d5-4665-4404-b786-31a6306749a6"; let reporter: TelemetryReporter; - +let telemetryEnabled: boolean | undefined = undefined; /** * Sends a telemetry event * @param {string} eventName The event name @@ -67,6 +67,12 @@ export function sendTelemetryEvent(eventName: string, properties?: { }, measures?: { [key: string]: number; }) { + if (telemetryEnabled === undefined) { + telemetryEnabled = workspace.getConfiguration('telemetry').get('enableTelemetry', true); + } + if (!telemetryEnabled) { + return; + } reporter = reporter ? reporter : new TelemetryReporter(extensionId, extensionVersion, aiKey); reporter.sendTelemetryEvent.apply(reporter, arguments); } diff --git a/src/client/common/utils.ts b/src/client/common/utils.ts index bb3c0a2dd4bc..85930a837a8c 100644 --- a/src/client/common/utils.ts +++ b/src/client/common/utils.ts @@ -1,17 +1,16 @@ -/// -/// - 'use strict'; // TODO: Cleanup this place // Add options for execPythonFile -import * as path from 'path'; +import * as child_process from 'child_process'; import * as fs from 'fs'; +import * as fsExtra from 'fs-extra'; import * as os from 'os'; -import * as child_process from 'child_process'; +import * as path from 'path'; +import { CancellationToken, Range, TextDocument, Uri } from 'vscode'; import * as settings from './configSettings'; -import { CancellationToken, TextDocument, Range } from 'vscode'; -import { isNotInstalledError } from './helpers'; import { mergeEnvVariables, parseEnvFile } from './envFileParser'; +import { isNotInstalledError } from './helpers'; +import { InterpreterInfoCache } from './interpreterInfoCache'; export const IS_WINDOWS = /^win/.test(process.platform); export const Is_64Bit = os.arch() === 'x64'; @@ -52,32 +51,29 @@ export function fsReaddirAsync(root: string): Promise { }); } -let pythonInterpretterDirectory: string = null; -let previouslyIdentifiedPythonPath: string = null; -let customEnvVariables: any = null; - -// If config settings change then clear env variables that we have cached -// Remember, the path to the python interpreter can change, hence we need to re-set the paths -settings.PythonSettings.getInstance().on('change', function () { - pythonInterpretterDirectory = null; - previouslyIdentifiedPythonPath = null; - customEnvVariables = null; -}); +async function getPythonInterpreterDirectory(resource?: Uri): Promise { + const cache = InterpreterInfoCache.get(resource); + const pythonFileName = settings.PythonSettings.getInstance(resource).pythonPath; -export function getPythonInterpreterDirectory(): Promise { // If we already have it and the python path hasn't changed, yay - if (pythonInterpretterDirectory && previouslyIdentifiedPythonPath === settings.PythonSettings.getInstance().pythonPath) { - return Promise.resolve(pythonInterpretterDirectory); + if (cache.pythonInterpreterDirectory && cache.pythonInterpreterDirectory.length > 0 + && cache.pythonSettingsPath === pythonFileName) { + return cache.pythonInterpreterDirectory; } - let pythonFileName = settings.PythonSettings.getInstance().pythonPath; // Check if we have the path if (path.basename(pythonFileName) === pythonFileName) { - // No path provided, however we can get it by using sys.executableFile - return getPathFromPythonCommand(["-c", "import sys;print(sys.executable)"]) - .then(pythonExecutablePath => pythonInterpretterDirectory = path.dirname(pythonExecutablePath)) - .catch(() => pythonInterpretterDirectory = ''); + try { + const pythonInterpreterPath = await getPathFromPythonCommand(pythonFileName); + const pythonInterpreterDirectory = path.dirname(pythonInterpreterPath); + InterpreterInfoCache.setPaths(resource, pythonFileName, pythonInterpreterPath, pythonInterpreterDirectory); + return pythonInterpreterDirectory; + // tslint:disable-next-line:variable-name + } catch (_ex) { + InterpreterInfoCache.setPaths(resource, pythonFileName, pythonFileName, ''); + return ''; + } } return new Promise(resolve => { @@ -85,88 +81,78 @@ export function getPythonInterpreterDirectory(): Promise { child_process.execFile(pythonFileName, ['-c', 'print(1234)'], (error, stdout, stderr) => { // Yes this is a valid python path if (stdout.startsWith('1234')) { - previouslyIdentifiedPythonPath = path.dirname(pythonFileName); - } - else { - previouslyIdentifiedPythonPath = ''; + const pythonInterpreterDirectory = path.dirname(pythonFileName); + InterpreterInfoCache.setPaths(resource, pythonFileName, pythonFileName, pythonInterpreterDirectory); + resolve(pythonInterpreterDirectory); + } else { + // No idea, didn't work, hence don't reject, but return empty path + InterpreterInfoCache.setPaths(resource, pythonFileName, pythonFileName, ''); + resolve(''); } - // No idea, didn't work, hence don't reject, but return empty path - resolve(previouslyIdentifiedPythonPath); }); }); } -export function getFullyQualifiedPythonInterpreterPath(): Promise { - return getPythonInterpreterDirectory() - .then(pyPath => path.join(pyPath, path.basename(settings.PythonSettings.getInstance().pythonPath))); +export async function getFullyQualifiedPythonInterpreterPath(resource?: Uri): Promise { + const pyDir = await getPythonInterpreterDirectory(resource); + const cache = InterpreterInfoCache.get(resource); + return cache.pythonInterpreterPath; } -export function getPathFromPythonCommand(args: string[]): Promise { - return execPythonFile(settings.PythonSettings.getInstance().pythonPath, args, __dirname).then(stdout => { - if (stdout.length === 0) { - return ""; - } - let lines = stdout.split(/\r?\n/g).filter(line => line.length > 0); - return validatePath(lines[0]); - }).catch(() => { - return ""; +export async function getPathFromPythonCommand(pythonPath: string): Promise { + return await new Promise((resolve, reject) => { + child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { + if (stdout) { + const lines = stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); + resolve(lines.length > 0 ? lines[0] : ''); + } else { + reject(); + } + }); }); } -export function execPythonFile(file: string, args: string[], cwd: string, includeErrorAsResponse: boolean = false, stdOut: (line: string) => void = null, token?: CancellationToken): Promise { - const execAsModule = file.toUpperCase() === 'PYTHON' && args.length > 0 && args[0] === '-m'; - - // If running the python file, then always revert to execFileInternal - // Cuz python interpreter is always a file and we can and will always run it using child_process.execFile() - if (file === settings.PythonSettings.getInstance().pythonPath) { - if (stdOut) { - return spawnFileInternal(file, args, { cwd }, includeErrorAsResponse, stdOut, token); - } - if (execAsModule) { - return getFullyQualifiedPythonInterpreterPath() - .then(p => execPythonModule(p, args, { cwd: cwd }, includeErrorAsResponse, token)); - } - return execFileInternal(file, args, { cwd: cwd }, includeErrorAsResponse, token); +async function getEnvVariables(resource?: Uri): Promise<{}> { + const cache = InterpreterInfoCache.get(resource); + if (cache.customEnvVariables) { + return cache.customEnvVariables; } - return getPythonInterpreterDirectory().then(pyPath => { - // We don't have a path - if (pyPath.length === 0) { - let options: child_process.ExecFileOptions = { cwd }; - const envVars = customEnvVariables || getCustomEnvVars(); - if (envVars) { - options.env = envVars; - } - if (stdOut) { - return spawnFileInternal(file, args, options, includeErrorAsResponse, stdOut, token); - } - return execFileInternal(file, args, options, includeErrorAsResponse, token); + const pyPath = await getPythonInterpreterDirectory(resource); + let customEnvVariables = await getCustomEnvVars(resource) || {}; + + if (pyPath.length > 0) { + // Ensure to include the path of the current python. + let newPath = ''; + const currentPath = typeof customEnvVariables[PATH_VARIABLE_NAME] === 'string' ? customEnvVariables[PATH_VARIABLE_NAME] : process.env[PATH_VARIABLE_NAME]; + if (IS_WINDOWS) { + newPath = `${pyPath}\\${path.delimiter}${path.join(pyPath, 'Scripts\\')}${path.delimiter}${currentPath}`; + // This needs to be done for windows. + process.env[PATH_VARIABLE_NAME] = newPath; + } else { + newPath = `${pyPath}${path.delimiter}${currentPath}`; } + customEnvVariables = mergeEnvVariables(customEnvVariables, process.env); + customEnvVariables[PATH_VARIABLE_NAME] = newPath; + } - if (customEnvVariables === null) { - customEnvVariables = getCustomEnvVars(); - customEnvVariables = customEnvVariables ? customEnvVariables : {}; - // Ensure to include the path of the current python - let newPath = ''; - let currentPath = typeof customEnvVariables[PATH_VARIABLE_NAME] === 'string' ? customEnvVariables[PATH_VARIABLE_NAME] : process.env[PATH_VARIABLE_NAME]; - if (IS_WINDOWS) { - newPath = pyPath + '\\' + path.delimiter + path.join(pyPath, 'Scripts\\') + path.delimiter + currentPath; - // This needs to be done for windows - process.env[PATH_VARIABLE_NAME] = newPath; - } - else { - newPath = pyPath + path.delimiter + currentPath; - } - customEnvVariables = mergeEnvVariables(customEnvVariables, process.env); - customEnvVariables[PATH_VARIABLE_NAME] = newPath; - } + InterpreterInfoCache.setCustomEnvVariables(resource, customEnvVariables); + return customEnvVariables; +} +export async function execPythonFile(resource: string | Uri | undefined, file: string, args: string[], cwd: string, includeErrorAsResponse: boolean = false, stdOut: (line: string) => void = null, token?: CancellationToken): Promise { + const resourceUri = typeof resource === 'string' ? Uri.file(resource) : resource; + const env = await getEnvVariables(resourceUri); + const options = { cwd, env }; - if (stdOut) { - return spawnFileInternal(file, args, { cwd, env: customEnvVariables }, includeErrorAsResponse, stdOut, token); - } - if (execAsModule) { - return getFullyQualifiedPythonInterpreterPath() - .then(p => execPythonModule(p, args, { cwd: cwd, env: customEnvVariables }, includeErrorAsResponse, token)); - } - return execFileInternal(file, args, { cwd, env: customEnvVariables }, includeErrorAsResponse, token); - }); + if (stdOut) { + return spawnFileInternal(file, args, options, includeErrorAsResponse, stdOut, token); + } + + const fileIsPythonInterpreter = (file.toUpperCase() === 'PYTHON' || file === settings.PythonSettings.getInstance(resourceUri).pythonPath); + const execAsModule = fileIsPythonInterpreter && args.length > 0 && args[0] === '-m'; + + if (execAsModule) { + return getFullyQualifiedPythonInterpreterPath(resourceUri) + .then(p => execPythonModule(p, args, options, includeErrorAsResponse, token)); + } + return execFileInternal(file, args, options, includeErrorAsResponse, token); } function handleResponse(file: string, includeErrorAsResponse: boolean, error: Error, stdout: string, stderr: string, token?: CancellationToken): Promise { @@ -179,7 +165,7 @@ function handleResponse(file: string, includeErrorAsResponse: boolean, error: Er // pylint: // In the case of pylint we have some messages (such as config file not found and using default etc...) being returned in stderr - // These error messages are useless when using pylint + // These error messages are useless when using pylint if (includeErrorAsResponse && (stdout.length > 0 || stderr.length > 0)) { return Promise.resolve(stdout + '\n' + stderr); } @@ -203,7 +189,7 @@ function handlePythonModuleResponse(includeErrorAsResponse: boolean, error: Erro // pylint: // In the case of pylint we have some messages (such as config file not found and using default etc...) being returned in stderr - // These error messages are useless when using pylint + // These error messages are useless when using pylint if (includeErrorAsResponse && (stdout.length > 0 || stderr.length > 0)) { return Promise.resolve(stdout + '\n' + stderr); } @@ -252,6 +238,8 @@ function execFileInternal(file: string, args: string[], options: child_process.E } function spawnFileInternal(file: string, args: string[], options: child_process.ExecFileOptions, includeErrorAsResponse: boolean, stdOut: (line: string) => void, token?: CancellationToken): Promise { return new Promise((resolve, reject) => { + options.env = options.env || {}; + options.env['PYTHONIOENCODING'] = 'UTF-8'; let proc = child_process.spawn(file, args, options); let error = ''; let exited = false; @@ -359,21 +347,41 @@ export function getSubDirectories(rootDir: string): Promise { }); } -export function getCustomEnvVars(): any { - const envFile = settings.PythonSettings.getInstance().envFile; - if (typeof envFile === 'string' && - envFile.length > 0 && - fs.existsSync(envFile)) { - - try { - let vars = parseEnvFile(envFile); - if (vars && typeof vars === 'object' && Object.keys(vars).length > 0) { - return vars; - } +export async function getCustomEnvVars(resource?: Uri): Promise<{} | undefined | null> { + const envFile = settings.PythonSettings.getInstance(resource).envFile; + if (typeof envFile !== 'string' || envFile.length === 0) { + return null; + } + const exists = await fsExtra.pathExists(envFile); + if (!exists) { + return null; + } + try { + const vars = parseEnvFile(envFile); + if (vars && typeof vars === 'object' && Object.keys(vars).length > 0) { + return vars; } - catch (ex) { - console.error('Failed to load env file', ex); + } catch (ex) { + console.error('Failed to parse env file', ex); + } + return null; +} +export function getCustomEnvVarsSync(resource?: Uri): {} | undefined | null { + const envFile = settings.PythonSettings.getInstance(resource).envFile; + if (typeof envFile !== 'string' || envFile.length === 0) { + return null; + } + const exists = fsExtra.pathExistsSync(envFile); + if (!exists) { + return null; + } + try { + const vars = parseEnvFile(envFile); + if (vars && typeof vars === 'object' && Object.keys(vars).length > 0) { + return vars; } + } catch (ex) { + console.error('Failed to parse env file', ex); } return null; } @@ -415,10 +423,12 @@ export function areBasePathsSame(path1: string, path2: string) { path2 = IS_WINDOWS ? path2.replace(/\//g, "\\") : path2; return path.dirname(path1).toUpperCase() === path.dirname(path2).toUpperCase(); } -export function getInterpreterDisplayName(pythonPath: string) { - return execPythonFile(pythonPath, ['--version'], __dirname, true) - .then(version => { - version = version.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0).join(''); - return version; +export async function getInterpreterDisplayName(pythonPath: string) { + return await new Promise((resolve, reject) => { + child_process.execFile(pythonPath, ['--version'], (error, stdout, stdErr) => { + const out = (typeof stdErr === 'string' ? stdErr : '') + os.EOL + (typeof stdout === 'string' ? stdout : ''); + const lines = out.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); + resolve(lines.length > 0 ? lines[0] : ''); }); + }); } diff --git a/src/client/debugger/configProviders/simpleProvider.ts b/src/client/debugger/configProviders/simpleProvider.ts new file mode 100644 index 000000000000..391edd782d99 --- /dev/null +++ b/src/client/debugger/configProviders/simpleProvider.ts @@ -0,0 +1,66 @@ +import * as path from 'path'; +import { PythonSettings } from '../../common/configSettings'; +import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, ProviderResult, Uri, window, workspace, WorkspaceFolder } from 'vscode'; + +type PythonDebugConfiguration = DebugConfiguration & { + stopOnEntry?: boolean, + pythonPath?: string, + program?: string, + cwd?: string, + env?: object, + envFile?: string, + debugOptions?: string[] +}; + +export class SimpleConfigurationProvider implements DebugConfigurationProvider { + private getProgram(config: PythonDebugConfiguration): string | undefined { + const editor = window.activeTextEditor; + if (editor && editor.document.languageId === 'python') { + return editor.document.fileName; + } + return undefined; + } + private getWorkspaceFolder(config: PythonDebugConfiguration): string | undefined { + const program = this.getProgram(config); + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return program ? path.dirname(program) : undefined; + } + if (workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri.fsPath; + } + if (program) { + const workspaceFolder = workspace.getWorkspaceFolder(Uri.file(program)); + if (workspaceFolder) { + return workspaceFolder.uri.fsPath; + } + } + + return undefined; + } + resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult { + if (Object.keys(debugConfiguration).length > 0) { + return debugConfiguration; + } + + const config = debugConfiguration as PythonDebugConfiguration; + const defaultProgram = this.getProgram(config); + const workspaceFolder = this.getWorkspaceFolder(config); + const envFile = workspaceFolder ? path.join(workspaceFolder, '.env') : undefined; + return { + name: 'Launch', + type: 'python', + request: 'launch', + stopOnEntry: true, + pythonPath: PythonSettings.getInstance(workspaceFolder ? Uri.file(workspaceFolder) : undefined).pythonPath, + program: defaultProgram, + cwd: workspaceFolder, + envFile, + env: {}, + debugOptions: [ + 'WaitOnAbnormalExit', + 'WaitOnNormalExit', + 'RedirectOutput' + ] + }; + } +} diff --git a/src/client/debugger/index.ts b/src/client/debugger/index.ts new file mode 100644 index 000000000000..d2343dd26a9e --- /dev/null +++ b/src/client/debugger/index.ts @@ -0,0 +1 @@ +export * from './configProviders/simpleProvider'; \ No newline at end of file diff --git a/src/client/extension.ts b/src/client/extension.ts index 90f5c7d2e2ce..bb5cf5252048 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -1,53 +1,53 @@ 'use strict'; +import * as fs from 'fs'; +import * as os from 'os'; +import { workspace } from 'vscode'; import * as vscode from 'vscode'; +import * as settings from './common/configSettings'; +import { Commands } from './common/constants'; +import { createDeferred } from './common/helpers'; +import * as telemetryHelper from './common/telemetry'; +import * as telemetryContracts from './common/telemetryContracts'; +import { SimpleConfigurationProvider } from './debugger'; +import { HelpProvider } from './helpProvider'; +import { InterpreterManager } from './interpreter'; +import { SetInterpreterProvider } from './interpreter/configuration/setInterpreterProvider'; +import { ShebangCodeLensProvider } from './interpreter/display/shebangCodeLensProvider'; +import * as jup from './jupyter/main'; +import { JupyterProvider } from './jupyter/provider'; +import { JediFactory } from './languageServices/jediProxyFactory'; import { PythonCompletionItemProvider } from './providers/completionProvider'; -import { PythonHoverProvider } from './providers/hoverProvider'; import { PythonDefinitionProvider } from './providers/definitionProvider'; -import { PythonReferenceProvider } from './providers/referenceProvider'; -import { PythonRenameProvider } from './providers/renameProvider'; +import { activateExecInTerminalProvider } from './providers/execInTerminalProvider'; +import { activateFormatOnSaveProvider } from './providers/formatOnSaveProvider'; import { PythonFormattingEditProvider } from './providers/formatProvider'; -import { ShebangCodeLensProvider } from './providers/shebangCodeLensProvider' -import * as sortImports from './sortImports'; +import { PythonHoverProvider } from './providers/hoverProvider'; import { LintProvider } from './providers/lintProvider'; -import { PythonSymbolProvider } from './providers/symbolProvider'; +import { activateGoToObjectDefinitionProvider } from './providers/objectDefinitionProvider'; +import { PythonReferenceProvider } from './providers/referenceProvider'; +import { PythonRenameProvider } from './providers/renameProvider'; +import { ReplProvider } from './providers/replProvider'; import { PythonSignatureProvider } from './providers/signatureProvider'; -import * as settings from './common/configSettings'; -import * as telemetryHelper from './common/telemetry'; -import * as telemetryContracts from './common/telemetryContracts'; import { activateSimplePythonRefactorProvider } from './providers/simpleRefactorProvider'; -import { SetInterpreterProvider } from './providers/setInterpreterProvider'; -import { activateExecInTerminalProvider } from './providers/execInTerminalProvider'; -import { Commands } from './common/constants'; -import * as tests from './unittests/main'; -import * as jup from './jupyter/main'; -import { HelpProvider } from './helpProvider'; +import { PythonSymbolProvider } from './providers/symbolProvider'; import { activateUpdateSparkLibraryProvider } from './providers/updateSparkLibraryProvider'; -import { activateFormatOnSaveProvider } from './providers/formatOnSaveProvider'; -import { WorkspaceSymbols } from './workspaceSymbols/main'; +import * as sortImports from './sortImports'; import { BlockFormatProviders } from './typeFormatters/blockFormatProvider'; -import * as os from 'os'; -import * as fs from 'fs'; -import { activateSingleFileDebug } from './singleFileDebug'; -import { getPathFromPythonCommand } from './common/utils'; -import { JupyterProvider } from './jupyter/provider'; -import { activateGoToObjectDefinitionProvider } from './providers/objectDefinitionProvider'; -import { InterpreterManager } from './interpreter'; +import * as tests from './unittests/main'; +import { WorkspaceSymbols } from './workspaceSymbols/main'; const PYTHON: vscode.DocumentFilter = { language: 'python' }; let unitTestOutChannel: vscode.OutputChannel; let formatOutChannel: vscode.OutputChannel; let lintingOutChannel: vscode.OutputChannel; let jupMain: jup.Jupyter; +const activationDeferred = createDeferred(); +export const activated = activationDeferred.promise; +// tslint:disable-next-line:max-func-body-length export async function activate(context: vscode.ExtensionContext) { const pythonSettings = settings.PythonSettings.getInstance(); - const pythonExt = new PythonExt(); - context.subscriptions.push(pythonExt); - // telemetryHelper.sendTelemetryEvent(telemetryContracts.EVENT_LOAD, { - // CodeComplete_Has_ExtraPaths: pythonSettings.autoComplete.extraPaths.length > 0 ? 'true' : 'false', - // Format_Has_Custom_Python_Path: pythonSettings.pythonPath.length !== 'python'.length ? 'true' : 'false', - // Has_PySpark_Path: hasPySparkInCompletionPath ? 'true' : 'false' - // }); + sendStartupTelemetry(); lintingOutChannel = vscode.window.createOutputChannel(pythonSettings.linting.outputWindow); formatOutChannel = lintingOutChannel; if (pythonSettings.linting.outputWindow !== pythonSettings.formatting.outputWindow) { @@ -62,23 +62,17 @@ export async function activate(context: vscode.ExtensionContext) { sortImports.activate(context, formatOutChannel); const interpreterManager = new InterpreterManager(); await interpreterManager.autoSetInterpreter(); + await interpreterManager.refresh(); context.subscriptions.push(interpreterManager); context.subscriptions.push(new SetInterpreterProvider(interpreterManager)); context.subscriptions.push(...activateExecInTerminalProvider()); context.subscriptions.push(activateUpdateSparkLibraryProvider()); activateSimplePythonRefactorProvider(context, formatOutChannel); context.subscriptions.push(activateFormatOnSaveProvider(PYTHON, formatOutChannel)); - context.subscriptions.push(activateGoToObjectDefinitionProvider(context)); + const jediFactory = new JediFactory(context.asAbsolutePath('.')); + context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory)); - context.subscriptions.push(vscode.commands.registerCommand(Commands.Start_REPL, () => { - getPathFromPythonCommand(["-c", "import sys;print(sys.executable)"]).catch(() => { - return pythonSettings.pythonPath; - }).then(pythonExecutablePath => { - let term = vscode.window.createTerminal('Python', pythonExecutablePath); - term.show(); - context.subscriptions.push(term); - }); - })); + context.subscriptions.push(new ReplProvider()); // Enable indentAction vscode.languages.setLanguageConfiguration(PYTHON.language, { @@ -90,28 +84,28 @@ export async function activate(context: vscode.ExtensionContext) { { beforeText: /^ *#.*$/, afterText: /.+$/, - action: { indentAction: vscode.IndentAction.None, appendText: '# ' }, + action: { indentAction: vscode.IndentAction.None, appendText: '# ' } }, { beforeText: /^\s+(continue|break|return)\b.*$/, - action: { indentAction: vscode.IndentAction.Outdent }, + action: { indentAction: vscode.IndentAction.Outdent } } ] }); + context.subscriptions.push(jediFactory); context.subscriptions.push(vscode.languages.registerRenameProvider(PYTHON, new PythonRenameProvider(formatOutChannel))); - const definitionProvider = new PythonDefinitionProvider(context); - const jediProx = definitionProvider.JediProxy; + const definitionProvider = new PythonDefinitionProvider(jediFactory); context.subscriptions.push(vscode.languages.registerDefinitionProvider(PYTHON, definitionProvider)); - context.subscriptions.push(vscode.languages.registerHoverProvider(PYTHON, new PythonHoverProvider(context, jediProx))); - context.subscriptions.push(vscode.languages.registerReferenceProvider(PYTHON, new PythonReferenceProvider(context, jediProx))); - context.subscriptions.push(vscode.languages.registerCompletionItemProvider(PYTHON, new PythonCompletionItemProvider(context, jediProx), '.')); - context.subscriptions.push(vscode.languages.registerCodeLensProvider(PYTHON, new ShebangCodeLensProvider())) + context.subscriptions.push(vscode.languages.registerHoverProvider(PYTHON, new PythonHoverProvider(jediFactory))); + context.subscriptions.push(vscode.languages.registerReferenceProvider(PYTHON, new PythonReferenceProvider(jediFactory))); + context.subscriptions.push(vscode.languages.registerCompletionItemProvider(PYTHON, new PythonCompletionItemProvider(jediFactory), '.')); + context.subscriptions.push(vscode.languages.registerCodeLensProvider(PYTHON, new ShebangCodeLensProvider())); - const symbolProvider = new PythonSymbolProvider(context, jediProx); + const symbolProvider = new PythonSymbolProvider(jediFactory); context.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(PYTHON, symbolProvider)); if (pythonSettings.devOptions.indexOf('DISABLE_SIGNATURE') === -1) { - context.subscriptions.push(vscode.languages.registerSignatureHelpProvider(PYTHON, new PythonSignatureProvider(context, jediProx), '(', ',')); + context.subscriptions.push(vscode.languages.registerSignatureHelpProvider(PYTHON, new PythonSignatureProvider(jediFactory), '(', ',')); } if (pythonSettings.formatting.provider !== 'none') { const formatProvider = new PythonFormattingEditProvider(context, formatOutChannel); @@ -120,7 +114,7 @@ export async function activate(context: vscode.ExtensionContext) { } const jupyterExtInstalled = vscode.extensions.getExtension('donjayamanne.jupyter'); - let linterProvider = new LintProvider(context, lintingOutChannel, (a, b) => Promise.resolve(false)); + const linterProvider = new LintProvider(context, lintingOutChannel, (a, b) => Promise.resolve(false)); context.subscriptions.push(); if (jupyterExtInstalled) { if (jupyterExtInstalled.isActive) { @@ -132,8 +126,7 @@ export async function activate(context: vscode.ExtensionContext) { jupyterExtInstalled.exports.registerLanguageProvider(PYTHON.language, new JupyterProvider()); linterProvider.documentHasJupyterCodeCells = jupyterExtInstalled.exports.hasCodeCells; }); - } - else { + } else { jupMain = new jup.Jupyter(lintingOutChannel); const documentHasJupyterCodeCells = jupMain.hasCodeCells.bind(jupMain); jupMain.activate(); @@ -152,42 +145,10 @@ export async function activate(context: vscode.ExtensionContext) { const hepProvider = new HelpProvider(); context.subscriptions.push(hepProvider); - context.subscriptions.push(activateSingleFileDebug()); + context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('python', new SimpleConfigurationProvider())); + activationDeferred.resolve(); } -class PythonExt implements vscode.Disposable { - - private isDjangoProject: ContextKey; - - constructor() { - this.isDjangoProject = new ContextKey('python.isDjangoProject'); - this.ensureState(); - } - public dispose() { - this.isDjangoProject = null; - } - private ensureState(): void { - // context: python.isDjangoProject - if (typeof vscode.workspace.rootPath === 'string') { - this.isDjangoProject.set(fs.existsSync(vscode.workspace.rootPath.concat("/manage.py"))); - } - else { - this.isDjangoProject.set(false); - } - } +function sendStartupTelemetry() { + telemetryHelper.sendTelemetryEvent(telemetryContracts.EVENT_LOAD); } - -class ContextKey { - private lastValue: boolean; - - constructor(private name: string) { - } - - public set(value: boolean): void { - if (this.lastValue === value) { - return; - } - this.lastValue = value; - vscode.commands.executeCommand('setContext', this.name, this.lastValue); - } -} diff --git a/src/client/formatters/baseFormatter.ts b/src/client/formatters/baseFormatter.ts index 3fcce0d76661..1bbc4b3659c3 100644 --- a/src/client/formatters/baseFormatter.ts +++ b/src/client/formatters/baseFormatter.ts @@ -50,7 +50,7 @@ export abstract class BaseFormatter { if (token && token.isCancellationRequested) { return [filePath, '']; } - return Promise.all([Promise.resolve(filePath), execPythonFile(command, args.concat([filePath]), cwd)]); + return Promise.all([Promise.resolve(filePath), execPythonFile(document.uri, command, args.concat([filePath]), cwd)]); }).then(data => { // Delete the temporary file created if (tmpFileCreated) { @@ -61,14 +61,14 @@ export abstract class BaseFormatter { } return getTextEditsFromPatch(document.getText(), data[1]); }).catch(error => { - this.handleError(this.Id, command, error); + this.handleError(this.Id, command, error, document.uri); return []; }); vscode.window.setStatusBarMessage(`Formatting with ${this.Id}`, promise); return promise; } - protected handleError(expectedFileName: string, fileName: string, error: Error) { + protected handleError(expectedFileName: string, fileName: string, error: Error, resource?: Uri) { let customError = `Formatting with ${this.Id} failed.`; if (isNotInstalledError(error)) { @@ -84,7 +84,7 @@ export abstract class BaseFormatter { } else { customError += `\nYou could either install the '${this.Id}' formatter, turn it off or use another formatter.`; - this.installer.promptToInstall(this.product); + this.installer.promptToInstall(this.product, resource); } } diff --git a/src/client/interpreter/configuration/pythonPathUpdaterService.ts b/src/client/interpreter/configuration/pythonPathUpdaterService.ts new file mode 100644 index 000000000000..b81963f9d8fe --- /dev/null +++ b/src/client/interpreter/configuration/pythonPathUpdaterService.ts @@ -0,0 +1,41 @@ +import * as path from 'path'; +import { ConfigurationTarget, Uri, window } from 'vscode'; +import { WorkspacePythonPath } from '../contracts'; +import { IPythonPathUpdaterService, IPythonPathUpdaterServiceFactory } from './types'; + +export class PythonPathUpdaterService { + constructor(private pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory) { } + public async updatePythonPath(pythonPath: string, configTarget: ConfigurationTarget, wkspace?: Uri): Promise { + const pythonPathUpdater = this.getPythonUpdaterService(configTarget, wkspace); + + try { + await pythonPathUpdater.updatePythonPath(path.normalize(pythonPath)); + } catch (reason) { + // tslint:disable-next-line:no-unsafe-any prefer-type-cast + const message = reason && typeof reason.message === 'string' ? reason.message as string : ''; + window.showErrorMessage(`Failed to set 'pythonPath'. Error: ${message}`); + console.error(reason); + } + } + private getPythonUpdaterService(configTarget: ConfigurationTarget, wkspace?: Uri) { + switch (configTarget) { + case ConfigurationTarget.Global: { + return this.pythonPathSettingsUpdaterFactory.getGlobalPythonPathConfigurationService(); + } + case ConfigurationTarget.Workspace: { + if (!wkspace) { + throw new Error('Workspace Uri not defined'); + } + // tslint:disable-next-line:no-non-null-assertion + return this.pythonPathSettingsUpdaterFactory.getWorkspacePythonPathConfigurationService(wkspace!); + } + default: { + if (!wkspace) { + throw new Error('Workspace Uri not defined'); + } + // tslint:disable-next-line:no-non-null-assertion + return this.pythonPathSettingsUpdaterFactory.getWorkspaceFolderPythonPathConfigurationService(wkspace!); + } + } + } +} diff --git a/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts b/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts new file mode 100644 index 000000000000..a46c18275d35 --- /dev/null +++ b/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts @@ -0,0 +1,18 @@ +import { Uri } from 'vscode'; +import { InterpreterManager } from '../index'; +import { GlobalPythonPathUpdaterService } from './services/globalUpdaterService'; +import { WorkspaceFolderPythonPathUpdaterService } from './services/workspaceFolderUpdaterService'; +import { WorkspacePythonPathUpdaterService } from './services/workspaceUpdaterService'; +import { IPythonPathUpdaterService, IPythonPathUpdaterServiceFactory } from './types'; + +export class PythonPathUpdaterServiceFactory implements IPythonPathUpdaterServiceFactory { + public getGlobalPythonPathConfigurationService(): IPythonPathUpdaterService { + return new GlobalPythonPathUpdaterService(); + } + public getWorkspacePythonPathConfigurationService(wkspace: Uri): IPythonPathUpdaterService { + return new WorkspacePythonPathUpdaterService(wkspace); + } + public getWorkspaceFolderPythonPathConfigurationService(workspaceFolder: Uri): IPythonPathUpdaterService { + return new WorkspaceFolderPythonPathUpdaterService(workspaceFolder); + } +} diff --git a/src/client/interpreter/configuration/services/globalUpdaterService.ts b/src/client/interpreter/configuration/services/globalUpdaterService.ts new file mode 100644 index 000000000000..4b775adf0b71 --- /dev/null +++ b/src/client/interpreter/configuration/services/globalUpdaterService.ts @@ -0,0 +1,16 @@ +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { InterpreterManager } from '../..'; +import { WorkspacePythonPath } from '../../contracts'; +import { IPythonPathUpdaterService } from '../types'; + +export class GlobalPythonPathUpdaterService implements IPythonPathUpdaterService { + public async updatePythonPath(pythonPath: string): Promise { + const pythonConfig = workspace.getConfiguration('python'); + const pythonPathValue = pythonConfig.inspect('pythonPath'); + + if (pythonPathValue && pythonPathValue.globalValue === pythonPath) { + return; + } + await pythonConfig.update('pythonPath', pythonPath, true); + } +} diff --git a/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts new file mode 100644 index 000000000000..d39f9a831f93 --- /dev/null +++ b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts @@ -0,0 +1,23 @@ +import * as path from 'path'; +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { InterpreterManager } from '../..'; +import { WorkspacePythonPath } from '../../contracts'; +import { IPythonPathUpdaterService } from '../types'; + +export class WorkspaceFolderPythonPathUpdaterService implements IPythonPathUpdaterService { + constructor(private workspaceFolder: Uri) { + } + public async updatePythonPath(pythonPath: string): Promise { + const pythonConfig = workspace.getConfiguration('python', this.workspaceFolder); + const pythonPathValue = pythonConfig.inspect('pythonPath'); + + if (pythonPathValue && pythonPathValue.workspaceFolderValue === pythonPath) { + return; + } + if (pythonPath.startsWith(this.workspaceFolder.fsPath)) { + // tslint:disable-next-line:no-invalid-template-strings + pythonPath = path.join('${workspaceRoot}', path.relative(this.workspaceFolder.fsPath, pythonPath)); + } + await pythonConfig.update('pythonPath', pythonPath, ConfigurationTarget.WorkspaceFolder); + } +} diff --git a/src/client/interpreter/configuration/services/workspaceUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceUpdaterService.ts new file mode 100644 index 000000000000..b0da1b168345 --- /dev/null +++ b/src/client/interpreter/configuration/services/workspaceUpdaterService.ts @@ -0,0 +1,23 @@ +import * as path from 'path'; +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { InterpreterManager } from '../..'; +import { WorkspacePythonPath } from '../../contracts'; +import { IPythonPathUpdaterService } from '../types'; + +export class WorkspacePythonPathUpdaterService implements IPythonPathUpdaterService { + constructor(private wkspace: Uri) { + } + public async updatePythonPath(pythonPath: string): Promise { + const pythonConfig = workspace.getConfiguration('python', this.wkspace); + const pythonPathValue = pythonConfig.inspect('pythonPath'); + + if (pythonPathValue && pythonPathValue.workspaceValue === pythonPath) { + return; + } + if (pythonPath.startsWith(this.wkspace.fsPath)) { + // tslint:disable-next-line:no-invalid-template-strings + pythonPath = path.join('${workspaceRoot}', path.relative(this.wkspace.fsPath, pythonPath)); + } + await pythonConfig.update('pythonPath', pythonPath, false); + } +} diff --git a/src/client/interpreter/configuration/setInterpreterProvider.ts b/src/client/interpreter/configuration/setInterpreterProvider.ts new file mode 100644 index 000000000000..b155d7caa049 --- /dev/null +++ b/src/client/interpreter/configuration/setInterpreterProvider.ts @@ -0,0 +1,114 @@ +'use strict'; +import * as path from 'path'; +import { commands, ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; +import { InterpreterManager } from '../'; +import * as settings from '../../common/configSettings'; +import { PythonInterpreter, WorkspacePythonPath } from '../contracts'; +import { ShebangCodeLensProvider } from '../display/shebangCodeLensProvider'; +import { PythonPathUpdaterService } from './pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from './pythonPathUpdaterServiceFactory'; +import { IPythonPathUpdaterServiceFactory } from './types'; + +// tslint:disable-next-line:interface-name +interface PythonPathQuickPickItem extends QuickPickItem { + path: string; +} + +export class SetInterpreterProvider implements Disposable { + private disposables: Disposable[] = []; + private pythonPathUpdaterService: PythonPathUpdaterService; + constructor(private interpreterManager: InterpreterManager) { + this.disposables.push(commands.registerCommand('python.setInterpreter', this.setInterpreter.bind(this))); + this.disposables.push(commands.registerCommand('python.setShebangInterpreter', this.setShebangInterpreter.bind(this))); + this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory()); + } + public dispose() { + this.disposables.forEach(disposable => disposable.dispose()); + } + private async getWorkspaceToSetPythonPath(): Promise { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return undefined; + } + if (workspace.workspaceFolders.length === 1) { + return { folderUri: workspace.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; + } + + // Ok we have multiple interpreters, get the user to pick a folder. + // tslint:disable-next-line:no-any prefer-type-cast + const workspaceFolder = await (window as any).showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); + return workspaceFolder ? { folderUri: workspaceFolder.uri, configTarget: ConfigurationTarget.WorkspaceFolder } : undefined; + } + private async suggestionToQuickPickItem(suggestion: PythonInterpreter, workspaceUri?: Uri): Promise { + let detail = suggestion.path; + if (workspaceUri && suggestion.path.startsWith(workspaceUri.fsPath)) { + detail = `.${path.sep}${path.relative(workspaceUri.fsPath, suggestion.path)}`; + } + return { + // tslint:disable-next-line:no-non-null-assertion + label: suggestion.displayName!, + description: suggestion.companyDisplayName || '', + detail: detail, + path: suggestion.path + }; + } + + private async getSuggestions(resourceUri?: Uri) { + const interpreters = await this.interpreterManager.getInterpreters(resourceUri); + // tslint:disable-next-line:no-non-null-assertion + interpreters.sort((a, b) => a.displayName! > b.displayName! ? 1 : -1); + return Promise.all(interpreters.map(item => this.suggestionToQuickPickItem(item, resourceUri))); + } + + private async setInterpreter() { + const setInterpreterGlobally = !Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0; + let configTarget = ConfigurationTarget.Global; + let wkspace: Uri; + if (!setInterpreterGlobally) { + const targetConfig = await this.getWorkspaceToSetPythonPath(); + if (!targetConfig) { + return; + } + configTarget = targetConfig.configTarget; + wkspace = targetConfig.folderUri; + } + + const suggestions = await this.getSuggestions(wkspace); + let currentPythonPath = settings.PythonSettings.getInstance().pythonPath; + if (wkspace && currentPythonPath.startsWith(wkspace.fsPath)) { + currentPythonPath = `.${path.sep}${path.relative(wkspace.fsPath, currentPythonPath)}`; + } + const quickPickOptions: QuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: `current: ${currentPythonPath}` + }; + + const selection = await window.showQuickPick(suggestions, quickPickOptions); + if (selection !== undefined) { + await this.pythonPathUpdaterService.updatePythonPath(selection.path, configTarget, wkspace); + } + } + + private async setShebangInterpreter(): Promise { + const shebang = await ShebangCodeLensProvider.detectShebang(window.activeTextEditor.document); + if (!shebang) { + return; + } + + const isGlobalChange = !Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0; + const workspaceFolder = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri); + const isWorkspaceChange = Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length === 1; + + if (isGlobalChange) { + await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Global); + return; + } + + if (isWorkspaceChange || !workspaceFolder) { + await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Workspace, workspace.workspaceFolders[0].uri); + return; + } + + await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.WorkspaceFolder, workspaceFolder.uri); + } +} diff --git a/src/client/interpreter/configuration/types.ts b/src/client/interpreter/configuration/types.ts new file mode 100644 index 000000000000..05825416f3bc --- /dev/null +++ b/src/client/interpreter/configuration/types.ts @@ -0,0 +1,12 @@ +import { Uri } from 'vscode'; +import { IPythonPathUpdaterService } from './types'; + +export interface IPythonPathUpdaterService { + updatePythonPath(pythonPath: string): Promise; +} + +export interface IPythonPathUpdaterServiceFactory { + getGlobalPythonPathConfigurationService(): IPythonPathUpdaterService; + getWorkspacePythonPathConfigurationService(wkspace: Uri): IPythonPathUpdaterService; + getWorkspaceFolderPythonPathConfigurationService(workspaceFolder: Uri): IPythonPathUpdaterService; +} diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index cb5120a549e0..24c11f906597 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -1,12 +1,20 @@ -import { Architecture } from "../common/registry"; +import { ConfigurationTarget, Disposable, Uri } from 'vscode'; +import { Architecture } from '../common/registry'; -export interface IInterpreterLocatorService { - getInterpreters(): Promise; +export interface IInterpreterLocatorService extends Disposable { + getInterpreters(resource?: Uri): Promise; } -export interface PythonInterpreter { + +export type PythonInterpreter = { path: string; companyDisplayName?: string; displayName?: string; version?: string; architecture?: Architecture; -} +}; + +export type WorkspacePythonPath = { + folderUri: Uri; + pytonPath?: string; + configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder; +}; diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index de114a51faf5..b104f4953216 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -1,30 +1,36 @@ 'use strict'; -import * as path from 'path'; -import * as utils from '../../common/utils'; import * as child_process from 'child_process'; -import { StatusBarItem, Disposable } from 'vscode'; -import { PythonSettings } from '../../common/configSettings'; import { EOL } from 'os'; +import * as path from 'path'; +import { Disposable, StatusBarItem } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; +import * as utils from '../../common/utils'; import { IInterpreterLocatorService } from '../contracts'; +import { getActiveWorkspaceUri, getFirstNonEmptyLineFromMultilineString } from '../helpers'; import { IInterpreterVersionService } from '../interpreterVersion'; import { VirtualEnvironmentManager } from '../virtualEnvs/index'; -import { getFirstNonEmptyLineFromMultilineString } from '../helpers'; -const settings = PythonSettings.getInstance(); +// tslint:disable-next-line:completed-docs export class InterpreterDisplay implements Disposable { constructor(private statusBar: StatusBarItem, private interpreterLocator: IInterpreterLocatorService, private virtualEnvMgr: VirtualEnvironmentManager, private versionProvider: IInterpreterVersionService) { + this.statusBar.command = 'python.setInterpreter'; } public dispose() { + // } public async refresh() { - const pythonPath = await this.getFullyQualifiedPathToInterpreter(settings.pythonPath); + const wkspc = getActiveWorkspaceUri(); + if (!wkspc) { + return; + } + const pythonPath = await this.getFullyQualifiedPathToInterpreter(PythonSettings.getInstance(wkspc.folderUri).pythonPath); await this.updateDisplay(pythonPath); } - private getInterpreters() { + private async getInterpreters() { return this.interpreterLocator.getInterpreters(); } private async updateDisplay(pythonPath: string) { @@ -34,18 +40,19 @@ export class InterpreterDisplay implements Disposable { this.statusBar.color = ''; this.statusBar.tooltip = pythonPath; if (interpreter) { + // tslint:disable-next-line:no-non-null-assertion this.statusBar.text = interpreter.displayName!; if (interpreter.companyDisplayName) { const toolTipSuffix = `${EOL}${interpreter.companyDisplayName}`; this.statusBar.tooltip += toolTipSuffix; } - } - else { + } else { const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; - const interpreterExists = utils.fsExistsAsync(pythonPath); - const displayName = this.versionProvider.getVersion(pythonPath, defaultDisplayName); - const virtualEnvName = this.getVirtualEnvironmentName(pythonPath); - await Promise.all([interpreterExists, displayName, virtualEnvName]) + await Promise.all([ + utils.fsExistsAsync(pythonPath), + this.versionProvider.getVersion(pythonPath, defaultDisplayName), + this.getVirtualEnvironmentName(pythonPath) + ]) .then(([interpreterExists, displayName, virtualEnvName]) => { const dislayNameSuffix = virtualEnvName.length > 0 ? ` (${virtualEnvName})` : ''; this.statusBar.text = `${displayName}${dislayNameSuffix}`; @@ -63,13 +70,13 @@ export class InterpreterDisplay implements Disposable { .detect(pythonPath) .then(env => env ? env.name : ''); } - private getFullyQualifiedPathToInterpreter(pythonPath: string) { + private async getFullyQualifiedPathToInterpreter(pythonPath: string) { return new Promise(resolve => { - child_process.execFile(pythonPath, ["-c", "import sys;print(sys.executable)"], (_, stdout) => { + child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { resolve(getFirstNonEmptyLineFromMultilineString(stdout)); }); }) - .then(value => value.length === 0 ? pythonPath : value); + .then(value => value.length === 0 ? pythonPath : value) + .catch(() => pythonPath); } } - diff --git a/src/client/interpreter/display/shebangCodeLensProvider.ts b/src/client/interpreter/display/shebangCodeLensProvider.ts new file mode 100644 index 000000000000..a45f4a4db431 --- /dev/null +++ b/src/client/interpreter/display/shebangCodeLensProvider.ts @@ -0,0 +1,71 @@ +'use strict'; +import * as child_process from 'child_process'; +import * as vscode from 'vscode'; +import { CancellationToken, CodeLens, TextDocument } from 'vscode'; +import * as settings from '../../common/configSettings'; +import { IS_WINDOWS } from '../../common/utils'; +import { getFirstNonEmptyLineFromMultilineString } from '../../interpreter/helpers'; + +export class ShebangCodeLensProvider implements vscode.CodeLensProvider { + public onDidChangeCodeLenses: vscode.Event = vscode.workspace.onDidChangeConfiguration; + // tslint:disable-next-line:function-name + public static async detectShebang(document: TextDocument): Promise { + const firstLine = document.lineAt(0); + if (firstLine.isEmptyOrWhitespace) { + return; + } + + if (!firstLine.text.startsWith('#!')) { + return; + } + + const shebang = firstLine.text.substr(2).trim(); + const pythonPath = await ShebangCodeLensProvider.getFullyQualifiedPathToInterpreter(shebang); + return typeof pythonPath === 'string' && pythonPath.length > 0 ? pythonPath : undefined; + } + private static async getFullyQualifiedPathToInterpreter(pythonPath: string) { + if (pythonPath.indexOf('bin/env ') >= 0 && !IS_WINDOWS) { + // In case we have pythonPath as '/usr/bin/env python' + return new Promise(resolve => { + const command = child_process.exec(`${pythonPath} -c 'import sys;print(sys.executable)'`); + let result = ''; + command.stdout.on('data', (data) => { + result += data.toString(); + }); + command.on('close', () => { + resolve(getFirstNonEmptyLineFromMultilineString(result)); + }); + }); + } else { + return new Promise(resolve => { + child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { + resolve(getFirstNonEmptyLineFromMultilineString(stdout)); + }); + }); + } + } + + public async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { + const codeLenses = await this.createShebangCodeLens(document); + return Promise.resolve(codeLenses); + } + + private async createShebangCodeLens(document: TextDocument) { + const shebang = await ShebangCodeLensProvider.detectShebang(document); + if (!shebang || shebang === settings.PythonSettings.getInstance(document.uri).pythonPath) { + return []; + } + + const firstLine = document.lineAt(0); + const startOfShebang = new vscode.Position(0, 0); + const endOfShebang = new vscode.Position(0, firstLine.text.length - 1); + const shebangRange = new vscode.Range(startOfShebang, endOfShebang); + + const cmd: vscode.Command = { + command: 'python.setShebangInterpreter', + title: 'Set as interpreter' + }; + + return [(new CodeLens(shebangRange, cmd))]; + } +} diff --git a/src/client/interpreter/helpers.ts b/src/client/interpreter/helpers.ts index 7bd180b5b067..dc8308344d2f 100644 --- a/src/client/interpreter/helpers.ts +++ b/src/client/interpreter/helpers.ts @@ -1,3 +1,6 @@ +import { ConfigurationTarget, window, workspace } from 'vscode'; +import { WorkspacePythonPath } from './contracts'; + export function getFirstNonEmptyLineFromMultilineString(stdout: string) { if (!stdout) { return ''; @@ -5,3 +8,18 @@ export function getFirstNonEmptyLineFromMultilineString(stdout: string) { const lines = stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); return lines.length > 0 ? lines[0] : ''; } +export function getActiveWorkspaceUri(): WorkspacePythonPath | undefined { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return undefined; + } + if (workspace.workspaceFolders.length === 1) { + return { folderUri: workspace.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; + } + if (window.activeTextEditor) { + const workspaceFolder = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri); + if (workspaceFolder) { + return { configTarget: ConfigurationTarget.WorkspaceFolder, folderUri: workspaceFolder.uri }; + } + } + return undefined; +} diff --git a/src/client/interpreter/index.ts b/src/client/interpreter/index.ts index d2eb6fd9e71d..a90c2592fcb0 100644 --- a/src/client/interpreter/index.ts +++ b/src/client/interpreter/index.ts @@ -1,43 +1,53 @@ 'use strict'; -import { InterpreterVersionService } from './interpreterVersion'; -import { VirtualEnv } from './virtualEnvs/virtualEnv'; -import { VEnv } from './virtualEnvs/venv'; -import { Disposable, window, StatusBarAlignment, workspace } from 'vscode'; +import * as path from 'path'; +import { ConfigurationTarget, Disposable, StatusBarAlignment, Uri, window, workspace } from 'vscode'; import { PythonSettings } from '../common/configSettings'; +import { IS_WINDOWS } from '../common/utils'; +import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory'; +import { WorkspacePythonPath } from './contracts'; import { InterpreterDisplay } from './display'; +import { getActiveWorkspaceUri } from './helpers'; +import { InterpreterVersionService } from './interpreterVersion'; import { PythonInterpreterLocatorService } from './locators'; import { VirtualEnvironmentManager } from './virtualEnvs/index'; -import { IS_WINDOWS } from '../common/utils'; -import * as path from 'path'; - -const settings = PythonSettings.getInstance(); +import { VEnv } from './virtualEnvs/venv'; +import { VirtualEnv } from './virtualEnvs/virtualEnv'; export class InterpreterManager implements Disposable { private disposables: Disposable[] = []; private display: InterpreterDisplay | null | undefined; private interpreterProvider: PythonInterpreterLocatorService; + private pythonPathUpdaterService: PythonPathUpdaterService; constructor() { const virtualEnvMgr = new VirtualEnvironmentManager([new VEnv(), new VirtualEnv()]); const statusBar = window.createStatusBarItem(StatusBarAlignment.Left); this.interpreterProvider = new PythonInterpreterLocatorService(virtualEnvMgr); const versionService = new InterpreterVersionService(); this.display = new InterpreterDisplay(statusBar, this.interpreterProvider, virtualEnvMgr, versionService); - settings.addListener('change', this.onConfigChanged.bind(this)); - this.display.refresh(); - + this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory()); + PythonSettings.getInstance().addListener('change', () => this.onConfigChanged()); + this.disposables.push(window.onDidChangeActiveTextEditor(() => this.refresh())); this.disposables.push(statusBar); this.disposables.push(this.display); } - public getInterpreters() { - return this.interpreterProvider.getInterpreters(); + public async refresh() { + return this.display.refresh(); + } + public getInterpreters(resource?: Uri) { + return this.interpreterProvider.getInterpreters(resource); } public async autoSetInterpreter() { if (!this.shouldAutoSetInterpreter()) { return; } - const interpreters = await this.interpreterProvider.getInterpreters(); - const rootPath = workspace.rootPath!.toUpperCase(); - const interpretersInWorkspace = interpreters.filter(interpreter => interpreter.path.toUpperCase().startsWith(rootPath)); + const activeWorkspace = getActiveWorkspaceUri(); + if (!activeWorkspace) { + return; + } + const interpreters = await this.interpreterProvider.getInterpreters(activeWorkspace.folderUri); + const workspacePathUpper = activeWorkspace.folderUri.fsPath.toUpperCase(); + const interpretersInWorkspace = interpreters.filter(interpreter => interpreter.path.toUpperCase().startsWith(workspacePathUpper)); if (interpretersInWorkspace.length !== 1) { return; } @@ -46,45 +56,30 @@ export class InterpreterManager implements Disposable { // In windows the interpreter is under scripts/python.exe on linux it is under bin/python. // Meaning the sub directory must be either scripts, bin or other (but only one level deep). const pythonPath = interpretersInWorkspace[0].path; - const relativePath = path.dirname(pythonPath).substring(workspace.rootPath!.length); + const relativePath = path.dirname(pythonPath).substring(activeWorkspace.folderUri.fsPath.length); if (relativePath.split(path.sep).filter(l => l.length > 0).length === 2) { - this.setPythonPath(pythonPath); - } - } - - public setPythonPath(pythonPath: string) { - pythonPath = IS_WINDOWS ? pythonPath.replace(/\\/g, "/") : pythonPath; - if (pythonPath.startsWith(workspace.rootPath!)) { - pythonPath = path.join('${workspaceRoot}', path.relative(workspace.rootPath!, pythonPath)); + await this.pythonPathUpdaterService.updatePythonPath(pythonPath, activeWorkspace.configTarget, activeWorkspace.folderUri); } - const pythonConfig = workspace.getConfiguration('python'); - let global = null; - if (typeof workspace.rootPath !== 'string') { - global = true; - } - pythonConfig.update('pythonPath', pythonPath, global).then(() => { - //Done - }, reason => { - window.showErrorMessage(`Failed to set 'pythonPath'. Error: ${reason.message}`); - console.error(reason); - }); } - - public dispose() { - this.disposables.forEach(disposable => disposable.dispose()); + public dispose(): void { + // tslint:disable-next-line:prefer-type-cast + this.disposables.forEach(disposable => disposable.dispose() as void); this.display = null; + this.interpreterProvider.dispose(); } private shouldAutoSetInterpreter() { - if (!workspace.rootPath) { + const activeWorkspace = getActiveWorkspaceUri(); + if (!activeWorkspace) { return false; } const pythonConfig = workspace.getConfiguration('python'); - const pythonPathInConfig = pythonConfig.get('pythonPath', 'python'); - return pythonPathInConfig.toUpperCase() === 'PYTHON'; + const pythonPathInConfig = pythonConfig.get('pythonPath', 'python'); + return path.basename(pythonPathInConfig) === pythonPathInConfig; } private onConfigChanged() { if (this.display) { + // tslint:disable-next-line:no-floating-promises this.display.refresh(); } } -} \ No newline at end of file +} diff --git a/src/client/interpreter/locators/index.ts b/src/client/interpreter/locators/index.ts index d12e527dcb13..09ab1c21bc18 100644 --- a/src/client/interpreter/locators/index.ts +++ b/src/client/interpreter/locators/index.ts @@ -1,61 +1,90 @@ -"use strict"; +'use strict'; import * as _ from 'lodash'; -import { fixInterpreterPath, fixInterpreterDisplayName } from './helpers'; -import { IInterpreterLocatorService, PythonInterpreter } from '../contracts'; -import { InterpreterVersionService } from '../interpreterVersion'; -import { IS_WINDOWS, Is_64Bit, arePathsSame, areBasePathsSame } from '../../common/utils'; +import { Disposable, Uri, workspace } from 'vscode'; import { RegistryImplementation } from '../../common/registry'; +import { areBasePathsSame, arePathsSame, Is_64Bit, IS_WINDOWS } from '../../common/utils'; +import { IInterpreterLocatorService, PythonInterpreter } from '../contracts'; +import { IInterpreterVersionService, InterpreterVersionService } from '../interpreterVersion'; +import { VirtualEnvironmentManager } from '../virtualEnvs'; +import { fixInterpreterDisplayName, fixInterpreterPath } from './helpers'; +import { CondaEnvFileService, getEnvironmentsFile as getCondaEnvFile } from './services/condaEnvFileService'; import { CondaEnvService } from './services/condaEnvService'; -import { VirtualEnvService, getKnownSearchPathsForVirtualEnvs } from './services/virtualEnvService'; -import { KnownPathsService, getKnownSearchPathsForInterpreters } from './services/KnownPathsService'; import { CurrentPathService } from './services/currentPathService'; +import { getKnownSearchPathsForInterpreters, KnownPathsService } from './services/KnownPathsService'; +import { getKnownSearchPathsForVirtualEnvs, VirtualEnvService } from './services/virtualEnvService'; import { WindowsRegistryService } from './services/windowsRegistryService'; -import { VirtualEnvironmentManager } from '../virtualEnvs'; -import { CondaEnvFileService, getEnvironmentsFile as getCondaEnvFile } from './services/condaEnvFileService'; export class PythonInterpreterLocatorService implements IInterpreterLocatorService { - private interpreters: PythonInterpreter[] = []; - private locators: IInterpreterLocatorService[] = []; + private interpretersPerResource: Map; + private disposables: Disposable[] = []; constructor(private virtualEnvMgr: VirtualEnvironmentManager) { + this.interpretersPerResource = new Map(); + this.disposables.push(workspace.onDidChangeConfiguration(this.onConfigChanged, this)); + } + public async getInterpreters(resource?: Uri) { + const resourceKey = this.getResourceKey(resource); + if (!this.interpretersPerResource.has(resourceKey)) { + const interpreters = await this.getInterpretersPerResource(resource); + this.interpretersPerResource.set(resourceKey, interpreters); + } + + // tslint:disable-next-line:no-non-null-assertion + return this.interpretersPerResource.get(resourceKey)!; + } + public dispose() { + this.disposables.forEach(disposable => disposable.dispose()); + } + private onConfigChanged() { + this.interpretersPerResource.clear(); + } + private getResourceKey(resource?: Uri) { + if (!resource) { + return ''; + } + const workspaceFolder = workspace.getWorkspaceFolder(resource); + return workspaceFolder ? workspaceFolder.uri.fsPath : ''; + } + private async getInterpretersPerResource(resource?: Uri) { + const locators = this.getLocators(resource); + const promises = locators.map(provider => provider.getInterpreters(resource)); + const listOfInterpreters = await Promise.all(promises); + + // tslint:disable-next-line:underscore-consistent-invocation + return _.flatten(listOfInterpreters) + .map(fixInterpreterDisplayName) + .map(fixInterpreterPath) + .reduce((accumulator, current) => { + if (accumulator.findIndex(item => arePathsSame(item.path, current.path)) === -1 && + accumulator.findIndex(item => areBasePathsSame(item.path, current.path)) === -1) { + accumulator.push(current); + } + return accumulator; + }, []); + } + private getLocators(resource?: Uri) { + const locators: IInterpreterLocatorService[] = []; const versionService = new InterpreterVersionService(); // The order of the services is important. if (IS_WINDOWS) { const windowsRegistryProvider = new WindowsRegistryService(new RegistryImplementation(), Is_64Bit); - this.locators.push(windowsRegistryProvider); - this.locators.push(new CondaEnvService(windowsRegistryProvider)); - } - else { - this.locators.push(new CondaEnvService()); + locators.push(windowsRegistryProvider); + locators.push(new CondaEnvService(windowsRegistryProvider)); + } else { + locators.push(new CondaEnvService()); } // Supplements the above list of conda environments. - this.locators.push(new CondaEnvFileService(getCondaEnvFile(), versionService)); - this.locators.push(new VirtualEnvService(getKnownSearchPathsForVirtualEnvs(), this.virtualEnvMgr, versionService)); + locators.push(new CondaEnvFileService(getCondaEnvFile(), versionService)); + locators.push(new VirtualEnvService(getKnownSearchPathsForVirtualEnvs(resource), this.virtualEnvMgr, versionService)); if (!IS_WINDOWS) { - // This must be last, it is possible we have paths returned here that are already returned + // This must be last, it is possible we have paths returned here that are already returned // in one of the above lists. - this.locators.push(new KnownPathsService(getKnownSearchPathsForInterpreters(), versionService)); + locators.push(new KnownPathsService(getKnownSearchPathsForInterpreters(), versionService)); } - // This must be last, it is possible we have paths returned here that are already returned + // This must be last, it is possible we have paths returned here that are already returned // in one of the above lists. - this.locators.push(new CurrentPathService(this.virtualEnvMgr, versionService)); - } - public async getInterpreters() { - if (this.interpreters.length > 0) { - return this.interpreters; - } - const promises = this.locators.map(provider => provider.getInterpreters()); - return Promise.all(promises) - .then(interpreters => _.flatten(interpreters)) - .then(items => items.map(fixInterpreterDisplayName)) - .then(items => items.map(fixInterpreterPath)) - .then(items => items.reduce((accumulator, current) => { - if (accumulator.findIndex(item => arePathsSame(item.path, current.path)) === -1 && - accumulator.findIndex(item => areBasePathsSame(item.path, current.path)) === -1) { - accumulator.push(current); - } - return accumulator; - }, [])) - .then(interpreters => this.interpreters = interpreters); + locators.push(new CurrentPathService(this.virtualEnvMgr, versionService)); + + return locators; } } diff --git a/src/client/interpreter/locators/services/KnownPathsService.ts b/src/client/interpreter/locators/services/KnownPathsService.ts index bbfdea9ab4cd..bdb240ededad 100644 --- a/src/client/interpreter/locators/services/KnownPathsService.ts +++ b/src/client/interpreter/locators/services/KnownPathsService.ts @@ -1,22 +1,27 @@ -"use strict"; -import * as path from 'path'; +'use strict'; import * as _ from 'lodash'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { fsExistsAsync, IS_WINDOWS } from '../../../common/utils'; import { IInterpreterLocatorService } from '../../contracts'; import { IInterpreterVersionService } from '../../interpreterVersion'; -import { fsExistsAsync, IS_WINDOWS } from '../../../common/utils'; import { lookForInterpretersInDirectory } from '../helpers'; +// tslint:disable-next-line:no-require-imports no-var-requires const untildify = require('untildify'); export class KnownPathsService implements IInterpreterLocatorService { public constructor(private knownSearchPaths: string[], private versionProvider: IInterpreterVersionService) { } - public getInterpreters() { + // tslint:disable-next-line:no-shadowed-variable + public getInterpreters(_?: Uri) { return this.suggestionsFromKnownPaths(); } - + // tslint:disable-next-line:no-empty + public dispose() { } private suggestionsFromKnownPaths() { const promises = this.knownSearchPaths.map(dir => this.getInterpretersInDirectory(dir)); return Promise.all(promises) + // tslint:disable-next-line:underscore-consistent-invocation .then(listOfInterpreters => _.flatten(listOfInterpreters)) .then(interpreters => interpreters.filter(item => item.length > 0)) .then(interpreters => Promise.all(interpreters.map(interpreter => this.getInterpreterDetails(interpreter)))); @@ -40,14 +45,14 @@ export function getKnownSearchPathsForInterpreters(): string[] { if (IS_WINDOWS) { return []; } else { - let paths = ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/sbin']; + const paths = ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/sbin']; paths.forEach(p => { - paths.push(untildify('~' + p)); + paths.push(untildify(`~${p}`)); }); // Add support for paths such as /Users/xxx/anaconda/bin. - if (process.env['HOME']) { - paths.push(path.join(process.env['HOME'], 'anaconda', 'bin')); - paths.push(path.join(process.env['HOME'], 'python', 'bin')); + if (process.env.HOME) { + paths.push(path.join(process.env.HOME, 'anaconda', 'bin')); + paths.push(path.join(process.env.HOME, 'python', 'bin')); } return paths; } diff --git a/src/client/interpreter/locators/services/condaEnvFileService.ts b/src/client/interpreter/locators/services/condaEnvFileService.ts index 5a8c010a961b..1383d571088b 100644 --- a/src/client/interpreter/locators/services/condaEnvFileService.ts +++ b/src/client/interpreter/locators/services/condaEnvFileService.ts @@ -1,24 +1,26 @@ -"use strict"; +'use strict'; import * as fs from 'fs-extra'; import * as path from 'path'; +import { Uri } from 'vscode'; import { IS_WINDOWS } from '../../../common/configSettings'; -import { IInterpreterVersionService } from '../../interpreterVersion'; import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; -import { AnacondaDisplayName, AnacondaCompanyName, AnacondaCompanyNames, CONDA_RELATIVE_PY_PATH } from './conda'; +import { IInterpreterVersionService } from '../../interpreterVersion'; +import { AnacondaCompanyName, AnacondaCompanyNames, AnacondaDisplayName, CONDA_RELATIVE_PY_PATH } from './conda'; export class CondaEnvFileService implements IInterpreterLocatorService { constructor(private condaEnvironmentFile: string, private versionService: IInterpreterVersionService) { } - public getInterpreters() { + public async getInterpreters(_?: Uri) { return this.getSuggestionsFromConda(); } - - private getSuggestionsFromConda(): Promise { + // tslint:disable-next-line:no-empty + public dispose() { } + private async getSuggestionsFromConda(): Promise { return fs.pathExists(this.condaEnvironmentFile) .then(exists => exists ? this.getEnvironmentsFromFile(this.condaEnvironmentFile) : Promise.resolve([])); } - private getEnvironmentsFromFile(envFile: string) { + private async getEnvironmentsFromFile(envFile: string) { return fs.readFile(envFile) .then(buffer => buffer.toString().split(/\r?\n/g)) .then(lines => lines.map(line => line.trim())) @@ -29,11 +31,12 @@ export class CondaEnvFileService implements IInterpreterLocatorService { .then(interpreterPaths => interpreterPaths.map(item => this.getInterpreterDetails(item))) .then(promises => Promise.all(promises)); } - private getInterpreterDetails(interpreter: string) { + private async getInterpreterDetails(interpreter: string) { return this.versionService.getVersion(interpreter, path.basename(interpreter)) .then(version => { version = this.stripCompanyName(version); const envName = this.getEnvironmentRootDirectory(interpreter); + // tslint:disable-next-line:no-unnecessary-local-variable const info: PythonInterpreter = { displayName: `${AnacondaDisplayName} ${version} (${envName})`, path: interpreter, diff --git a/src/client/interpreter/locators/services/condaEnvService.ts b/src/client/interpreter/locators/services/condaEnvService.ts index a11cdc54f64b..0b7b281efff1 100644 --- a/src/client/interpreter/locators/services/condaEnvService.ts +++ b/src/client/interpreter/locators/services/condaEnvService.ts @@ -1,48 +1,25 @@ -"use strict"; +'use strict'; import * as child_process from 'child_process'; import * as fs from 'fs-extra'; import * as path from 'path'; +import { Uri } from 'vscode'; +import { VersionUtils } from '../../../common/versionUtils'; import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; -import { VersionUtils } from "../../../common/versionUtils"; import { AnacondaCompanyName, AnacondaDisplayName, CONDA_RELATIVE_PY_PATH } from './conda'; type CondaInfo = { envs?: string[]; - "sys.version"?: string; + 'sys.version'?: string; default_prefix?: string; -} +}; export class CondaEnvService implements IInterpreterLocatorService { constructor(private registryLookupForConda?: IInterpreterLocatorService) { } - public getInterpreters() { + public getInterpreters(resource?: Uri) { return this.getSuggestionsFromConda(); } - - private getSuggestionsFromConda(): Promise { - return this.getCondaFile() - .then(condaFile => { - return new Promise((resolve, reject) => { - // interrogate conda (if it's on the path) to find all environments. - child_process.execFile(condaFile, ['info', '--json'], (_, stdout) => { - if (stdout.length === 0) { - return resolve([]); - } - - try { - const info = JSON.parse(stdout); - resolve(this.parseCondaInfo(info)); - } catch (e) { - // Failed because either: - // 1. conda is not installed. - // 2. `conda info --json` has changed signature. - // 3. output of `conda info --json` has changed in structure. - // In all cases, we can't offer conda pythonPath suggestions. - return resolve([]); - } - }); - }); - }); - } + // tslint:disable-next-line:no-empty + public dispose() { } public getCondaFile() { if (this.registryLookupForConda) { return this.registryLookupForConda.getInterpreters() @@ -63,6 +40,7 @@ export class CondaEnvService implements IInterpreterLocatorService { } public getLatestVersion(interpreters: PythonInterpreter[]) { const sortedInterpreters = interpreters.filter(interpreter => interpreter.version && interpreter.version.length > 0); + // tslint:disable-next-line:no-non-null-assertion sortedInterpreters.sort((a, b) => VersionUtils.compareVersion(a.version!, b.version!)); if (sortedInterpreters.length > 0) { return sortedInterpreters[sortedInterpreters.length - 1]; @@ -83,6 +61,7 @@ export class CondaEnvService implements IInterpreterLocatorService { .map(env => { // If it is an environment, hence suffix with env name. const interpreterDisplayName = env === info.default_prefix ? displayName : `${displayName} (${path.basename(env)})`; + // tslint:disable-next-line:no-unnecessary-local-variable const interpreter: PythonInterpreter = { path: path.join(env, ...CONDA_RELATIVE_PY_PATH), displayName: interpreterDisplayName, @@ -94,8 +73,34 @@ export class CondaEnvService implements IInterpreterLocatorService { return Promise.all(promises) .then(interpreters => interpreters.filter(interpreter => interpreter !== null && interpreter !== undefined)) + // tslint:disable-next-line:no-non-null-assertion .then(interpreters => interpreters.map(interpreter => interpreter!)); } + private getSuggestionsFromConda(): Promise { + return this.getCondaFile() + .then(condaFile => { + return new Promise((resolve, reject) => { + // interrogate conda (if it's on the path) to find all environments. + child_process.execFile(condaFile, ['info', '--json'], (_, stdout) => { + if (stdout.length === 0) { + return resolve([]); + } + + try { + const info = JSON.parse(stdout); + resolve(this.parseCondaInfo(info)); + } catch (e) { + // Failed because either: + // 1. conda is not installed. + // 2. `conda info --json` has changed signature. + // 3. output of `conda info --json` has changed in structure. + // In all cases, we can't offer conda pythonPath suggestions. + return resolve([]); + } + }); + }); + }); + } private getDisplayNameFromVersionInfo(versionInfo: string = '') { if (!versionInfo) { return AnacondaDisplayName; diff --git a/src/client/interpreter/locators/services/currentPathService.ts b/src/client/interpreter/locators/services/currentPathService.ts index e2ddf45e4fba..55b6c4728fe9 100644 --- a/src/client/interpreter/locators/services/currentPathService.ts +++ b/src/client/interpreter/locators/services/currentPathService.ts @@ -1,36 +1,39 @@ -"use strict"; +'use strict'; +import * as child_process from 'child_process'; import * as _ from 'lodash'; import * as path from 'path'; -import * as child_process from 'child_process'; +import { Uri } from 'vscode'; +import { PythonSettings } from '../../../common/configSettings'; import { IInterpreterLocatorService } from '../../contracts'; -import { IInterpreterVersionService } from '../../interpreterVersion'; import { getFirstNonEmptyLineFromMultilineString } from '../../helpers'; +import { IInterpreterVersionService } from '../../interpreterVersion'; import { VirtualEnvironmentManager } from '../../virtualEnvs'; -import { PythonSettings } from '../../../common/configSettings'; - -const settings = PythonSettings.getInstance(); export class CurrentPathService implements IInterpreterLocatorService { public constructor(private virtualEnvMgr: VirtualEnvironmentManager, private versionProvider: IInterpreterVersionService) { } - public getInterpreters() { + public async getInterpreters(resource?: Uri) { return this.suggestionsFromKnownPaths(); } - - private suggestionsFromKnownPaths() { - const currentPythonInterpreter = this.getInterpreter(settings.pythonPath, '').then(interpreter => [interpreter]); + // tslint:disable-next-line:no-empty + public dispose() { } + private async suggestionsFromKnownPaths(resource?: Uri) { + const currentPythonInterpreter = this.getInterpreter(PythonSettings.getInstance(resource).pythonPath, '').then(interpreter => [interpreter]); const python = this.getInterpreter('python', '').then(interpreter => [interpreter]); const python2 = this.getInterpreter('python2', '').then(interpreter => [interpreter]); const python3 = this.getInterpreter('python3', '').then(interpreter => [interpreter]); return Promise.all([currentPythonInterpreter, python, python2, python3]) + // tslint:disable-next-line:underscore-consistent-invocation .then(listOfInterpreters => _.flatten(listOfInterpreters)) .then(interpreters => interpreters.filter(item => item.length > 0)) + // tslint:disable-next-line:promise-function-async .then(interpreters => Promise.all(interpreters.map(interpreter => this.getInterpreterDetails(interpreter)))); } - private getInterpreterDetails(interpreter: string) { - const virtualEnv = this.virtualEnvMgr.detect(interpreter); - const displayName = this.versionProvider.getVersion(interpreter, path.basename(interpreter)); - return Promise.all([displayName, virtualEnv]) + private async getInterpreterDetails(interpreter: string) { + return Promise.all([ + this.versionProvider.getVersion(interpreter, path.basename(interpreter)), + this.virtualEnvMgr.detect(interpreter) + ]) .then(([displayName, virtualEnv]) => { displayName += virtualEnv ? ` (${virtualEnv.name})` : ''; return { @@ -39,9 +42,10 @@ export class CurrentPathService implements IInterpreterLocatorService { }; }); } - private getInterpreter(pythonPath: string, defaultValue: string) { + private async getInterpreter(pythonPath: string, defaultValue: string) { return new Promise(resolve => { - child_process.execFile(pythonPath, ["-c", "import sys;print(sys.executable)"], (_, stdout) => { + // tslint:disable-next-line:variable-name + child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_err, stdout) => { resolve(getFirstNonEmptyLineFromMultilineString(stdout)); }); }) diff --git a/src/client/interpreter/locators/services/virtualEnvService.ts b/src/client/interpreter/locators/services/virtualEnvService.ts index 64e3b7a3d4e9..d7430dad07ad 100644 --- a/src/client/interpreter/locators/services/virtualEnvService.ts +++ b/src/client/interpreter/locators/services/virtualEnvService.ts @@ -1,32 +1,36 @@ -"use strict"; +'use strict'; import * as _ from 'lodash'; import * as path from 'path'; -import * as settings from './../../../common/configSettings'; -import { VirtualEnvironmentManager } from '../../virtualEnvs'; +import { Uri, workspace } from 'vscode'; +import { fsReaddirAsync, IS_WINDOWS } from '../../../common/utils'; import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; import { IInterpreterVersionService } from '../../interpreterVersion'; -import { IS_WINDOWS, fsReaddirAsync } from "../../../common/utils"; +import { VirtualEnvironmentManager } from '../../virtualEnvs'; import { lookForInterpretersInDirectory } from '../helpers'; -import { workspace } from 'vscode'; +import * as settings from './../../../common/configSettings'; +// tslint:disable-next-line:no-require-imports no-var-requires const untildify = require('untildify'); export class VirtualEnvService implements IInterpreterLocatorService { public constructor(private knownSearchPaths: string[], private virtualEnvMgr: VirtualEnvironmentManager, private versionProvider: IInterpreterVersionService) { } - public getInterpreters() { + public async getInterpreters(resource?: Uri) { return this.suggestionsFromKnownVenvs(); } - - private suggestionsFromKnownVenvs() { + // tslint:disable-next-line:no-empty + public dispose() { } + private async suggestionsFromKnownVenvs() { return Promise.all(this.knownSearchPaths.map(dir => this.lookForInterpretersInVenvs(dir))) + // tslint:disable-next-line:underscore-consistent-invocation .then(listOfInterpreters => _.flatten(listOfInterpreters)); } - private lookForInterpretersInVenvs(pathToCheck: string) { + private async lookForInterpretersInVenvs(pathToCheck: string) { return fsReaddirAsync(pathToCheck) .then(subDirs => Promise.all(this.getProspectiveDirectoriesForLookup(subDirs))) .then(dirs => dirs.filter(dir => dir.length > 0)) - .then(dirs => Promise.all(dirs.map(dir => lookForInterpretersInDirectory(dir)))) + .then(dirs => Promise.all(dirs.map(lookForInterpretersInDirectory))) + // tslint:disable-next-line:underscore-consistent-invocation .then(pathsWithInterpreters => _.flatten(pathsWithInterpreters)) .then(interpreters => Promise.all(interpreters.map(interpreter => this.getVirtualEnvDetails(interpreter)))); } @@ -41,9 +45,10 @@ export class VirtualEnvService implements IInterpreterLocatorService { })); } private async getVirtualEnvDetails(interpreter: string): Promise { - const displayName = this.versionProvider.getVersion(interpreter, path.basename(interpreter)); - const virtualEnv = this.virtualEnvMgr.detect(interpreter); - return Promise.all([displayName, virtualEnv]) + return Promise.all([ + this.versionProvider.getVersion(interpreter, path.basename(interpreter)), + this.virtualEnvMgr.detect(interpreter) + ]) .then(([displayName, virtualEnv]) => { const virtualEnvSuffix = virtualEnv ? virtualEnv.name : this.getVirtualEnvironmentRootDirectory(interpreter); return { @@ -57,20 +62,20 @@ export class VirtualEnvService implements IInterpreterLocatorService { } } -export function getKnownSearchPathsForVirtualEnvs(): string[] { +export function getKnownSearchPathsForVirtualEnvs(resource?: Uri): string[] { const paths: string[] = []; if (!IS_WINDOWS) { const defaultPaths = ['/Envs', '/.virtualenvs', '/.pyenv', '/.pyenv/versions']; defaultPaths.forEach(p => { - paths.push(untildify('~' + p)); + paths.push(untildify(`~${p}`)); }); } - const venvPath = settings.PythonSettings.getInstance().venvPath; + const venvPath = settings.PythonSettings.getInstance(resource).venvPath; if (venvPath) { paths.push(untildify(venvPath)); } - if (workspace.rootPath) { - paths.push(workspace.rootPath); + if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length === 0) { + paths.push(workspace.workspaceFolders[0].uri.fsPath); } return paths; -} \ No newline at end of file +} diff --git a/src/client/interpreter/locators/services/windowsRegistryService.ts b/src/client/interpreter/locators/services/windowsRegistryService.ts index 222c88e7139d..de01c5bfadce 100644 --- a/src/client/interpreter/locators/services/windowsRegistryService.ts +++ b/src/client/interpreter/locators/services/windowsRegistryService.ts @@ -1,13 +1,18 @@ -import * as path from 'path'; -import * as _ from 'lodash'; import * as fs from 'fs-extra'; +import * as _ from 'lodash'; +import * as path from 'path'; +import { Uri } from 'vscode'; import { Architecture, Hive, IRegistry } from '../../../common/registry'; import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; +// tslint:disable-next-line:variable-name const DefaultPythonExecutable = 'python.exe'; +// tslint:disable-next-line:variable-name const CompaniesToIgnore = ['PYLAUNCHER']; -const PythonCoreCompanyDisplayName = "Python Software Foundation"; -const PythonCoreComany = "PYTHONCORE"; +// tslint:disable-next-line:variable-name +const PythonCoreCompanyDisplayName = 'Python Software Foundation'; +// tslint:disable-next-line:variable-name +const PythonCoreComany = 'PYTHONCORE'; type CompanyInterpreter = { companyKey: string, @@ -19,9 +24,12 @@ export class WindowsRegistryService implements IInterpreterLocatorService { constructor(private registry: IRegistry, private is64Bit: boolean) { } - public getInterpreters() { + // tslint:disable-next-line:variable-name + public getInterpreters(_resource?: Uri) { return this.getInterpretersFromRegistry(); } + // tslint:disable-next-line:no-empty + public dispose() { } private async getInterpretersFromRegistry() { // https://github.com/python/peps/blob/master/pep-0514.txt#L357 const hkcuArch = this.is64Bit ? undefined : Architecture.x86; @@ -35,14 +43,17 @@ export class WindowsRegistryService implements IInterpreterLocatorService { } const companies = await Promise.all(promises); + // tslint:disable-next-line:underscore-consistent-invocation const companyInterpreters = await Promise.all(_.flatten(companies) .filter(item => item !== undefined && item !== null) .map(company => { return this.getInterpretersForCompany(company.companyKey, company.hive, company.arch); })); + // tslint:disable-next-line:underscore-consistent-invocation return _.flatten(companyInterpreters) .filter(item => item !== undefined && item !== null) + // tslint:disable-next-line:no-non-null-assertion .map(item => item!) .reduce((prev, current) => { if (prev.findIndex(item => item.path.toUpperCase() === current.path.toUpperCase()) === -1) { @@ -52,7 +63,7 @@ export class WindowsRegistryService implements IInterpreterLocatorService { }, []); } private async getCompanies(hive: Hive, arch?: Architecture): Promise { - return this.registry.getKeys(`\\Software\\Python`, hive, arch) + return this.registry.getKeys('\\Software\\Python', hive, arch) .then(companyKeys => companyKeys .filter(companyKey => CompaniesToIgnore.indexOf(path.basename(companyKey).toUpperCase()) === -1) .map(companyKey => { @@ -84,12 +95,14 @@ export class WindowsRegistryService implements IInterpreterLocatorService { return Promise.all([ Promise.resolve(installPath), this.registry.getValue(key, hive, arch, 'ExecutablePath'), - this.getInterpreterDisplayName(tagKey, companyKey, hive, arch)!, - this.registry.getValue(tagKey, hive, arch, 'Version')!, - this.getCompanyDisplayName(companyKey, hive, arch)! + // tslint:disable-next-line:no-non-null-assertion + this.getInterpreterDisplayName(tagKey, companyKey, hive, arch), + this.registry.getValue(tagKey, hive, arch, 'Version'), + this.getCompanyDisplayName(companyKey, hive, arch) ]) - .then(([installPath, executablePath, displayName, version, companyDisplayName]) => { - return { installPath, executablePath, displayName, version, companyDisplayName } as InterpreterInformation; + .then(([installedPath, executablePath, displayName, version, companyDisplayName]) => { + // tslint:disable-next-line:prefer-type-cast + return { installPath: installedPath, executablePath, displayName, version, companyDisplayName } as InterpreterInformation; }); }) .then((interpreterInfo?: InterpreterInformation) => { @@ -100,6 +113,7 @@ export class WindowsRegistryService implements IInterpreterLocatorService { const executablePath = interpreterInfo.executablePath && interpreterInfo.executablePath.length > 0 ? interpreterInfo.executablePath : path.join(interpreterInfo.installPath, DefaultPythonExecutable); const displayName = interpreterInfo.displayName; const version = interpreterInfo.version ? path.basename(interpreterInfo.version) : path.basename(tagKey); + // tslint:disable-next-line:prefer-type-cast return { architecture: arch, displayName, @@ -129,4 +143,4 @@ export class WindowsRegistryService implements IInterpreterLocatorService { const company = path.basename(companyKey); return company.toUpperCase() === PythonCoreComany ? PythonCoreCompanyDisplayName : company; } -} \ No newline at end of file +} diff --git a/src/client/jedi/main.ts b/src/client/jedi/main.ts index 9d2055f8961d..6586a1e27eb4 100644 --- a/src/client/jedi/main.ts +++ b/src/client/jedi/main.ts @@ -1,10 +1,10 @@ "use strict"; -import { SocketClient } from './socketClient'; -import { SocketServer } from '../common/comms/socketServer'; import * as child_process from 'child_process'; import * as path from 'path'; import * as vscode from 'vscode'; +import { SocketClient } from './socketClient'; +import { SocketServer } from '../common/comms/socketServer'; import { createDeferred, Deferred } from '../common/helpers'; import { PythonSettings } from '../common/configSettings'; import { EventEmitter } from 'events'; @@ -80,7 +80,8 @@ export class ClientAdapter extends EventEmitter { this.startSocketServer().then(port => { const def = createDeferred(); const options = { env: newEnv, cwd: this.rootDir }; - this.process = child_process.spawn(PythonSettings.getInstance().pythonPath, [pyFile, port.toString()], options); + const rootDirUri = this.rootDir ? vscode.Uri.file(this.rootDir) : undefined; + this.process = child_process.spawn(PythonSettings.getInstance(rootDirUri).pythonPath, [pyFile, port.toString()], options); this.process.stdout.setEncoding('utf8'); this.process.stderr.setEncoding('utf8'); diff --git a/src/client/jedi/parsers/CompletionParser.ts b/src/client/jedi/parsers/CompletionParser.ts index 8f7368e5e4d9..eedc47842730 100644 --- a/src/client/jedi/parsers/CompletionParser.ts +++ b/src/client/jedi/parsers/CompletionParser.ts @@ -1,12 +1,10 @@ -import { CompletionItem, SymbolKind, SnippetString } from 'vscode'; import * as proxy from '../../providers/jediProxy'; import { extractSignatureAndDocumentation } from '../../providers/jediHelpers'; import { PythonSettings } from '../../common/configSettings'; - -const pythonSettings = PythonSettings.getInstance(); +import { CompletionItem, SymbolKind, SnippetString, Uri } from 'vscode'; export class CompletionParser { - public static parse(data: proxy.ICompletionResult): CompletionItem[] { + public static parse(data: proxy.ICompletionResult, resource: Uri): CompletionItem[] { if (!data || data.items.length === 0) { return []; } @@ -16,7 +14,7 @@ export class CompletionParser { completionItem.kind = item.type; completionItem.documentation = sigAndDocs[1].length === 0 ? item.description : sigAndDocs[1]; completionItem.detail = sigAndDocs[0].split(/\r?\n/).join(''); - if (pythonSettings.autoComplete.addBrackets === true && + if (PythonSettings.getInstance(resource).autoComplete.addBrackets === true && (item.kind === SymbolKind.Function || item.kind === SymbolKind.Method)) { completionItem.insertText = new SnippetString(item.text).appendText("(").appendTabstop().appendText(")"); } diff --git a/src/client/jupyter/jupyter_client/main.ts b/src/client/jupyter/jupyter_client/main.ts index 8d715fa6940a..76ef4b6f4dfa 100644 --- a/src/client/jupyter/jupyter_client/main.ts +++ b/src/client/jupyter/jupyter_client/main.ts @@ -70,7 +70,7 @@ export class JupyterClientAdapter extends EventEmitter implements IJupyterClient let processStarted = false; let handshakeDone = false; - let isInTestRun = newEnv['PYTHON_DONJAYAMANNE_TEST'] === "1"; + let isInTestRun = newEnv['VSC_PYTHON_CI_TEST'] === "1"; const testDef = createDeferred(); const promiseToResolve = isInTestRun ? testDef.resolve.bind(testDef) : def.resolve.bind(def); diff --git a/src/client/languageServices/jediProxyFactory.ts b/src/client/languageServices/jediProxyFactory.ts new file mode 100644 index 000000000000..9af0b012d648 --- /dev/null +++ b/src/client/languageServices/jediProxyFactory.ts @@ -0,0 +1,38 @@ +import { Disposable, Uri, workspace } from 'vscode'; +import { JediProxy, JediProxyHandler, ICommandResult } from '../providers/jediProxy'; + +export class JediFactory implements Disposable { + private disposables: Disposable[]; + private jediProxyHandlers: Map>; + + constructor(private extensionRootPath: string) { + this.disposables = []; + this.jediProxyHandlers = new Map>(); + } + + public dispose() { + this.disposables.forEach(disposable => disposable.dispose()); + this.disposables = []; + } + public getJediProxyHandler(resource: Uri): JediProxyHandler { + const workspaceFolder = workspace.getWorkspaceFolder(resource); + let workspacePath = workspaceFolder ? workspaceFolder.uri.fsPath : undefined; + if (!workspacePath) { + if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + workspacePath = workspace.workspaceFolders[0].uri.fsPath; + } + else { + workspacePath = __dirname; + } + } + + if (!this.jediProxyHandlers.has(workspacePath)) { + const jediProxy = new JediProxy(this.extensionRootPath, workspacePath); + const jediProxyHandler = new JediProxyHandler(jediProxy); + this.disposables.push(jediProxy, jediProxyHandler); + this.jediProxyHandlers.set(workspacePath, jediProxyHandler); + } + // tslint:disable-next-line:no-non-null-assertion + return this.jediProxyHandlers.get(workspacePath)! as JediProxyHandler; + } +} diff --git a/src/client/linters/baseLinter.ts b/src/client/linters/baseLinter.ts index 9e1dcd8a54f5..46c89d16dff9 100644 --- a/src/client/linters/baseLinter.ts +++ b/src/client/linters/baseLinter.ts @@ -1,7 +1,7 @@ 'use strict'; import { IPythonSettings, PythonSettings } from '../common/configSettings'; import { execPythonFile } from './../common/utils'; -import { OutputChannel } from 'vscode'; +import { OutputChannel, Uri } from 'vscode'; import { Installer, Product } from '../common/installer'; import * as vscode from 'vscode'; import { ErrorHandler } from './errorHandlers/main'; @@ -131,7 +131,7 @@ export abstract class BaseLinter { this.outputChannel.append(data); } protected run(command: string, args: string[], document: vscode.TextDocument, cwd: string, cancellation: vscode.CancellationToken, regEx: string = REGEX): Promise { - return execPythonFile(command, args, cwd, true, null, cancellation).then(data => { + return execPythonFile(document.uri, command, args, cwd, true, null, cancellation).then(data => { if (!data) { data = ''; } @@ -139,12 +139,12 @@ export abstract class BaseLinter { let outputLines = data.split(/\r?\n/g); return this.parseLines(outputLines, regEx); }).catch(error => { - this.handleError(this.Id, command, error); + this.handleError(this.Id, command, error, document.uri); return []; }); } - protected handleError(expectedFileName: string, fileName: string, error: Error) { - this._errorHandler.handleError(expectedFileName, fileName, error); + protected handleError(expectedFileName: string, fileName: string, error: Error, resource: Uri) { + this._errorHandler.handleError(expectedFileName, fileName, error, resource); } } diff --git a/src/client/linters/errorHandlers/invalidArgs.ts b/src/client/linters/errorHandlers/invalidArgs.ts index 514b2b82e523..b2d4b155cadb 100644 --- a/src/client/linters/errorHandlers/invalidArgs.ts +++ b/src/client/linters/errorHandlers/invalidArgs.ts @@ -1,6 +1,6 @@ 'use strict'; import { isNotInstalledError } from '../../common/helpers'; -import * as vscode from 'vscode'; +import { Uri, window } from 'vscode'; import { StandardErrorHandler } from './standard'; export class InvalidArgumentsErrorHandler extends StandardErrorHandler { @@ -13,14 +13,14 @@ export class InvalidArgumentsErrorHandler extends StandardErrorHandler { private displayInvalidArgsError() { // Ok if we have a space after the file name, this means we have some arguments defined and this isn't supported - vscode.window.showErrorMessage(`Unsupported configuration for '${this.id}'`, 'View Errors').then(item => { + window.showErrorMessage(`Unsupported configuration for '${this.id}'`, 'View Errors').then(item => { if (item === 'View Errors') { this.outputChannel.show(); } }); } - public handleError(expectedFileName: string, fileName: string, error: Error): boolean { + public handleError(expectedFileName: string, fileName: string, error: Error, resource?: Uri): boolean { if (!isNotInstalledError(error)) { return false; } diff --git a/src/client/linters/errorHandlers/main.ts b/src/client/linters/errorHandlers/main.ts index 9503e01e4a69..e8b9c4d82b35 100644 --- a/src/client/linters/errorHandlers/main.ts +++ b/src/client/linters/errorHandlers/main.ts @@ -1,11 +1,12 @@ 'use strict'; -import { OutputChannel } from 'vscode'; +import { OutputChannel, Uri } from 'vscode'; import { Installer, Product } from '../../common/installer'; import { InvalidArgumentsErrorHandler } from './invalidArgs'; -import { StandardErrorHandler } from './standard'; import { NotInstalledErrorHandler } from './notInstalled'; +import { StandardErrorHandler } from './standard'; export class ErrorHandler { + // tslint:disable-next-line:variable-name private _errorHandlers: StandardErrorHandler[] = []; constructor(protected id: string, protected product: Product, protected installer: Installer, protected outputChannel: OutputChannel) { this._errorHandlers = [ @@ -15,7 +16,7 @@ export class ErrorHandler { ]; } - public handleError(expectedFileName: string, fileName: string, error: Error) { - this._errorHandlers.some(handler => handler.handleError(expectedFileName, fileName, error)); + public handleError(expectedFileName: string, fileName: string, error: Error, resource: Uri) { + this._errorHandlers.some(handler => handler.handleError(expectedFileName, fileName, error, resource)); } -} \ No newline at end of file +} diff --git a/src/client/linters/errorHandlers/notInstalled.ts b/src/client/linters/errorHandlers/notInstalled.ts index bc7c676a9390..f875997b184f 100644 --- a/src/client/linters/errorHandlers/notInstalled.ts +++ b/src/client/linters/errorHandlers/notInstalled.ts @@ -1,14 +1,15 @@ 'use strict'; +import { Uri } from 'vscode'; import { isNotInstalledError } from '../../common/helpers'; import { StandardErrorHandler } from './standard'; export class NotInstalledErrorHandler extends StandardErrorHandler { - public handleError(expectedFileName: string, fileName: string, error: Error): boolean { + public handleError(expectedFileName: string, fileName: string, error: Error, resource?: Uri): boolean { if (!isNotInstalledError(error)) { return false; } - this.installer.promptToInstall(this.product); + this.installer.promptToInstall(this.product, resource); const customError = `Linting with ${this.id} failed.\nYou could either install the '${this.id}' linter or turn it off in setings.json via "python.linting.${this.id}Enabled = false".`; this.outputChannel.appendLine(`\n${customError}\n${error + ''}`); return true; diff --git a/src/client/linters/errorHandlers/standard.ts b/src/client/linters/errorHandlers/standard.ts index 114d99e5c79b..966f8afa41a9 100644 --- a/src/client/linters/errorHandlers/standard.ts +++ b/src/client/linters/errorHandlers/standard.ts @@ -1,18 +1,17 @@ 'use strict'; -import { OutputChannel } from 'vscode'; -import { Installer, Product, disableLinter } from '../../common/installer'; -import * as vscode from 'vscode'; +import { OutputChannel, Uri, window } from 'vscode'; +import { Installer, Product } from '../../common/installer'; export class StandardErrorHandler { constructor(protected id: string, protected product: Product, protected installer: Installer, protected outputChannel: OutputChannel) { } - private displayLinterError() { + private displayLinterError(resource: Uri) { const message = `There was an error in running the linter '${this.id}'`; - vscode.window.showErrorMessage(message, 'Disable linter', 'View Errors').then(item => { + window.showErrorMessage(message, 'Disable linter', 'View Errors').then(item => { switch (item) { case 'Disable linter': { - disableLinter(this.product); + this.installer.disableLinter(this.product, resource); break; } case 'View Errors': { @@ -23,7 +22,7 @@ export class StandardErrorHandler { }); } - public handleError(expectedFileName: string, fileName: string, error: Error): boolean { + public handleError(expectedFileName: string, fileName: string, error: Error, resource: Uri): boolean { if (typeof error === 'string' && (error as string).indexOf("OSError: [Errno 2] No such file or directory: '/") > 0) { return false; } @@ -31,7 +30,7 @@ export class StandardErrorHandler { console.error(error); this.outputChannel.appendLine(`Linting with ${this.id} failed.\n${error + ''}`); - this.displayLinterError(); + this.displayLinterError(resource); return true; } } diff --git a/src/client/linters/prospector.ts b/src/client/linters/prospector.ts index e02a10f210be..2fb44b7d9449 100644 --- a/src/client/linters/prospector.ts +++ b/src/client/linters/prospector.ts @@ -36,14 +36,14 @@ export class Linter extends baseLinter.BaseLinter { let prospectorPath = this.pythonSettings.linting.prospectorPath; let outputChannel = this.outputChannel; let prospectorArgs = Array.isArray(this.pythonSettings.linting.prospectorArgs) ? this.pythonSettings.linting.prospectorArgs : []; - - if (prospectorArgs.length === 0 && ProductExecutableAndArgs.has(Product.prospector) && prospectorPath.toLocaleLowerCase() === 'prospector'){ + + if (prospectorArgs.length === 0 && ProductExecutableAndArgs.has(Product.prospector) && prospectorPath.toLocaleLowerCase() === 'prospector') { prospectorPath = ProductExecutableAndArgs.get(Product.prospector).executable; prospectorArgs = ProductExecutableAndArgs.get(Product.prospector).args; } return new Promise((resolve, reject) => { - execPythonFile(prospectorPath, prospectorArgs.concat(['--absolute-paths', '--output-format=json', document.uri.fsPath]), this.getWorkspaceRootPath(document), false, null, cancellation).then(data => { + execPythonFile(document.uri, prospectorPath, prospectorArgs.concat(['--absolute-paths', '--output-format=json', document.uri.fsPath]), this.getWorkspaceRootPath(document), false, null, cancellation).then(data => { let parsedData: IProspectorResponse; try { parsedData = JSON.parse(data); @@ -70,7 +70,7 @@ export class Linter extends baseLinter.BaseLinter { resolve(diagnostics); }).catch(error => { - this.handleError(this.Id, prospectorPath, error); + this.handleError(this.Id, prospectorPath, error, document.uri); resolve([]); }); }); diff --git a/src/client/linters/pydocstyle.ts b/src/client/linters/pydocstyle.ts index e58fa4edd5a5..81f2cdbd029f 100644 --- a/src/client/linters/pydocstyle.ts +++ b/src/client/linters/pydocstyle.ts @@ -42,7 +42,7 @@ export class Linter extends baseLinter.BaseLinter { let outputChannel = this.outputChannel; return new Promise((resolve, reject) => { - execPythonFile(commandLine, args, this.getWorkspaceRootPath(document), true, null, cancellation).then(data => { + execPythonFile(document.uri, commandLine, args, this.getWorkspaceRootPath(document), true, null, cancellation).then(data => { outputChannel.append('#'.repeat(10) + 'Linting Output - ' + this.Id + '#'.repeat(10) + '\n'); outputChannel.append(data); let outputLines = data.split(/\r?\n/g); @@ -99,7 +99,7 @@ export class Linter extends baseLinter.BaseLinter { }); resolve(diagnostics); }, error => { - this.handleError(this.Id, commandLine, error); + this.handleError(this.Id, commandLine, error, document.uri); resolve([]); }); }); diff --git a/src/client/providers/completionProvider.ts b/src/client/providers/completionProvider.ts index 7bbafce3f561..a72e45a679bf 100644 --- a/src/client/providers/completionProvider.ts +++ b/src/client/providers/completionProvider.ts @@ -7,17 +7,13 @@ import * as telemetryContracts from '../common/telemetryContracts'; import { extractSignatureAndDocumentation } from './jediHelpers'; import { EOL } from 'os'; import { PythonSettings } from '../common/configSettings'; -import { SnippetString } from 'vscode'; - -const pythonSettings = PythonSettings.getInstance(); +import { SnippetString, Uri } from 'vscode'; +import { JediFactory } from '../languageServices/jediProxyFactory'; export class PythonCompletionItemProvider implements vscode.CompletionItemProvider { - private jediProxyHandler: proxy.JediProxyHandler; - public constructor(context: vscode.ExtensionContext, jediProxy: proxy.JediProxy = null) { - this.jediProxyHandler = new proxy.JediProxyHandler(context, jediProxy); - } - private static parseData(data: proxy.ICompletionResult): vscode.CompletionItem[] { + public constructor(private jediFactory: JediFactory) { } + private static parseData(data: proxy.ICompletionResult, resource: Uri): vscode.CompletionItem[] { if (data && data.items.length > 0) { return data.items.map(item => { const sigAndDocs = extractSignatureAndDocumentation(item); @@ -25,7 +21,7 @@ export class PythonCompletionItemProvider implements vscode.CompletionItemProvid completionItem.kind = item.type; completionItem.documentation = sigAndDocs[1].length === 0 ? item.description : sigAndDocs[1]; completionItem.detail = sigAndDocs[0].split(/\r?\n/).join(''); - if (pythonSettings.autoComplete.addBrackets === true && + if (PythonSettings.getInstance(resource).autoComplete.addBrackets === true && (item.kind === vscode.SymbolKind.Function || item.kind === vscode.SymbolKind.Method)) { completionItem.insertText = new SnippetString(item.text).appendText("(").appendTabstop().appendText(")"); } @@ -67,10 +63,10 @@ export class PythonCompletionItemProvider implements vscode.CompletionItemProvid }; const timer = new telemetryHelper.Delays(); - return this.jediProxyHandler.sendCommand(cmd, token).then(data => { + return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token).then(data => { timer.stop(); telemetryHelper.sendTelemetryEvent(telemetryContracts.IDE.Completion, {}, timer.toMeasures()); - const completions = PythonCompletionItemProvider.parseData(data); + const completions = PythonCompletionItemProvider.parseData(data, document.uri); return completions; }); } diff --git a/src/client/providers/definitionProvider.ts b/src/client/providers/definitionProvider.ts index f678baaf5351..9fd32b58c222 100644 --- a/src/client/providers/definitionProvider.ts +++ b/src/client/providers/definitionProvider.ts @@ -2,17 +2,11 @@ import * as vscode from 'vscode'; import * as proxy from './jediProxy'; -import * as telemetryContracts from "../common/telemetryContracts"; +import * as telemetryContracts from '../common/telemetryContracts'; +import { JediFactory } from '../languageServices/jediProxyFactory'; export class PythonDefinitionProvider implements vscode.DefinitionProvider { - private jediProxyHandler: proxy.JediProxyHandler; - public get JediProxy(): proxy.JediProxy { - return this.jediProxyHandler.JediProxy; - } - - public constructor(context: vscode.ExtensionContext) { - this.jediProxyHandler = new proxy.JediProxyHandler(context); - } + public constructor(private jediFactory: JediFactory) { } private static parseData(data: proxy.IDefinitionResult, possibleWord: string): vscode.Definition { if (data && Array.isArray(data.definitions) && data.definitions.length > 0) { const definitions = data.definitions.filter(d => d.text === possibleWord); @@ -46,7 +40,7 @@ export class PythonDefinitionProvider implements vscode.DefinitionProvider { cmd.source = document.getText(); } let possibleWord = document.getText(range); - return this.jediProxyHandler.sendCommand(cmd, token).then(data => { + return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token).then(data => { return PythonDefinitionProvider.parseData(data, possibleWord); }); } diff --git a/src/client/providers/execInTerminalProvider.ts b/src/client/providers/execInTerminalProvider.ts index e248e32bc432..c25d784dc5d2 100644 --- a/src/client/providers/execInTerminalProvider.ts +++ b/src/client/providers/execInTerminalProvider.ts @@ -1,12 +1,15 @@ 'use strict'; +import * as fs from 'fs-extra'; +import { EOL } from 'os'; +import * as path from 'path'; import * as vscode from 'vscode'; +import { Disposable, workspace } from 'vscode'; import * as settings from '../common/configSettings'; import { Commands, PythonLanguage } from '../common/constants'; -import { EOL } from 'os'; -let path = require('path'); -let terminal: vscode.Terminal; +import { ContextKey } from '../common/contextKey'; import { IS_WINDOWS } from '../common/utils'; +let terminal: vscode.Terminal; export function activateExecInTerminalProvider(): vscode.Disposable[] { const disposables: vscode.Disposable[] = []; disposables.push(vscode.commands.registerCommand(Commands.Exec_In_Terminal, execInTerminal)); @@ -17,13 +20,14 @@ export function activateExecInTerminalProvider(): vscode.Disposable[] { terminal = null; } })); + disposables.push(new DjangoContextInitializer()); return disposables; } function removeBlankLines(code: string): string { - let codeLines = code.split(/\r?\n/g); - let codeLinesWithoutEmptyLines = codeLines.filter(line => line.trim().length > 0); - let lastLineIsEmpty = codeLines.length > 0 && codeLines[codeLines.length - 1].trim().length === 0; + const codeLines = code.split(/\r?\n/g); + const codeLinesWithoutEmptyLines = codeLines.filter(line => line.trim().length > 0); + const lastLineIsEmpty = codeLines.length > 0 && codeLines[codeLines.length - 1].trim().length === 0; if (lastLineIsEmpty) { codeLinesWithoutEmptyLines.unshift(''); } @@ -31,9 +35,10 @@ function removeBlankLines(code: string): string { } function execInTerminal(fileUri?: vscode.Uri) { const terminalShellSettings = vscode.workspace.getConfiguration('terminal.integrated.shell'); + // tslint:disable-next-line:no-backbone-get-set-outside-model const IS_POWERSHELL = /powershell/.test(terminalShellSettings.get('windows')); - let pythonSettings = settings.PythonSettings.getInstance(); + const pythonSettings = settings.PythonSettings.getInstance(fileUri); let filePath: string; let currentPythonPath = pythonSettings.pythonPath; @@ -67,138 +72,191 @@ function execInTerminal(fileUri?: vscode.Uri) { filePath = `"${filePath}"`; } - terminal = terminal ? terminal : vscode.window.createTerminal(`Python`); + terminal = terminal ? terminal : vscode.window.createTerminal('Python'); if (pythonSettings.terminal && pythonSettings.terminal.executeInFileDir) { const fileDirPath = path.dirname(filePath); - if (fileDirPath !== vscode.workspace.rootPath && fileDirPath.substring(1) !== vscode.workspace.rootPath) { + const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)); + if (wkspace && fileDirPath !== wkspace.uri.fsPath && fileDirPath.substring(1) !== wkspace.uri.fsPath) { terminal.sendText(`cd "${fileDirPath}"`); } } - const launchArgs = settings.PythonSettings.getInstance().terminal.launchArgs; - const launchArgsString = launchArgs.length > 0 ? " ".concat(launchArgs.join(" ")) : ""; + const launchArgs = settings.PythonSettings.getInstance(fileUri).terminal.launchArgs; + const launchArgsString = launchArgs.length > 0 ? ' '.concat(launchArgs.join(' ')) : ''; const command = `${currentPythonPath}${launchArgsString} ${filePath}`; if (IS_WINDOWS) { - const commandWin = command.replace(/\\/g, "/"); + const commandWin = command.replace(/\\/g, '/'); if (IS_POWERSHELL) { terminal.sendText(`& ${commandWin}`); - } - else { + } else { terminal.sendText(commandWin); } - } - else { + } else { terminal.sendText(command); } terminal.show(); } function execSelectionInTerminal() { + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + return; + } + const terminalShellSettings = vscode.workspace.getConfiguration('terminal.integrated.shell'); + // tslint:disable-next-line:no-backbone-get-set-outside-model const IS_POWERSHELL = /powershell/.test(terminalShellSettings.get('windows')); - let currentPythonPath = settings.PythonSettings.getInstance().pythonPath; + let currentPythonPath = settings.PythonSettings.getInstance(activeEditor.document.uri).pythonPath; if (currentPythonPath.indexOf(' ') > 0) { currentPythonPath = `"${currentPythonPath}"`; } - const activeEditor = vscode.window.activeTextEditor; - if (!activeEditor) { - return; - } - const selection = vscode.window.activeTextEditor.selection; let code: string; if (selection.isEmpty) { code = vscode.window.activeTextEditor.document.lineAt(selection.start.line).text; - } - else { - let textRange = new vscode.Range(selection.start, selection.end); + } else { + const textRange = new vscode.Range(selection.start, selection.end); code = vscode.window.activeTextEditor.document.getText(textRange); } if (code.length === 0) { return; } code = removeBlankLines(code); - const launchArgs = settings.PythonSettings.getInstance().terminal.launchArgs; - const launchArgsString = launchArgs.length > 0 ? " ".concat(launchArgs.join(" ")) : ""; + const launchArgs = settings.PythonSettings.getInstance(activeEditor.document.uri).terminal.launchArgs; + const launchArgsString = launchArgs.length > 0 ? ' '.concat(launchArgs.join(' ')) : ''; const command = `${currentPythonPath}${launchArgsString}`; if (!terminal) { - terminal = vscode.window.createTerminal(`Python`); + terminal = vscode.window.createTerminal('Python'); if (IS_WINDOWS) { - const commandWin = command.replace(/\\/g, "/"); + const commandWin = command.replace(/\\/g, '/'); if (IS_POWERSHELL) { terminal.sendText(`& ${commandWin}`); - } - else { + } else { terminal.sendText(commandWin); } - } - else { + } else { terminal.sendText(command); } } - const unix_code = code.replace(/\r\n/g, "\n"); + // tslint:disable-next-line:variable-name + const unix_code = code.replace(/\r\n/g, '\n'); if (IS_WINDOWS) { - terminal.sendText(unix_code.replace(/\n/g, "\r\n")); - } - else { + terminal.sendText(unix_code.replace(/\n/g, '\r\n')); + } else { terminal.sendText(unix_code); } terminal.show(); } function execSelectionInDjangoShell() { + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + return; + } + const terminalShellSettings = vscode.workspace.getConfiguration('terminal.integrated.shell'); + // tslint:disable-next-line:no-backbone-get-set-outside-model const IS_POWERSHELL = /powershell/.test(terminalShellSettings.get('windows')); - let currentPythonPath = settings.PythonSettings.getInstance().pythonPath; + let currentPythonPath = settings.PythonSettings.getInstance(activeEditor.document.uri).pythonPath; if (currentPythonPath.indexOf(' ') > 0) { currentPythonPath = `"${currentPythonPath}"`; } - const activeEditor = vscode.window.activeTextEditor; - if (!activeEditor) { - return; - } - - const workspaceRoot = vscode.workspace.rootPath; - const djangoShellCmd = `"${workspaceRoot}/manage.py" shell`; + const workspaceUri = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri); + const defaultWorkspace = Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0 ? vscode.workspace.workspaceFolders[0].uri.fsPath : ''; + const workspaceRoot = workspaceUri ? workspaceUri.uri.fsPath : defaultWorkspace; + const djangoShellCmd = `"${path.join(workspaceRoot, 'manage.py')}" shell`; const selection = vscode.window.activeTextEditor.selection; let code: string; if (selection.isEmpty) { code = vscode.window.activeTextEditor.document.lineAt(selection.start.line).text; - } - else { - let textRange = new vscode.Range(selection.start, selection.end); + } else { + const textRange = new vscode.Range(selection.start, selection.end); code = vscode.window.activeTextEditor.document.getText(textRange); } if (code.length === 0) { return; } - const launchArgs = settings.PythonSettings.getInstance().terminal.launchArgs; - const launchArgsString = launchArgs.length > 0 ? " ".concat(launchArgs.join(" ")) : ""; + const launchArgs = settings.PythonSettings.getInstance(activeEditor.document.uri).terminal.launchArgs; + const launchArgsString = launchArgs.length > 0 ? ' '.concat(launchArgs.join(' ')) : ''; const command = `${currentPythonPath}${launchArgsString} ${djangoShellCmd}`; if (!terminal) { - terminal = vscode.window.createTerminal(`Django Shell`); + terminal = vscode.window.createTerminal('Django Shell'); if (IS_WINDOWS) { - const commandWin = command.replace(/\\/g, "/"); + const commandWin = command.replace(/\\/g, '/'); if (IS_POWERSHELL) { terminal.sendText(`& ${commandWin}`); - } - else { + } else { terminal.sendText(commandWin); } - } - else { + } else { terminal.sendText(command); } } - const unix_code = code.replace(/\r\n/g, "\n"); + // tslint:disable-next-line:variable-name + const unix_code = code.replace(/\r\n/g, '\n'); if (IS_WINDOWS) { - terminal.sendText(unix_code.replace(/\n/g, "\r\n")); - } - else { + terminal.sendText(unix_code.replace(/\n/g, '\r\n')); + } else { terminal.sendText(unix_code); } terminal.show(); -} \ No newline at end of file +} + +class DjangoContextInitializer implements vscode.Disposable { + private isDjangoProject: ContextKey; + private monitoringActiveTextEditor: boolean; + private workspaceContextKeyValues = new Map(); + private lastCheckedWorkspace: string; + private disposables: Disposable[] = []; + constructor() { + this.isDjangoProject = new ContextKey('python.isDjangoProject'); + this.ensureState(); + this.disposables.push(vscode.workspace.onDidChangeWorkspaceFolders(() => this.updateContextKeyBasedOnActiveWorkspace())); + } + + public dispose() { + this.isDjangoProject = null; + this.disposables.forEach(disposable => disposable.dispose()); + } + private updateContextKeyBasedOnActiveWorkspace() { + if (this.monitoringActiveTextEditor) { + return; + } + this.monitoringActiveTextEditor = true; + this.disposables.push(vscode.window.onDidChangeActiveTextEditor(() => this.ensureState())); + } + private getActiveWorkspace(): string | undefined { + if (!Array.isArray(workspace.workspaceFolders || workspace.workspaceFolders.length === 0)) { + return undefined; + } + if (workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri.fsPath; + } + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + return undefined; + } + const workspaceFolder = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri); + return workspaceFolder ? workspaceFolder.uri.fsPath : undefined; + } + private async ensureState(): Promise { + const activeWorkspace = this.getActiveWorkspace(); + if (!activeWorkspace) { + return await this.isDjangoProject.set(false); + } + if (this.lastCheckedWorkspace === activeWorkspace) { + return; + } + if (this.workspaceContextKeyValues.has(activeWorkspace)) { + await this.isDjangoProject.set(this.workspaceContextKeyValues.get(activeWorkspace)); + } else { + const exists = await fs.pathExists(path.join(activeWorkspace, 'manage.py')); + await this.isDjangoProject.set(exists); + this.workspaceContextKeyValues.set(activeWorkspace, exists); + this.lastCheckedWorkspace = activeWorkspace; + } + } +} diff --git a/src/client/providers/hoverProvider.ts b/src/client/providers/hoverProvider.ts index b6a1abf70faf..376150f298a8 100644 --- a/src/client/providers/hoverProvider.ts +++ b/src/client/providers/hoverProvider.ts @@ -4,13 +4,10 @@ import * as vscode from 'vscode'; import * as proxy from './jediProxy'; import { highlightCode } from './jediHelpers'; import { EOL } from 'os'; +import { JediFactory } from '../languageServices/jediProxyFactory'; export class PythonHoverProvider implements vscode.HoverProvider { - private jediProxyHandler: proxy.JediProxyHandler; - - public constructor(context: vscode.ExtensionContext, jediProxy: proxy.JediProxy = null) { - this.jediProxyHandler = new proxy.JediProxyHandler(context, jediProxy); - } + public constructor(private jediFactory: JediFactory) { } private static parseData(data: proxy.IHoverResult, currentWord: string): vscode.Hover { let results = []; let capturedInfo: string[] = []; @@ -96,7 +93,7 @@ export class PythonHoverProvider implements vscode.HoverProvider { cmd.source = document.getText(); } - const data = await this.jediProxyHandler.sendCommand(cmd, token); + const data = await this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token); if (!data || !data.items.length) { return; } diff --git a/src/client/providers/importSortProvider.ts b/src/client/providers/importSortProvider.ts index c3628427f48b..9084547de29f 100644 --- a/src/client/providers/importSortProvider.ts +++ b/src/client/providers/importSortProvider.ts @@ -1,57 +1,54 @@ -"use strict"; - -import * as vscode from "vscode"; -import * as path from "path"; -import * as fs from "fs"; -import * as child_process from "child_process"; -import * as settings from '../common/configSettings'; -import { getTextEditsFromPatch, getTempFileWithDocumentContents } from "../common/editor"; +'use strict'; +import * as child_process from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { PythonSettings } from '../common/configSettings'; +import { getTempFileWithDocumentContents, getTextEditsFromPatch } from '../common/editor'; +// tslint:disable-next-line:completed-docs export class PythonImportSortProvider { - public sortImports(extensionDir: string, document: vscode.TextDocument): Promise { + public async sortImports(extensionDir: string, document: vscode.TextDocument): Promise { if (document.lineCount === 1) { - return Promise.resolve([]); + return []; + } + // isort does have the ability to read from the process input stream and return the formatted code out of the output stream. + // However they don't support returning the diff of the formatted text when reading data from the input stream. + // Yes getting text formatted that way avoids having to create a temporary file, however the diffing will have + // to be done here in node (extension), i.e. extension cpu, i.e. less responsive solution. + const importScript = path.join(extensionDir, 'pythonFiles', 'sortImports.py'); + const tmpFileCreated = document.isDirty; + const filePath = tmpFileCreated ? await getTempFileWithDocumentContents(document) : document.fileName; + const settings = PythonSettings.getInstance(document.uri); + const pythonPath = settings.pythonPath; + const isort = settings.sortImports.path; + const args = settings.sortImports.args.join(' '); + let isortCmd = ''; + if (typeof isort === 'string' && isort.length > 0) { + if (isort.indexOf(' ') > 0) { + isortCmd = `"${isort}" "${filePath}" --diff ${args}`; + } else { + isortCmd = `${isort} "${filePath}" --diff ${args}`; + } + } else { + if (pythonPath.indexOf(' ') > 0) { + isortCmd = `"${pythonPath}" "${importScript}" "${filePath}" --diff ${args}`; + } else { + isortCmd = `${pythonPath} "${importScript}" "${filePath}" --diff ${args}`; + } } - return new Promise((resolve, reject) => { - // isort does have the ability to read from the process input stream and return the formatted code out of the output stream - // However they don't support returning the diff of the formatted text when reading data from the input stream - // Yes getting text formatted that way avoids having to create a temporary file, however the diffing will have - // to be done here in node (extension), i.e. extension cpu, i.e. les responsive solution - let importScript = path.join(extensionDir, "pythonFiles", "sortImports.py"); - let tmpFileCreated = document.isDirty; - let filePromise = tmpFileCreated ? getTempFileWithDocumentContents(document) : Promise.resolve(document.fileName); - filePromise.then(filePath => { - const pythonPath = settings.PythonSettings.getInstance().pythonPath; - const isort = settings.PythonSettings.getInstance().sortImports.path; - const args = settings.PythonSettings.getInstance().sortImports.args.join(' '); - let isort_cmd = ''; - if (typeof isort === 'string' && isort.length > 0) { - if (isort.indexOf(' ') > 0) { - isort_cmd = `"${isort}" "${filePath}" --diff ${args}`; - } - else { - isort_cmd = `${isort} "${filePath}" --diff ${args}`; - } + // tslint:disable-next-line:promise-must-complete + return await new Promise((resolve, reject) => { + child_process.exec(isortCmd, (error, stdout, stderr) => { + if (tmpFileCreated) { + fs.unlink(filePath); + } + if (error || (stderr && stderr.length > 0)) { + reject(error ? error : stderr); } else { - if (pythonPath.indexOf(' ') > 0) { - isort_cmd = `"${pythonPath}" "${importScript}" "${filePath}" --diff ${args}`; - } - else { - isort_cmd = `${pythonPath} "${importScript}" "${filePath}" --diff ${args}`; - } + resolve(getTextEditsFromPatch(document.getText(), stdout)); } - child_process.exec(isort_cmd, (error, stdout, stderr) => { - if (tmpFileCreated) { - fs.unlink(filePath); - } - if (error || (stderr && stderr.length > 0)) { - return reject(error ? error : stderr); - } - - let edits = getTextEditsFromPatch(document.getText(), stdout); - resolve(edits); - }); - }).catch(reject); + }); }); } } diff --git a/src/client/providers/jediProxy.ts b/src/client/providers/jediProxy.ts index 5b9220dfb223..9b01703ba13a 100644 --- a/src/client/providers/jediProxy.ts +++ b/src/client/providers/jediProxy.ts @@ -5,15 +5,14 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as settings from './../common/configSettings'; import * as logger from './../common/logger'; -import * as telemetryHelper from "../common/telemetry"; -import { execPythonFile, validatePath } from "../common/utils"; +import * as telemetryHelper from '../common/telemetry'; +import { execPythonFile, getCustomEnvVarsSync, validatePath } from '../common/utils'; import { createDeferred, Deferred } from '../common/helpers'; import { getCustomEnvVars } from '../common/utils'; import { mergeEnvVariables } from '../common/envFileParser'; +import { IPythonSettings, PythonSettings } from '../common/configSettings'; const IS_WINDOWS = /^win/.test(process.platform); -var proc: child_process.ChildProcess; -var pythonSettings = settings.PythonSettings.getInstance(); const pythonVSCodeTypeMappings = new Map(); pythonVSCodeTypeMappings.set('none', vscode.CompletionItemKind.Value); @@ -122,202 +121,250 @@ commandNames.set(CommandType.Hover, "tooltip"); commandNames.set(CommandType.Usages, "usages"); commandNames.set(CommandType.Symbols, "names"); -export class JediProxy extends vscode.Disposable { - public constructor(context: vscode.ExtensionContext) { - super(killProcess); - - context.subscriptions.push(this); - initialize(context.asAbsolutePath(".")); +export class JediProxy implements vscode.Disposable { + private proc: child_process.ChildProcess; + private pythonSettings: PythonSettings; + + public constructor(private extensionRootDir: string, private workspacePath: string) { + this.pythonSettings = PythonSettings.getInstance(vscode.Uri.file(workspacePath)); + this.lastKnownPythonInterpreter = this.pythonSettings.pythonPath + this.pythonSettings.on('change', this.onPythonSettingsChanged.bind(this)); + vscode.workspace.onDidChangeConfiguration(this.onConfigChanged.bind(this)); + this.onConfigChanged(); + this.initialize(extensionRootDir); + } + public dispose() { + this.killProcess(); } - private cmdId: number = 0; public getNextCommandId(): number { return this.cmdId++; } - public sendCommand(cmd: ICommand): Promise { - return sendCommand(cmd); - } -} -// keep track of the directory so we can re-spawn the process -let pythonProcessCWD = ""; -function initialize(dir: string) { - pythonProcessCWD = dir; - spawnProcess(path.join(dir, "pythonFiles")); -} + // keep track of the directory so we can re-spawn the process + private pythonProcessCWD = ""; + private initialize(dir: string) { + this.pythonProcessCWD = dir; + this.spawnProcess(path.join(dir, "pythonFiles")); + } -// Check if settings changes -let lastKnownPythonInterpreter = pythonSettings.pythonPath; -pythonSettings.on('change', onPythonSettingsChanged); + // Check if settings changes + private lastKnownPythonInterpreter: string; + private onPythonSettingsChanged() { + if (this.lastKnownPythonInterpreter === this.pythonSettings.pythonPath) { + return; + } + this.killProcess(); + this.clearPendingRequests(); + this.initialize(this.pythonProcessCWD); + } -function onPythonSettingsChanged() { - if (lastKnownPythonInterpreter === pythonSettings.pythonPath) { - return; + private clearPendingRequests() { + this.commandQueue = []; + this.commands.forEach(item => { + item.deferred.resolve(); + }); + this.commands.clear(); } - killProcess(); - clearPendingRequests(); - initialize(pythonProcessCWD); -} + private previousData = ""; + private commands = new Map>(); + private commandQueue: number[] = []; -function clearPendingRequests() { - commandQueue = []; - commands.forEach(item => { - item.deferred.resolve(); - }); - commands.clear(); -} -var previousData = ""; -var commands = new Map>(); -var commandQueue: number[] = []; - -function killProcess() { - try { - if (proc) { - proc.kill(); + private killProcess() { + try { + if (this.proc) { + this.proc.kill(); + } } + catch (ex) { } + this.proc = null; } - catch (ex) { } - proc = null; -} -function handleError(source: string, errorMessage: string) { - logger.error(source + ' jediProxy', `Error (${source}) ${errorMessage}`); -} + private handleError(source: string, errorMessage: string) { + logger.error(source + ' jediProxy', `Error (${source}) ${errorMessage}`); + } -let spawnRetryAttempts = 0; -function spawnProcess(dir: string) { - try { - let environmentVariables = { 'PYTHONUNBUFFERED': '1' }; - let customEnvironmentVars = getCustomEnvVars(); - if (customEnvironmentVars) { - environmentVariables = mergeEnvVariables(environmentVariables, customEnvironmentVars); - } - environmentVariables = mergeEnvVariables(environmentVariables); - - logger.log('child_process.spawn in jediProxy', 'Value of pythonSettings.pythonPath is :' + pythonSettings.pythonPath); - const args = ["completion.py"]; - if (typeof pythonSettings.jediPath !== 'string' || pythonSettings.jediPath.length === 0) { - if (Array.isArray(pythonSettings.devOptions) && - pythonSettings.devOptions.some(item => item.toUpperCase().trim() === 'USERELEASEAUTOCOMP')) { - // Use standard version of jedi library - args.push('std'); + private spawnRetryAttempts = 0; + private spawnProcess(dir: string) { + try { + let environmentVariables = { 'PYTHONUNBUFFERED': '1' }; + let customEnvironmentVars = getCustomEnvVarsSync(vscode.Uri.file(dir)); + if (customEnvironmentVars) { + environmentVariables = mergeEnvVariables(environmentVariables, customEnvironmentVars); + } + environmentVariables = mergeEnvVariables(environmentVariables); + + logger.log('child_process.spawn in jediProxy', 'Value of pythonSettings.pythonPath is :' + this.pythonSettings.pythonPath); + const args = ["completion.py"]; + if (typeof this.pythonSettings.jediPath !== 'string' || this.pythonSettings.jediPath.length === 0) { + if (Array.isArray(this.pythonSettings.devOptions) && + this.pythonSettings.devOptions.some(item => item.toUpperCase().trim() === 'USERELEASEAUTOCOMP')) { + // Use standard version of jedi library + args.push('std'); + } + else { + // Use preview version of jedi library + args.push('preview'); + } } else { - // Use preview version of jedi library - args.push('preview'); + args.push('custom'); + args.push(this.pythonSettings.jediPath); } + if (Array.isArray(this.pythonSettings.autoComplete.preloadModules) && + this.pythonSettings.autoComplete.preloadModules.length > 0) { + var modules = this.pythonSettings.autoComplete.preloadModules.filter(m => m.trim().length > 0).join(','); + args.push(modules); + } + this.proc = child_process.spawn(this.pythonSettings.pythonPath, args, { + cwd: dir, + env: environmentVariables + }); } - else { - args.push('custom'); - args.push(pythonSettings.jediPath); - } - if (Array.isArray(pythonSettings.autoComplete.preloadModules) && - pythonSettings.autoComplete.preloadModules.length > 0) { - var modules = pythonSettings.autoComplete.preloadModules.filter(m => m.trim().length > 0).join(','); - args.push(modules); + catch (ex) { + return this.handleError("spawnProcess", ex.message); } - proc = child_process.spawn(pythonSettings.pythonPath, args, { - cwd: dir, - env: environmentVariables + this.proc.stderr.setEncoding('utf8'); + this.proc.stderr.on("data", (data: string) => { + this.handleError("stderr", data); }); - } - catch (ex) { - return handleError("spawnProcess", ex.message); - } - proc.stderr.setEncoding('utf8'); - proc.stderr.on("data", (data: string) => { - handleError("stderr", data); - }); - proc.on("end", (end) => { - logger.error('spawnProcess.end', "End - " + end); - }); - proc.on("error", error => { - handleError("error", error + ''); - spawnRetryAttempts++; - if (spawnRetryAttempts < 10 && error && error.message && - error.message.indexOf('This socket has been ended by the other party') >= 0) { - spawnProcess(dir); - } - }); - proc.stdout.setEncoding('utf8'); - proc.stdout.on("data", (data: string) => { - //Possible there was an exception in parsing the data returned - //So append the data then parse it - var dataStr = previousData = previousData + data + ""; - var responses: any[]; - try { - responses = dataStr.split(/\r?\n/g).filter(line => line.length > 0).map(resp => JSON.parse(resp)); - previousData = ""; - } - catch (ex) { - // Possible we've only received part of the data, hence don't clear previousData - // Don't log errors when we haven't received the entire response - if (ex.message.indexOf('Unexpected end of input') === -1 && - ex.message.indexOf('Unexpected end of JSON input') === -1 && - ex.message.indexOf('Unexpected token') === -1) { - handleError("stdout", ex.message); + this.proc.on("end", (end) => { + logger.error('spawnProcess.end', "End - " + end); + }); + this.proc.on("error", error => { + this.handleError("error", error + ''); + this.spawnRetryAttempts++; + if (this.spawnRetryAttempts < 10 && error && error.message && + error.message.indexOf('This socket has been ended by the other party') >= 0) { + this.spawnProcess(dir); } - return; - } - - responses.forEach((response) => { - // What's this, can't remember, - // Great example of poorly written code (this whole file is a mess) - // I think this needs to be removed, because this is misspelt, it is argments, 'U' is missing - // And that case is handled further down - // case CommandType.Arguments: { - // Rewrite this mess to use stratergy.. - if (response["argments"]) { - var index = commandQueue.indexOf(cmd.id); - commandQueue.splice(index, 1); - return; + }); + this.proc.stdout.setEncoding('utf8'); + this.proc.stdout.on("data", (data: string) => { + //Possible there was an exception in parsing the data returned + //So append the data then parse it + var dataStr = this.previousData = this.previousData + data + ""; + var responses: any[]; + try { + responses = dataStr.split(/\r?\n/g).filter(line => line.length > 0).map(resp => JSON.parse(resp)); + this.previousData = ""; } - var responseId = response["id"]; - - var cmd = >commands.get(responseId); - if (typeof cmd === "object" && cmd !== null) { - commands.delete(responseId); - var index = commandQueue.indexOf(cmd.id); - commandQueue.splice(index, 1); - - if (cmd.delays && typeof cmd.telemetryEvent === 'string') { - // cmd.delays.stop(); - // telemetryHelper.sendTelemetryEvent(cmd.telemetryEvent, null, cmd.delays.toMeasures()); + catch (ex) { + // Possible we've only received part of the data, hence don't clear previousData + // Don't log errors when we haven't received the entire response + if (ex.message.indexOf('Unexpected end of input') === -1 && + ex.message.indexOf('Unexpected end of JSON input') === -1 && + ex.message.indexOf('Unexpected token') === -1) { + this.handleError("stdout", ex.message); } + return; + } - // Check if this command has expired - if (cmd.token.isCancellationRequested) { - cmd.deferred.resolve(); + responses.forEach((response) => { + // What's this, can't remember, + // Great example of poorly written code (this whole file is a mess) + // I think this needs to be removed, because this is misspelt, it is argments, 'U' is missing + // And that case is handled further down + // case CommandType.Arguments: { + // Rewrite this mess to use stratergy.. + if (response["argments"]) { + var index = this.commandQueue.indexOf(cmd.id); + this.commandQueue.splice(index, 1); return; } + var responseId = response["id"]; + + var cmd = >this.commands.get(responseId); + if (typeof cmd === "object" && cmd !== null) { + this.commands.delete(responseId); + var index = this.commandQueue.indexOf(cmd.id); + this.commandQueue.splice(index, 1); + + if (cmd.delays && typeof cmd.telemetryEvent === 'string') { + // cmd.delays.stop(); + // telemetryHelper.sendTelemetryEvent(cmd.telemetryEvent, null, cmd.delays.toMeasures()); + } - switch (cmd.command) { - case CommandType.Completions: { - let results = response['results']; - results = Array.isArray(results) ? results : []; - results.forEach(item => { - const originalType = item.type; - item.type = getMappedVSCodeType(originalType); - item.kind = getMappedVSCodeSymbol(originalType); - item.rawType = getMappedVSCodeType(originalType); - }); - - let completionResult: ICompletionResult = { - items: results, - requestId: cmd.id - }; - cmd.deferred.resolve(completionResult); - break; + // Check if this command has expired + if (cmd.token.isCancellationRequested) { + cmd.deferred.resolve(); + return; } - case CommandType.Definitions: { - let defs = response['results']; - let defResult: IDefinitionResult = { - requestId: cmd.id, - definitions: [] - }; - if (defs.length > 0) { - defResult.definitions = defs.map(def => { + + switch (cmd.command) { + case CommandType.Completions: { + let results = response['results']; + results = Array.isArray(results) ? results : []; + results.forEach(item => { + const originalType = item.type; + item.type = getMappedVSCodeType(originalType); + item.kind = getMappedVSCodeSymbol(originalType); + item.rawType = getMappedVSCodeType(originalType); + }); + + let completionResult: ICompletionResult = { + items: results, + requestId: cmd.id + }; + cmd.deferred.resolve(completionResult); + break; + } + case CommandType.Definitions: { + let defs = response['results']; + let defResult: IDefinitionResult = { + requestId: cmd.id, + definitions: [] + }; + if (defs.length > 0) { + defResult.definitions = defs.map(def => { + const originalType = def.type as string; + return { + fileName: def.fileName, + text: def.text, + rawType: originalType, + type: getMappedVSCodeType(originalType), + kind: getMappedVSCodeSymbol(originalType), + container: def.container, + range: { + startLine: def.range.start_line, + startColumn: def.range.start_column, + endLine: def.range.end_line, + endColumn: def.range.end_column + } + }; + }); + } + + cmd.deferred.resolve(defResult); + break; + } + case CommandType.Hover: { + let defs = response['results']; + var defResult: IHoverResult = { + requestId: cmd.id, + items: defs.map(def => { + return { + kind: getMappedVSCodeSymbol(def.type), + description: def.description, + signature: def.signature, + docstring: def.docstring, + text: def.text + }; + }) + }; + + cmd.deferred.resolve(defResult); + break; + } + case CommandType.Symbols: { + let defs = response['results']; + defs = Array.isArray(defs) ? defs : []; + var defResults: ISymbolResult = { + requestId: cmd.id, + definitions: [] + }; + defResults.definitions = defs.map(def => { const originalType = def.type as string; return { fileName: def.fileName, @@ -334,250 +381,201 @@ function spawnProcess(dir: string) { } }; }); - } - cmd.deferred.resolve(defResult); - break; - } - case CommandType.Hover: { - let defs = response['results']; - var defResult: IHoverResult = { - requestId: cmd.id, - items: defs.map(def => { - return { - kind: getMappedVSCodeSymbol(def.type), - description: def.description, - signature: def.signature, - docstring: def.docstring, - text: def.text - }; - }) - }; - - cmd.deferred.resolve(defResult); - break; - } - case CommandType.Symbols: { - let defs = response['results']; - defs = Array.isArray(defs) ? defs : []; - var defResults: ISymbolResult = { - requestId: cmd.id, - definitions: [] - }; - defResults.definitions = defs.map(def => { - const originalType = def.type as string; - return { - fileName: def.fileName, - text: def.text, - rawType: originalType, - type: getMappedVSCodeType(originalType), - kind: getMappedVSCodeSymbol(originalType), - container: def.container, - range: { - startLine: def.range.start_line, - startColumn: def.range.start_column, - endLine: def.range.end_line, - endColumn: def.range.end_column + cmd.deferred.resolve(defResults); + break; + } + case CommandType.Usages: { + let defs = response['results']; + defs = Array.isArray(defs) ? defs : []; + var refResult: IReferenceResult = { + requestId: cmd.id, + references: defs.map(item => { + return { + columnIndex: item.column, + fileName: item.fileName, + lineIndex: item.line - 1, + moduleName: item.moduleName, + name: item.name + }; } + ) }; - }); - - cmd.deferred.resolve(defResults); - break; - } - case CommandType.Usages: { - let defs = response['results']; - defs = Array.isArray(defs) ? defs : []; - var refResult: IReferenceResult = { - requestId: cmd.id, - references: defs.map(item => { - return { - columnIndex: item.column, - fileName: item.fileName, - lineIndex: item.line - 1, - moduleName: item.moduleName, - name: item.name - }; - } - ) - }; - cmd.deferred.resolve(refResult); - break; - } - case CommandType.Arguments: { - let defs = response["results"]; - cmd.deferred.resolve({ - requestId: cmd.id, - definitions: defs - }); - break; + cmd.deferred.resolve(refResult); + break; + } + case CommandType.Arguments: { + let defs = response["results"]; + cmd.deferred.resolve({ + requestId: cmd.id, + definitions: defs + }); + break; + } } } - } - //Ok, check if too many pending requets - if (commandQueue.length > 10) { - var items = commandQueue.splice(0, commandQueue.length - 10); - items.forEach(id => { - if (commands.has(id)) { - const cmd = commands.get(id); - try { - cmd.deferred.resolve(null); - } - catch (ex) { + //Ok, check if too many pending requets + if (this.commandQueue.length > 10) { + var items = this.commandQueue.splice(0, this.commandQueue.length - 10); + items.forEach(id => { + if (this.commands.has(id)) { + const cmd = this.commands.get(id); + try { + cmd.deferred.resolve(null); + } + catch (ex) { + } + this.commands.delete(id); } - commands.delete(id); - } - }); - } + }); + } + }); }); - }); -} - -function sendCommand(cmd: ICommand): Promise { - if (!proc) { - return Promise.reject(new Error("Python proc not initialized")); - } - var executionCmd = >cmd; - var payload = createPayload(executionCmd); - executionCmd.deferred = createDeferred(); - // if (typeof executionCmd.telemetryEvent === 'string') { - // executionCmd.delays = new telemetryHelper.Delays(); - // } - try { - proc.stdin.write(JSON.stringify(payload) + "\n"); - commands.set(executionCmd.id, executionCmd); - commandQueue.push(executionCmd.id); } - catch (ex) { - console.error(ex); - //If 'This socket is closed.' that means process didn't start at all (at least not properly) - if (ex.message === "This socket is closed.") { - killProcess(); + public sendCommand(cmd: ICommand): Promise { + if (!this.proc) { + return Promise.reject(new Error("Python proc not initialized")); } - else { - handleError("sendCommand", ex.message); + var executionCmd = >cmd; + var payload = this.createPayload(executionCmd); + executionCmd.deferred = createDeferred(); + // if (typeof executionCmd.telemetryEvent === 'string') { + // executionCmd.delays = new telemetryHelper.Delays(); + // } + try { + this.proc.stdin.write(JSON.stringify(payload) + "\n"); + this.commands.set(executionCmd.id, executionCmd); + this.commandQueue.push(executionCmd.id); } - return Promise.reject(ex); - } - return executionCmd.deferred.promise; -} - -function createPayload(cmd: IExecutionCommand): any { - var payload = { - id: cmd.id, - prefix: "", - lookup: commandNames.get(cmd.command), - path: cmd.fileName, - source: cmd.source, - line: cmd.lineIndex, - column: cmd.columnIndex, - config: getConfig() - }; - - if (cmd.command === CommandType.Symbols) { - delete payload.column; - delete payload.line; - } - - return payload; -} + catch (ex) { + console.error(ex); + //If 'This socket is closed.' that means process didn't start at all (at least not properly) + if (ex.message === "This socket is closed.") { -let lastKnownPythonPath: string = null; -let additionalAutoCopletePaths: string[] = []; -function getPathFromPythonCommand(args: string[]): Promise { - return execPythonFile(pythonSettings.pythonPath, args, vscode.workspace.rootPath).then(stdout => { - if (stdout.length === 0) { - return ""; + this.killProcess(); + } + else { + this.handleError("sendCommand", ex.message); + } + return Promise.reject(ex); } - let lines = stdout.split(/\r?\n/g).filter(line => line.length > 0); - return validatePath(lines[0]); - }).catch(() => { - return ""; - }); -} -vscode.workspace.onDidChangeConfiguration(onConfigChanged); -onConfigChanged(); -function onConfigChanged() { - // We're only interested in changes to the python path - if (lastKnownPythonPath === pythonSettings.pythonPath) { - return; + return executionCmd.deferred.promise; } - lastKnownPythonPath = pythonSettings.pythonPath; - let filePaths = [ - // Sysprefix - getPathFromPythonCommand(["-c", "import sys;print(sys.prefix)"]), - // exeucutable path - getPathFromPythonCommand(["-c", "import sys;print(sys.executable)"]), - // Python specific site packages - getPathFromPythonCommand(["-c", "from distutils.sysconfig import get_python_lib; print(get_python_lib())"]), - // Python global site packages, as a fallback in case user hasn't installed them in custom environment - getPathFromPythonCommand(["-m", "site", "--user-site"]), - ]; - - let PYTHONPATH: string = process.env['PYTHONPATH']; - if (typeof PYTHONPATH !== 'string') { - PYTHONPATH = ''; - } - let customEnvironmentVars = getCustomEnvVars(); - if (customEnvironmentVars && customEnvironmentVars['PYTHONPATH']) { - let PYTHONPATHFromEnvFile = customEnvironmentVars['PYTHONPATH'] as string; - if (!path.isAbsolute(PYTHONPATHFromEnvFile) && typeof vscode.workspace.rootPath === 'string') { - PYTHONPATHFromEnvFile = path.resolve(vscode.workspace.rootPath, PYTHONPATHFromEnvFile); + private createPayload(cmd: IExecutionCommand): any { + var payload = { + id: cmd.id, + prefix: "", + lookup: commandNames.get(cmd.command), + path: cmd.fileName, + source: cmd.source, + line: cmd.lineIndex, + column: cmd.columnIndex, + config: this.getConfig() + }; + + if (cmd.command === CommandType.Symbols) { + delete payload.column; + delete payload.line; } - PYTHONPATH += (PYTHONPATH.length > 0 ? + path.delimiter : '') + PYTHONPATHFromEnvFile; + + return payload; } - if (typeof PYTHONPATH === 'string' && PYTHONPATH.length > 0) { - filePaths.push(Promise.resolve(PYTHONPATH.trim())); + + private lastKnownPythonPath: string = null; + private additionalAutoCopletePaths: string[] = []; + private getPathFromPythonCommand(args: string[]): Promise { + return execPythonFile(this.workspacePath, this.pythonSettings.pythonPath, args, this.workspacePath).then(stdout => { + if (stdout.length === 0) { + return ""; + } + let lines = stdout.split(/\r?\n/g).filter(line => line.length > 0); + return validatePath(lines[0]); + }).catch(() => { + return ""; + }); } - Promise.all(filePaths).then(paths => { - // Last item return a path, we need only the folder - if (paths[1].length > 0) { - paths[1] = path.dirname(paths[1]); + private onConfigChanged() { + // We're only interested in changes to the python path + if (this.lastKnownPythonPath === this.pythonSettings.pythonPath) { + return; } - // On windows we also need the libs path (second item will return c:\xxx\lib\site-packages) - // This is returned by "from distutils.sysconfig import get_python_lib; print(get_python_lib())" - if (IS_WINDOWS && paths[2].length > 0) { - paths.splice(3, 0, path.join(paths[2], "..")); + this.lastKnownPythonPath = this.pythonSettings.pythonPath; + let filePaths = [ + // Sysprefix + this.getPathFromPythonCommand(["-c", "import sys;print(sys.prefix)"]), + // exeucutable path + this.getPathFromPythonCommand(["-c", "import sys;print(sys.executable)"]), + // Python specific site packages + this.getPathFromPythonCommand(["-c", "from distutils.sysconfig import get_python_lib; print(get_python_lib())"]), + // Python global site packages, as a fallback in case user hasn't installed them in custom environment + this.getPathFromPythonCommand(["-m", "site", "--user-site"]), + ]; + + let PYTHONPATH: string = process.env['PYTHONPATH']; + if (typeof PYTHONPATH !== 'string') { + PYTHONPATH = ''; } - additionalAutoCopletePaths = paths.filter(p => p.length > 0); - }); -} - -function getConfig() { - // Add support for paths relative to workspace - let extraPaths = pythonSettings.autoComplete.extraPaths.map(extraPath => { - if (path.isAbsolute(extraPath)) { - return extraPath; + let customEnvironmentVars = getCustomEnvVarsSync(vscode.Uri.file(this.pythonProcessCWD)); + if (customEnvironmentVars && customEnvironmentVars['PYTHONPATH']) { + let PYTHONPATHFromEnvFile = customEnvironmentVars['PYTHONPATH'] as string; + if (!path.isAbsolute(PYTHONPATHFromEnvFile) && this.workspacePath === 'string') { + PYTHONPATHFromEnvFile = path.resolve(this.workspacePath, PYTHONPATHFromEnvFile); + } + PYTHONPATH += (PYTHONPATH.length > 0 ? + path.delimiter : '') + PYTHONPATHFromEnvFile; } - if (typeof vscode.workspace.rootPath !== 'string') { - return ''; + if (typeof PYTHONPATH === 'string' && PYTHONPATH.length > 0) { + filePaths.push(Promise.resolve(PYTHONPATH.trim())); } - return path.join(vscode.workspace.rootPath, extraPath); - }); + Promise.all(filePaths).then(paths => { + // Last item return a path, we need only the folder + if (paths[1].length > 0) { + paths[1] = path.dirname(paths[1]); + } - // Always add workspace path into extra paths - if (typeof vscode.workspace.rootPath === 'string') { - extraPaths.unshift(vscode.workspace.rootPath); + // On windows we also need the libs path (second item will return c:\xxx\lib\site-packages) + // This is returned by "from distutils.sysconfig import get_python_lib; print(get_python_lib())" + if (IS_WINDOWS && paths[2].length > 0) { + paths.splice(3, 0, path.join(paths[2], "..")); + } + this.additionalAutoCopletePaths = paths.filter(p => p.length > 0); + }); } - let distinctExtraPaths = extraPaths.concat(additionalAutoCopletePaths) - .filter(value => value.length > 0) - .filter((value, index, self) => self.indexOf(value) === index); - - return { - extraPaths: distinctExtraPaths, - useSnippets: false, - caseInsensitiveCompletion: true, - showDescriptions: true, - fuzzyMatcher: true - }; -} + private getConfig() { + // Add support for paths relative to workspace + let extraPaths = this.pythonSettings.autoComplete.extraPaths.map(extraPath => { + if (path.isAbsolute(extraPath)) { + return extraPath; + } + if (typeof this.workspacePath !== 'string') { + return ''; + } + return path.join(this.workspacePath, extraPath); + }); + // Always add workspace path into extra paths + if (typeof this.workspacePath === 'string') { + extraPaths.unshift(this.workspacePath); + } + + let distinctExtraPaths = extraPaths.concat(this.additionalAutoCopletePaths) + .filter(value => value.length > 0) + .filter((value, index, self) => self.indexOf(value) === index); + + return { + extraPaths: distinctExtraPaths, + useSnippets: false, + caseInsensitiveCompletion: true, + showDescriptions: true, + fuzzyMatcher: true + }; + } +} export interface ICommand { telemetryEvent?: string; command: CommandType; @@ -675,31 +673,31 @@ export interface IHoverItem { signature: string; } -export class JediProxyHandler { - private jediProxy: JediProxy; - private cancellationTokenSource: vscode.CancellationTokenSource; +export class JediProxyHandler implements vscode.Disposable { + private commandCancellationTokenSources: Map; public get JediProxy(): JediProxy { return this.jediProxy; } - public constructor(context: vscode.ExtensionContext, jediProxy: JediProxy = null) { - this.jediProxy = jediProxy ? jediProxy : new JediProxy(context); + public constructor(private jediProxy: JediProxy = null) { + this.commandCancellationTokenSources = new Map(); + } + public dispose() { + this.jediProxy.dispose(); } - public sendCommand(cmd: ICommand, token?: vscode.CancellationToken): Promise { var executionCmd = >cmd; executionCmd.id = executionCmd.id || this.jediProxy.getNextCommandId(); - if (this.cancellationTokenSource) { - try { - this.cancellationTokenSource.cancel(); - } - catch (ex) { } + if (this.commandCancellationTokenSources.has(cmd.command)) { + const cancellation = this.commandCancellationTokenSources.get(cmd.command); + cancellation.cancel(); } - this.cancellationTokenSource = new vscode.CancellationTokenSource(); - executionCmd.token = this.cancellationTokenSource.token; + const cancellation = new vscode.CancellationTokenSource(); + this.commandCancellationTokenSources.set(cmd.command, cancellation); + executionCmd.token = cancellation.token; return this.jediProxy.sendCommand(executionCmd) .catch(reason => { diff --git a/src/client/providers/objectDefinitionProvider.ts b/src/client/providers/objectDefinitionProvider.ts index f7ffea73dfb4..b455c4ba1933 100644 --- a/src/client/providers/objectDefinitionProvider.ts +++ b/src/client/providers/objectDefinitionProvider.ts @@ -2,16 +2,18 @@ import * as vscode from 'vscode'; import * as defProvider from './definitionProvider'; +import { JediFactory } from '../languageServices/jediProxyFactory'; -export function activateGoToObjectDefinitionProvider(context: vscode.ExtensionContext): vscode.Disposable { - let def = new PythonObjectDefinitionProvider(context); - return vscode.commands.registerCommand("python.goToPythonObject", () => def.goToObjectDefinition()); +export function activateGoToObjectDefinitionProvider(jediFactory: JediFactory): vscode.Disposable[] { + const def = new PythonObjectDefinitionProvider(jediFactory); + const commandRegistration = vscode.commands.registerCommand("python.goToPythonObject", () => def.goToObjectDefinition()); + return [def, commandRegistration] as vscode.Disposable[]; } export class PythonObjectDefinitionProvider { private readonly _defProvider: defProvider.PythonDefinitionProvider; - public constructor(context: vscode.ExtensionContext) { - this._defProvider = new defProvider.PythonDefinitionProvider(context); + public constructor(jediFactory: JediFactory) { + this._defProvider = new defProvider.PythonDefinitionProvider(jediFactory); } public async goToObjectDefinition() { diff --git a/src/client/providers/referenceProvider.ts b/src/client/providers/referenceProvider.ts index d25eefb8823a..c8f2031cf5ed 100644 --- a/src/client/providers/referenceProvider.ts +++ b/src/client/providers/referenceProvider.ts @@ -2,14 +2,11 @@ import * as vscode from 'vscode'; import * as proxy from './jediProxy'; +import { JediFactory } from '../languageServices/jediProxyFactory'; export class PythonReferenceProvider implements vscode.ReferenceProvider { - private jediProxyHandler: proxy.JediProxyHandler; - - public constructor(context: vscode.ExtensionContext, jediProxy: proxy.JediProxy = null) { - this.jediProxyHandler = new proxy.JediProxyHandler(context, jediProxy); - } + public constructor(private jediFactory: JediFactory) { } private static parseData(data: proxy.IReferenceResult): vscode.Location[] { if (data && data.references.length > 0) { var references = data.references.filter(ref => { @@ -52,7 +49,7 @@ export class PythonReferenceProvider implements vscode.ReferenceProvider { cmd.source = document.getText(); } - return this.jediProxyHandler.sendCommand(cmd, token).then(data => { + return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token).then(data => { return PythonReferenceProvider.parseData(data); }); } diff --git a/src/client/providers/renameProvider.ts b/src/client/providers/renameProvider.ts index 9afcb4db6c47..e65b915b856b 100644 --- a/src/client/providers/renameProvider.ts +++ b/src/client/providers/renameProvider.ts @@ -7,7 +7,6 @@ import * as path from 'path'; import { PythonSettings } from '../common/configSettings'; import { Installer, Product } from '../common/installer'; -const pythonSettings = PythonSettings.getInstance(); const EXTENSION_DIR = path.join(__dirname, '..', '..', '..'); interface RenameResponse { results: [{ diff: string }]; @@ -41,14 +40,20 @@ export class PythonRenameProvider implements vscode.RenameProvider { return; } - let proxy = new RefactorProxy(EXTENSION_DIR, pythonSettings, vscode.workspace.rootPath); + let workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); + if (!workspaceFolder && Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0) { + workspaceFolder = vscode.workspace.workspaceFolders[0]; + } + const workspaceRoot = workspaceFolder ? workspaceFolder.uri.fsPath : __dirname; + const pythonSettings = PythonSettings.getInstance(workspaceFolder ? workspaceFolder.uri : undefined); + + let proxy = new RefactorProxy(EXTENSION_DIR, pythonSettings, workspaceRoot); return proxy.rename(document, newName, document.uri.fsPath, range).then(response => { - //return response.results[0].diff; - const workspaceEdit = getWorkspaceEditsFromPatch(response.results.map(fileChanges => fileChanges.diff)); - return workspaceEdit; + const fileDiffs = response.results.map(fileChanges => fileChanges.diff); + return getWorkspaceEditsFromPatch(fileDiffs, workspaceRoot); }).catch(reason => { if (reason === 'Not installed') { - this.installer.promptToInstall(Product.rope); + this.installer.promptToInstall(Product.rope, document.uri); return Promise.reject(''); } else { diff --git a/src/client/providers/replProvider.ts b/src/client/providers/replProvider.ts new file mode 100644 index 000000000000..1ef8b6cffc8e --- /dev/null +++ b/src/client/providers/replProvider.ts @@ -0,0 +1,46 @@ +import { commands, Disposable, Uri, window, workspace } from 'vscode'; +import { PythonSettings } from '../common/configSettings'; +import { Commands } from '../common/constants'; +import { getPathFromPythonCommand } from '../common/utils'; + +export class ReplProvider implements Disposable { + private readonly disposables: Disposable[] = []; + constructor() { + this.registerCommand(); + } + public dispose() { + this.disposables.forEach(disposable => disposable.dispose()); + } + private registerCommand() { + const disposable = commands.registerCommand(Commands.Start_REPL, this.commandHandler, this); + this.disposables.push(disposable); + } + private async commandHandler() { + const pythonPath = await this.getPythonPath(); + if (!pythonPath) { + return; + } + let pythonInterpreterPath: string; + try { + pythonInterpreterPath = await getPathFromPythonCommand(pythonPath).catch(() => pythonPath); + // tslint:disable-next-line:variable-name + } catch (_ex) { + pythonInterpreterPath = pythonPath; + } + const term = window.createTerminal('Python', pythonInterpreterPath); + term.show(); + this.disposables.push(term); + } + private async getPythonPath(): Promise { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return PythonSettings.getInstance().pythonPath; + } + if (workspace.workspaceFolders.length === 1) { + return PythonSettings.getInstance(workspace.workspaceFolders[0].uri).pythonPath; + } + + // tslint:disable-next-line:no-any prefer-type-cast + const workspaceFolder = await (window as any).showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); + return workspace ? PythonSettings.getInstance(workspaceFolder.uri).pythonPath : undefined; + } +} diff --git a/src/client/providers/setInterpreterProvider.ts b/src/client/providers/setInterpreterProvider.ts deleted file mode 100644 index 06eb804da172..000000000000 --- a/src/client/providers/setInterpreterProvider.ts +++ /dev/null @@ -1,72 +0,0 @@ -"use strict"; -import * as path from 'path'; -import * as vscode from 'vscode'; -import * as settings from './../common/configSettings'; -import { InterpreterManager } from '../interpreter'; -import { PythonInterpreter } from '../interpreter/contracts'; -import { ShebangCodeLensProvider } from './shebangCodeLensProvider'; - - -interface PythonPathQuickPickItem extends vscode.QuickPickItem { - path: string; -} - -export class SetInterpreterProvider implements vscode.Disposable { - private disposables: vscode.Disposable[] = []; - constructor(private interpreterManager: InterpreterManager) { - this.disposables.push(vscode.commands.registerCommand("python.setInterpreter", this.setInterpreter.bind(this))); - this.disposables.push(vscode.commands.registerCommand("python.setShebangInterpreter", this.setShebangInterpreter.bind(this))); - } - public dispose() { - this.disposables.forEach(disposable => disposable.dispose()); - } - - private suggestionToQuickPickItem(suggestion: PythonInterpreter): PythonPathQuickPickItem { - let detail = suggestion.path; - if (vscode.workspace.rootPath && suggestion.path.startsWith(vscode.workspace.rootPath)) { - detail = `.${path.sep}` + path.relative(vscode.workspace.rootPath!, suggestion.path); - } - return { - label: suggestion.displayName!, - description: suggestion.companyDisplayName || '', - detail: detail, - path: suggestion.path - }; - } - private presentQuickPick() { - this.getSuggestions().then(suggestions => { - let currentPythonPath = settings.PythonSettings.getInstance().pythonPath; - if (vscode.workspace.rootPath && currentPythonPath.startsWith(vscode.workspace.rootPath)) { - currentPythonPath = `.${path.sep}` + path.relative(vscode.workspace.rootPath, currentPythonPath); - } - const quickPickOptions: vscode.QuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${currentPythonPath}` - }; - vscode.window.showQuickPick(suggestions, quickPickOptions).then( - value => { - if (value !== undefined) { - this.interpreterManager.setPythonPath(value.path); - } - }); - }); - } - - private getSuggestions() { - return this.interpreterManager.getInterpreters() - .then(interpreters => interpreters.sort((a, b) => a.displayName! > b.displayName! ? 1 : -1)) - .then(interpreters => interpreters.map(this.suggestionToQuickPickItem)); - } - - private setInterpreter() { - this.presentQuickPick(); - } - - private setShebangInterpreter() { - const shebang = ShebangCodeLensProvider.detectShebang(vscode.window.activeTextEditor.document); - if (shebang) { - this.interpreterManager.setPythonPath(shebang); - } - } -} diff --git a/src/client/providers/shebangCodeLensProvider.ts b/src/client/providers/shebangCodeLensProvider.ts deleted file mode 100644 index 9544976035d7..000000000000 --- a/src/client/providers/shebangCodeLensProvider.ts +++ /dev/null @@ -1,57 +0,0 @@ -"use strict"; -import * as vscode from 'vscode' -import { TextDocument, CodeLens, CancellationToken } from 'vscode' - -export class ShebangCodeLensProvider implements vscode.CodeLensProvider { - private settings; - - // reload codeLenses on every configuration change. - onDidChangeCodeLenses: vscode.Event = vscode.workspace.onDidChangeConfiguration; - - public provideCodeLenses(document: TextDocument, token: CancellationToken): Thenable { - this.settings = vscode.workspace.getConfiguration('python'); - const codeLenses = this.createShebangCodeLens(document); - - return Promise.resolve(codeLenses); - } - - private createShebangCodeLens(document: TextDocument) { - const shebang = ShebangCodeLensProvider.detectShebang(document) - if (!shebang || shebang === this.settings.get('pythonPath')) { - // no shebang detected or interpreter is already set to shebang - return; - } - - // create CodeLens - const firstLine = document.lineAt(0); - const startOfShebang = new vscode.Position(0, 0); - const endOfShebang = new vscode.Position(0, firstLine.text.length - 1); - const shebangRange = new vscode.Range(startOfShebang, endOfShebang); - - const cmd : vscode.Command = { - command: 'python.setShebangInterpreter', - title: 'Set as interpreter' - } - - const codeLenses = [(new CodeLens(shebangRange, cmd))]; - return codeLenses; - } - - public static detectShebang(document: TextDocument) { - let error = false; - - let firstLine = document.lineAt(0); - if (firstLine.isEmptyOrWhitespace) { - error = true; - } - - if (!error && "#!" === firstLine.text.substr(0, 2)) { - // Shebang detected - const shebang = firstLine.text.substr(2).trim(); - return shebang; - } - - return null; - } - -} diff --git a/src/client/providers/signatureProvider.ts b/src/client/providers/signatureProvider.ts index 7d0794444578..5ed411ed47b6 100644 --- a/src/client/providers/signatureProvider.ts +++ b/src/client/providers/signatureProvider.ts @@ -1,8 +1,9 @@ "use strict"; -import * as vscode from "vscode"; +import * as vscode from 'vscode'; +import * as proxy from './jediProxy'; import { TextDocument, Position, CancellationToken, SignatureHelp } from "vscode"; -import * as proxy from "./jediProxy"; +import { JediFactory } from '../languageServices/jediProxyFactory'; const DOCSTRING_PARAM_PATTERNS = [ "\\s*:type\\s*PARAMNAME:\\s*([^\\n, ]+)", // Sphinx @@ -43,11 +44,7 @@ function extractParamDocString(paramName: string, docString: string): string { return paramDocString.trim(); } export class PythonSignatureProvider implements vscode.SignatureHelpProvider { - private jediProxyHandler: proxy.JediProxyHandler; - - public constructor(context: vscode.ExtensionContext, jediProxy: proxy.JediProxy = null) { - this.jediProxyHandler = new proxy.JediProxyHandler(context, jediProxy); - } + public constructor(private jediFactory: JediFactory) { } private static parseData(data: proxy.IArgumentsResult): vscode.SignatureHelp { if (data && Array.isArray(data.definitions) && data.definitions.length > 0) { let signature = new SignatureHelp(); @@ -86,7 +83,7 @@ export class PythonSignatureProvider implements vscode.SignatureHelpProvider { lineIndex: position.line, source: document.getText() }; - return this.jediProxyHandler.sendCommand(cmd, token).then(data => { + return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token).then(data => { return PythonSignatureProvider.parseData(data); }); } diff --git a/src/client/providers/simpleRefactorProvider.ts b/src/client/providers/simpleRefactorProvider.ts index 99e62ac55b5a..35f765165eb1 100644 --- a/src/client/providers/simpleRefactorProvider.ts +++ b/src/client/providers/simpleRefactorProvider.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode'; import { RefactorProxy } from '../refactor/proxy'; import { getTextEditsFromPatch } from '../common/editor'; -import { PythonSettings, IPythonSettings } from '../common/configSettings'; +import { PythonSettings } from '../common/configSettings'; import { Installer, Product } from '../common/installer'; interface RenameResponse { @@ -34,8 +34,14 @@ export function activateSimplePythonRefactorProvider(context: vscode.ExtensionCo // Exported for unit testing export function extractVariable(extensionDir: string, textEditor: vscode.TextEditor, range: vscode.Range, - outputChannel: vscode.OutputChannel, workspaceRoot: string = vscode.workspace.rootPath, - pythonSettings: IPythonSettings = PythonSettings.getInstance()): Promise { + outputChannel: vscode.OutputChannel): Promise { + + let workspaceFolder = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); + if (!workspaceFolder && Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0) { + workspaceFolder = vscode.workspace.workspaceFolders[0]; + } + const workspaceRoot = workspaceFolder ? workspaceFolder.uri.fsPath : __dirname; + const pythonSettings = PythonSettings.getInstance(workspaceFolder ? workspaceFolder.uri : undefined); return validateDocumentForRefactor(textEditor).then(() => { let newName = 'newvariable' + new Date().getMilliseconds().toString(); @@ -50,8 +56,14 @@ export function extractVariable(extensionDir: string, textEditor: vscode.TextEdi // Exported for unit testing export function extractMethod(extensionDir: string, textEditor: vscode.TextEditor, range: vscode.Range, - outputChannel: vscode.OutputChannel, workspaceRoot: string = vscode.workspace.rootPath, - pythonSettings: IPythonSettings = PythonSettings.getInstance()): Promise { + outputChannel: vscode.OutputChannel): Promise { + + let workspaceFolder = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); + if (!workspaceFolder && Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0) { + workspaceFolder = vscode.workspace.workspaceFolders[0]; + } + const workspaceRoot = workspaceFolder ? workspaceFolder.uri.fsPath : __dirname; + const pythonSettings = PythonSettings.getInstance(workspaceFolder ? workspaceFolder.uri : undefined); return validateDocumentForRefactor(textEditor).then(() => { let newName = 'newmethod' + new Date().getMilliseconds().toString(); @@ -127,7 +139,7 @@ function extractName(extensionDir: string, textEditor: vscode.TextEditor, range: } }).catch(error => { if (error === 'Not installed') { - installer.promptToInstall(Product.rope); + installer.promptToInstall(Product.rope, textEditor.document.uri); return Promise.reject(''); } let errorMessage = error + ''; diff --git a/src/client/providers/symbolProvider.ts b/src/client/providers/symbolProvider.ts index 4e89b27f8426..78181b2a62e8 100644 --- a/src/client/providers/symbolProvider.ts +++ b/src/client/providers/symbolProvider.ts @@ -2,13 +2,10 @@ import * as vscode from 'vscode'; import * as proxy from './jediProxy'; +import { JediFactory } from '../languageServices/jediProxyFactory'; export class PythonSymbolProvider implements vscode.DocumentSymbolProvider { - private jediProxyHandler: proxy.JediProxyHandler; - - public constructor(context: vscode.ExtensionContext, jediProxy: proxy.JediProxy = null) { - this.jediProxyHandler = new proxy.JediProxyHandler(context, jediProxy); - } + public constructor(private jediFactory: JediFactory) { } private static parseData(document: vscode.TextDocument, data: proxy.ISymbolResult): vscode.SymbolInformation[] { if (data) { let symbols = data.definitions.filter(sym => sym.fileName === document.fileName); @@ -38,7 +35,7 @@ export class PythonSymbolProvider implements vscode.DocumentSymbolProvider { cmd.source = document.getText(); } - return this.jediProxyHandler.sendCommand(cmd, token).then(data => { + return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token).then(data => { return PythonSymbolProvider.parseData(document, data); }); } @@ -56,7 +53,7 @@ export class PythonSymbolProvider implements vscode.DocumentSymbolProvider { cmd.source = document.getText(); } - return this.jediProxyHandler.sendCommandNonCancellableCommand(cmd, token).then(data => { + return this.jediFactory.getJediProxyHandler(document.uri).sendCommandNonCancellableCommand(cmd, token).then(data => { return PythonSymbolProvider.parseData(document, data); }); } diff --git a/src/client/providers/telemetry.ts b/src/client/providers/telemetry.ts deleted file mode 100644 index 489966e710cf..000000000000 --- a/src/client/providers/telemetry.ts +++ /dev/null @@ -1,71 +0,0 @@ -import {extensions} from "vscode"; -import TelemetryReporter from "vscode-extension-telemetry"; - -// Borrowed from omnisharpServer.ts (omnisharp-vscode) -export class Delays { - immediateDelays: number = 0; // 0-25 milliseconds - nearImmediateDelays: number = 0; // 26-50 milliseconds - shortDelays: number = 0; // 51-250 milliseconds - mediumDelays: number = 0; // 251-500 milliseconds - idleDelays: number = 0; // 501-1500 milliseconds - nonFocusDelays: number = 0; // 1501-3000 milliseconds - bigDelays: number = 0; // 3000+ milliseconds - private startTime: number = Date.now(); - public stop() { - let endTime = Date.now(); - let elapsedTime = endTime - this.startTime; - - if (elapsedTime <= 25) { - this.immediateDelays += 1; - } - else if (elapsedTime <= 50) { - this.nearImmediateDelays += 1; - } - else if (elapsedTime <= 250) { - this.shortDelays += 1; - } - else if (elapsedTime <= 500) { - this.mediumDelays += 1; - } - else if (elapsedTime <= 1500) { - this.idleDelays += 1; - } - else if (elapsedTime <= 3000) { - this.nonFocusDelays += 1; - } - else { - this.bigDelays += 1; - } - } - public toMeasures(): { [key: string]: number } { - return { - immedateDelays: this.immediateDelays, - nearImmediateDelays: this.nearImmediateDelays, - shortDelays: this.shortDelays, - mediumDelays: this.mediumDelays, - idleDelays: this.idleDelays, - nonFocusDelays: this.nonFocusDelays - }; - } -} - -const extensionId = "donjayamanne.python"; -const extension = extensions.getExtension(extensionId); -const extensionVersion = extension.packageJSON.version; -const aiKey = "2f1b85ee-b1eb-469f-8ae3-5adb7114efd7"; -let reporter = new TelemetryReporter(extensionId, extensionVersion, aiKey); - -/** - * Sends a telemetry event - * @param {string} eventName The event name - * @param {object} properties An associative array of strings - * @param {object} measures An associative array of numbers - */ -export function sendTelemetryEvent(eventName: string, properties?: { - [key: string]: string; -}, measures?: { - [key: string]: number; -}) { - reporter.sendTelemetryEvent.apply(reporter, arguments); -} - diff --git a/src/client/refactor/proxy.ts b/src/client/refactor/proxy.ts index 41a717036754..5a238273d8d8 100644 --- a/src/client/refactor/proxy.ts +++ b/src/client/refactor/proxy.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import * as child_process from 'child_process'; import { IPythonSettings } from '../common/configSettings'; import { REFACTOR } from '../common/telemetryContracts'; -import { IS_WINDOWS, getCustomEnvVars, getWindowsLineEndingCount } from '../common/utils'; +import { getCustomEnvVars, getCustomEnvVarsSync, getWindowsLineEndingCount, IS_WINDOWS } from '../common/utils'; import { mergeEnvVariables } from '../common/envFileParser'; export class RefactorProxy extends vscode.Disposable { @@ -17,7 +17,7 @@ export class RefactorProxy extends vscode.Disposable { private _commandResolve: (value?: any | PromiseLike) => void; private _commandReject: (reason?: any) => void; private _initializeReject: (reason?: any) => void; - constructor(extensionDir: string, private pythonSettings: IPythonSettings, private workspaceRoot: string = vscode.workspace.rootPath) { + constructor(extensionDir: string, private pythonSettings: IPythonSettings, private workspaceRoot: string) { super(() => { }); this._extensionDir = extensionDir; } @@ -106,7 +106,7 @@ export class RefactorProxy extends vscode.Disposable { return new Promise((resolve, reject) => { this._initializeReject = reject; let environmentVariables = { 'PYTHONUNBUFFERED': '1' }; - let customEnvironmentVars = getCustomEnvVars(); + let customEnvironmentVars = getCustomEnvVarsSync(vscode.Uri.file(this.workspaceRoot)); if (customEnvironmentVars) { environmentVariables = mergeEnvVariables(environmentVariables, customEnvironmentVars); } @@ -188,4 +188,4 @@ export class RefactorProxy extends vscode.Disposable { this._commandResolve(response[0]); this._commandResolve = null; } -} \ No newline at end of file +} diff --git a/src/client/singleFileDebug.ts b/src/client/singleFileDebug.ts deleted file mode 100644 index 540840fdd00d..000000000000 --- a/src/client/singleFileDebug.ts +++ /dev/null @@ -1,51 +0,0 @@ -import * as vscode from 'vscode'; -import { dirname, join } from 'path'; -import { PythonSettings } from './common/configSettings'; - -export function activateSingleFileDebug() { - return vscode.commands.registerCommand('python.python-debug.startSession', config => { - - if (!config.request) { // if 'request' is missing interpret this as a missing launch.json - config.type = 'python'; - config.name = 'Launch'; - config.request = 'launch'; - config.pythonPath = PythonSettings.getInstance().pythonPath; - config.debugOptions = [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput" - ]; - config.stopOnEntry = true; - config.module = ''; - config.args = []; - config.console = "none"; - config.exceptionHandling = []; - config.env = {}; - - if (vscode.workspace.rootPath) { - config.cwd = vscode.workspace.rootPath; - } - - if (!config.program) { - const editor = vscode.window.activeTextEditor; - if (editor && editor.document.languageId === 'python') { - config.program = editor.document.fileName; - } - } - - if (!config.cwd && config.program) { - // fall back if 'cwd' not known: derive it from 'program' - config.cwd = dirname(config.program); - } - if (vscode.workspace && vscode.workspace.rootPath) { - config.envFile = join(vscode.workspace.rootPath, '.env'); - } - if (!config.envFile && typeof config.cwd === 'string' && config.cwd.lengths > 0) { - config.envFile = join(config.cwd, '.env'); - } - - } - - vscode.commands.executeCommand('vscode.startDebug', config); - }); -} \ No newline at end of file diff --git a/src/client/sortImports.ts b/src/client/sortImports.ts index 90235b9a5094..9ba64f0a0dbf 100644 --- a/src/client/sortImports.ts +++ b/src/client/sortImports.ts @@ -46,4 +46,4 @@ export function activate(context: vscode.ExtensionContext, outChannel: vscode.Ou }); context.subscriptions.push(disposable); -} \ No newline at end of file +} diff --git a/src/client/unittests/codeLenses/main.ts b/src/client/unittests/codeLenses/main.ts index 40c4a58754d3..3efd30091227 100644 --- a/src/client/unittests/codeLenses/main.ts +++ b/src/client/unittests/codeLenses/main.ts @@ -1,16 +1,17 @@ import * as vscode from 'vscode'; import * as constants from '../../common/constants'; - -import { TestFileCodeLensProvider } from './testFiles'; import { PythonSymbolProvider } from '../../providers/symbolProvider'; +import { ITestCollectionStorageService } from '../common/types'; +import { TestFileCodeLensProvider } from './testFiles'; + +export function activateCodeLenses(onDidChange: vscode.EventEmitter, + symboldProvider: PythonSymbolProvider, testCollectionStorage: ITestCollectionStorageService): vscode.Disposable { -export function activateCodeLenses(onDidChange: vscode.EventEmitter, symboldProvider: PythonSymbolProvider): vscode.Disposable { const disposables: vscode.Disposable[] = []; - disposables.push(vscode.languages.registerCodeLensProvider(constants.PythonLanguage, new TestFileCodeLensProvider(onDidChange, symboldProvider))); + const codeLensProvider = new TestFileCodeLensProvider(onDidChange, symboldProvider, testCollectionStorage); + disposables.push(vscode.languages.registerCodeLensProvider(constants.PythonLanguage, codeLensProvider)); return { - dispose: function () { - disposables.forEach(d => d.dispose()); - } + dispose: () => { disposables.forEach(d => d.dispose()); } }; -} \ No newline at end of file +} diff --git a/src/client/unittests/codeLenses/testFiles.ts b/src/client/unittests/codeLenses/testFiles.ts index a9976899b74e..bfb3fd550ffe 100644 --- a/src/client/unittests/codeLenses/testFiles.ts +++ b/src/client/unittests/codeLenses/testFiles.ts @@ -1,124 +1,129 @@ 'use strict'; -import * as vscode from 'vscode'; -import { CodeLensProvider, TextDocument, CancellationToken, CodeLens, SymbolInformation } from 'vscode'; -import { TestFile, TestsToRun, TestSuite, TestFunction, TestStatus } from '../common/contracts'; +import { CancellationToken, CancellationTokenSource, CodeLens, CodeLensProvider, Event, EventEmitter, Position, Range, SymbolInformation, SymbolKind, TextDocument, workspace } from 'vscode'; +import { Uri } from 'vscode'; import * as constants from '../../common/constants'; -import { getDiscoveredTests } from '../common/testUtils'; import { PythonSymbolProvider } from '../../providers/symbolProvider'; +import { ITestCollectionStorageService, TestFile, TestFunction, TestStatus, TestsToRun, TestSuite } from '../common/types'; -interface CodeLensData { - symbolKind: vscode.SymbolKind; - symbolName: string; - fileName: string; -} -interface FunctionsAndSuites { +type FunctionsAndSuites = { functions: TestFunction[]; suites: TestSuite[]; -} +}; export class TestFileCodeLensProvider implements CodeLensProvider { - constructor(private _onDidChange: vscode.EventEmitter, private symbolProvider: PythonSymbolProvider) { + // tslint:disable-next-line:variable-name + constructor(private _onDidChange: EventEmitter, + private symbolProvider: PythonSymbolProvider, + private testCollectionStorage: ITestCollectionStorageService) { } - get onDidChangeCodeLenses(): vscode.Event { + get onDidChangeCodeLenses(): Event { return this._onDidChange.event; } - public provideCodeLenses(document: TextDocument, token: CancellationToken): Thenable { - let testItems = getDiscoveredTests(); + public async provideCodeLenses(document: TextDocument, token: CancellationToken) { + const wkspace = workspace.getWorkspaceFolder(document.uri); + if (!wkspace) { + return []; + } + const testItems = this.testCollectionStorage.getTests(wkspace.uri); if (!testItems || testItems.testFiles.length === 0 || testItems.testFunctions.length === 0) { - return Promise.resolve([]); + return []; } - let cancelTokenSrc = new vscode.CancellationTokenSource(); + const cancelTokenSrc = new CancellationTokenSource(); token.onCancellationRequested(() => { cancelTokenSrc.cancel(); }); // Strop trying to build the code lenses if unable to get a list of - // symbols in this file afrer x time + // symbols in this file afrer x time. setTimeout(() => { if (!cancelTokenSrc.token.isCancellationRequested) { cancelTokenSrc.cancel(); } }, constants.Delays.MaxUnitTestCodeLensDelay); - return getCodeLenses(document, token, this.symbolProvider); + return this.getCodeLenses(document, token, this.symbolProvider); } - resolveCodeLens(codeLens: CodeLens, token: CancellationToken): CodeLens | Thenable { + public resolveCodeLens(codeLens: CodeLens, token: CancellationToken): CodeLens | Thenable { codeLens.command = { command: 'python.runtests', title: 'Test' }; return Promise.resolve(codeLens); } -} -function getCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken, symbolProvider: PythonSymbolProvider): Thenable { - const documentUri = document.uri; - const tests = getDiscoveredTests(); - if (!tests) { - return null; - } - const file = tests.testFiles.find(file => file.fullPath === documentUri.fsPath); - if (!file) { - return Promise.resolve([]); + private async getCodeLenses(document: TextDocument, token: CancellationToken, symbolProvider: PythonSymbolProvider) { + const wkspace = workspace.getWorkspaceFolder(document.uri); + if (!wkspace) { + return []; + } + const tests = this.testCollectionStorage.getTests(wkspace.uri); + if (!tests) { + return []; + } + const file = tests.testFiles.find(item => item.fullPath === document.uri.fsPath); + if (!file) { + return []; + } + const allFuncsAndSuites = getAllTestSuitesAndFunctionsPerFile(file); + + return symbolProvider.provideDocumentSymbolsForInternalUse(document, token) + .then((symbols: SymbolInformation[]) => { + return symbols.filter(symbol => { + return symbol.kind === SymbolKind.Function || + symbol.kind === SymbolKind.Method || + symbol.kind === SymbolKind.Class; + }).map(symbol => { + // This is bloody crucial, if the start and end columns are the same + // then vscode goes bonkers when ever you edit a line (start scrolling magically). + const range = new Range(symbol.location.range.start, + new Position(symbol.location.range.end.line, + symbol.location.range.end.character + 1)); + + return this.getCodeLens(document.uri, allFuncsAndSuites, + range, symbol.name, symbol.kind, symbol.containerName); + }).reduce((previous, current) => previous.concat(current), []).filter(codeLens => codeLens !== null); + }, reason => { + if (token.isCancellationRequested) { + return []; + } + return Promise.reject(reason); + }); } - const allFuncsAndSuites = getAllTestSuitesAndFunctionsPerFile(file); - - return symbolProvider.provideDocumentSymbolsForInternalUse(document, token) - .then((symbols: vscode.SymbolInformation[]) => { - return symbols.filter(symbol => { - return symbol.kind === vscode.SymbolKind.Function || - symbol.kind === vscode.SymbolKind.Method || - symbol.kind === vscode.SymbolKind.Class; - }).map(symbol => { - // This is bloody crucial, if the start and end columns are the same - // then vscode goes bonkers when ever you edit a line (start scrolling magically) - const range = new vscode.Range(symbol.location.range.start, - new vscode.Position(symbol.location.range.end.line, - symbol.location.range.end.character + 1)); - - return getCodeLens(documentUri.fsPath, allFuncsAndSuites, - range, symbol.name, symbol.kind, symbol.containerName); - }).reduce((previous, current) => previous.concat(current), []).filter(codeLens => codeLens !== null); - }, reason => { - if (token.isCancellationRequested) { - return []; - } - return Promise.reject(reason); - }); -} -function getCodeLens(fileName: string, allFuncsAndSuites: FunctionsAndSuites, - range: vscode.Range, symbolName: string, symbolKind: vscode.SymbolKind, symbolContainer: string): vscode.CodeLens[] { + private getCodeLens(file: Uri, allFuncsAndSuites: FunctionsAndSuites, + range: Range, symbolName: string, symbolKind: SymbolKind, symbolContainer: string): CodeLens[] { - switch (symbolKind) { - case vscode.SymbolKind.Function: - case vscode.SymbolKind.Method: { - return getFunctionCodeLens(fileName, allFuncsAndSuites, symbolName, range, symbolContainer); - } - case vscode.SymbolKind.Class: { - const cls = allFuncsAndSuites.suites.find(cls => cls.name === symbolName); - if (!cls) { - return null; + switch (symbolKind) { + case SymbolKind.Function: + case SymbolKind.Method: { + return getFunctionCodeLens(file, allFuncsAndSuites, symbolName, range, symbolContainer); + } + case SymbolKind.Class: { + const cls = allFuncsAndSuites.suites.find(item => item.name === symbolName); + if (!cls) { + return null; + } + return [ + new CodeLens(range, { + title: getTestStatusIcon(cls.status) + constants.Text.CodeLensRunUnitTest, + command: constants.Commands.Tests_Run, + arguments: [file, { testSuite: [cls] }] + }), + new CodeLens(range, { + title: getTestStatusIcon(cls.status) + constants.Text.CodeLensDebugUnitTest, + command: constants.Commands.Tests_Debug, + arguments: [file, { testSuite: [cls] }] + }) + ]; + } + default: { + return []; } - return [ - new CodeLens(range, { - title: getTestStatusIcon(cls.status) + constants.Text.CodeLensRunUnitTest, - command: constants.Commands.Tests_Run, - arguments: [{ testSuite: [cls] }] - }), - new CodeLens(range, { - title: getTestStatusIcon(cls.status) + constants.Text.CodeLensDebugUnitTest, - command: constants.Commands.Tests_Debug, - arguments: [{ testSuite: [cls] }] - }) - ]; } } - - return null; } -function getTestStatusIcon(status: TestStatus): string { +function getTestStatusIcon(status?: TestStatus): string { switch (status) { case TestStatus.Pass: { return '✔ '; @@ -137,7 +142,7 @@ function getTestStatusIcon(status: TestStatus): string { } function getTestStatusIcons(fns: TestFunction[]): string { - let statuses: string[] = []; + const statuses: string[] = []; let count = fns.filter(fn => fn.status === TestStatus.Pass).length; if (count > 0) { statuses.push(`✔ ${count}`); @@ -153,15 +158,14 @@ function getTestStatusIcons(fns: TestFunction[]): string { return statuses.join(' '); } -function getFunctionCodeLens(filePath: string, functionsAndSuites: FunctionsAndSuites, - symbolName: string, range: vscode.Range, symbolContainer: string): vscode.CodeLens[] { +function getFunctionCodeLens(file: Uri, functionsAndSuites: FunctionsAndSuites, + symbolName: string, range: Range, symbolContainer: string): CodeLens[] { - let fn: TestFunction; + let fn: TestFunction | undefined; if (symbolContainer.length === 0) { - fn = functionsAndSuites.functions.find(fn => fn.name === symbolName); - } - else { - // Assume single levels for now + fn = functionsAndSuites.functions.find(func => func.name === symbolName); + } else { + // Assume single levels for now. functionsAndSuites.suites .filter(s => s.name === symbolContainer) .forEach(s => { @@ -177,54 +181,55 @@ function getFunctionCodeLens(filePath: string, functionsAndSuites: FunctionsAndS new CodeLens(range, { title: getTestStatusIcon(fn.status) + constants.Text.CodeLensRunUnitTest, command: constants.Commands.Tests_Run, - arguments: [{ testFunction: [fn] }] + arguments: [file, { testFunction: [fn] }] }), new CodeLens(range, { title: getTestStatusIcon(fn.status) + constants.Text.CodeLensDebugUnitTest, command: constants.Commands.Tests_Debug, - arguments: [{ testFunction: [fn] }] + arguments: [file, { testFunction: [fn] }] }) ]; } - // Ok, possible we're dealing with parameterized unit tests - // If we have [ in the name, then this is a parameterized function - let functions = functionsAndSuites.functions.filter(fn => fn.name.startsWith(symbolName + '[') && fn.name.endsWith(']')); + // Ok, possible we're dealing with parameterized unit tests. + // If we have [ in the name, then this is a parameterized function. + const functions = functionsAndSuites.functions.filter(func => func.name.startsWith(`${symbolName}[`) && func.name.endsWith(']')); if (functions.length === 0) { - return null; + return []; } if (functions.length === 0) { return [ new CodeLens(range, { title: constants.Text.CodeLensRunUnitTest, command: constants.Commands.Tests_Run, - arguments: [{ testFunction: functions }] + arguments: [file, { testFunction: functions }] }), new CodeLens(range, { title: constants.Text.CodeLensDebugUnitTest, command: constants.Commands.Tests_Debug, - arguments: [{ testFunction: functions }] + arguments: [file, { testFunction: functions }] }) ]; } - // Find all flattened functions + // Find all flattened functions. return [ new CodeLens(range, { - title: getTestStatusIcons(functions) + constants.Text.CodeLensRunUnitTest + ' (Multiple)', + title: `${getTestStatusIcons(functions)}${constants.Text.CodeLensRunUnitTest} (Multiple)`, command: constants.Commands.Tests_Picker_UI, - arguments: [filePath, functions] + arguments: [file, functions] }), new CodeLens(range, { - title: getTestStatusIcons(functions) + constants.Text.CodeLensDebugUnitTest + ' (Multiple)', + title: `${getTestStatusIcons(functions)}${constants.Text.CodeLensDebugUnitTest} (Multiple)`, command: constants.Commands.Tests_Picker_UI_Debug, - arguments: [filePath, functions] + arguments: [file, functions] }) ]; } function getAllTestSuitesAndFunctionsPerFile(testFile: TestFile): FunctionsAndSuites { - const all = { functions: testFile.functions, suites: [] }; + // tslint:disable-next-line:prefer-type-cast + const all = { functions: testFile.functions, suites: [] as TestSuite[] }; testFile.suites.forEach(suite => { all.suites.push(suite); @@ -247,4 +252,4 @@ function getAllTestSuitesAndFunctions(testSuite: TestSuite): FunctionsAndSuites all.suites.push(...allChildItems.suites); }); return all; -} \ No newline at end of file +} diff --git a/src/client/unittests/common/baseTestManager.ts b/src/client/unittests/common/baseTestManager.ts index e6c31d31a0be..4b4612c7da85 100644 --- a/src/client/unittests/common/baseTestManager.ts +++ b/src/client/unittests/common/baseTestManager.ts @@ -1,33 +1,59 @@ // import {TestFolder, TestsToRun, Tests, TestFile, TestSuite, TestFunction, TestStatus, FlattenedTestFunction, FlattenedTestSuite, CANCELLATION_REASON} from './contracts'; -import { Tests, TestStatus, TestsToRun, CANCELLATION_REASON } from './contracts'; import * as vscode from 'vscode'; -import { resetTestResults, displayTestErrorMessage, storeDiscoveredTests } from './testUtils'; -import { Installer, Product } from '../../common/installer'; +import { Uri, workspace } from 'vscode'; +import { IPythonSettings, PythonSettings } from '../../common/configSettings'; import { isNotInstalledError } from '../../common/helpers'; +import { Installer, Product } from '../../common/installer'; +import { CANCELLATION_REASON } from './constants'; +import { displayTestErrorMessage } from './testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestStatus, TestsToRun } from './types'; + +enum CancellationTokenType { + testDiscovery, + testRunner +} export abstract class BaseTestManager { + public readonly workspace: Uri; + protected readonly settings: IPythonSettings; private tests: Tests; + // tslint:disable-next-line:variable-name private _status: TestStatus = TestStatus.Unknown; - private cancellationTokenSource: vscode.CancellationTokenSource; + private testDiscoveryCancellationTokenSource: vscode.CancellationTokenSource; + private testRunnerCancellationTokenSource: vscode.CancellationTokenSource; private installer: Installer; - protected get cancellationToken(): vscode.CancellationToken { - if (this.cancellationTokenSource) { - return this.cancellationTokenSource.token; - } + private discoverTestsPromise: Promise; + constructor(private testProvider: string, private product: Product, protected rootDirectory: string, + protected outputChannel: vscode.OutputChannel, private testCollectionStorage: ITestCollectionStorageService, + protected testResultsService: ITestResultsService, protected testsHelper: ITestsHelper) { + this._status = TestStatus.Unknown; + this.installer = new Installer(); + this.settings = PythonSettings.getInstance(this.rootDirectory ? Uri.file(this.rootDirectory) : undefined); + this.workspace = workspace.getWorkspaceFolder(Uri.file(this.rootDirectory)).uri; + } + protected get testDiscoveryCancellationToken(): vscode.CancellationToken | undefined { + return this.testDiscoveryCancellationTokenSource ? this.testDiscoveryCancellationTokenSource.token : undefined; + } + protected get testRunnerCancellationToken(): vscode.CancellationToken | undefined { + return this.testRunnerCancellationTokenSource ? this.testRunnerCancellationTokenSource.token : undefined; } public dispose() { + this.stop(); } public get status(): TestStatus { return this._status; } + public get workingDirectory(): string { + const settings = PythonSettings.getInstance(vscode.Uri.file(this.rootDirectory)); + return settings.unitTest.cwd && settings.unitTest.cwd.length > 0 ? settings.unitTest.cwd : this.rootDirectory; + } public stop() { - if (this.cancellationTokenSource) { - this.cancellationTokenSource.cancel(); + if (this.testDiscoveryCancellationTokenSource) { + this.testDiscoveryCancellationTokenSource.cancel(); + } + if (this.testRunnerCancellationTokenSource) { + this.testRunnerCancellationTokenSource.cancel(); } - } - constructor(private testProvider: string, private product: Product, protected rootDirectory: string, protected outputChannel: vscode.OutputChannel) { - this._status = TestStatus.Unknown; - this.installer = new Installer(); } public reset() { this._status = TestStatus.Unknown; @@ -38,20 +64,9 @@ export abstract class BaseTestManager { return; } - resetTestResults(this.tests); + this.testResultsService.resetResults(this.tests); } - private createCancellationToken() { - this.disposeCancellationToken(); - this.cancellationTokenSource = new vscode.CancellationTokenSource(); - } - private disposeCancellationToken() { - if (this.cancellationTokenSource) { - this.cancellationTokenSource.dispose(); - } - this.cancellationTokenSource = null; - } - private discoverTestsPromise: Promise; - discoverTests(ignoreCache: boolean = false, quietMode: boolean = false): Promise { + public async discoverTests(ignoreCache: boolean = false, quietMode: boolean = false, userInitiated: boolean = false): Promise { if (this.discoverTestsPromise) { return this.discoverTestsPromise; } @@ -62,7 +77,13 @@ export abstract class BaseTestManager { } this._status = TestStatus.Discovering; - this.createCancellationToken(); + // If ignoreCache is true, its an indication of the fact that its a user invoked operation. + // Hence we can stop the debugger. + if (userInitiated) { + this.stop(); + } + + this.createCancellationToken(CancellationTokenType.testDiscovery); return this.discoverTestsPromise = this.discoverTestsImpl(ignoreCache) .then(tests => { this.tests = tests; @@ -84,38 +105,36 @@ export abstract class BaseTestManager { if (haveErrorsInDiscovering && !quietMode) { displayTestErrorMessage('There were some errors in disovering unit tests'); } - storeDiscoveredTests(tests); - this.disposeCancellationToken(); + const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(this.rootDirectory)).uri; + this.testCollectionStorage.storeTests(wkspace, tests); + this.disposeCancellationToken(CancellationTokenType.testDiscovery); return tests; }).catch(reason => { if (isNotInstalledError(reason) && !quietMode) { - this.installer.promptToInstall(this.product); + // tslint:disable-next-line:no-floating-promises + this.installer.promptToInstall(this.product, this.workspace); } this.tests = null; this.discoverTestsPromise = null; - if (this.cancellationToken && this.cancellationToken.isCancellationRequested) { + if (this.testDiscoveryCancellationToken && this.testDiscoveryCancellationToken.isCancellationRequested) { reason = CANCELLATION_REASON; this._status = TestStatus.Idle; - } - else { + } else { this._status = TestStatus.Error; this.outputChannel.appendLine('Test Disovery failed: '); + // tslint:disable-next-line:prefer-template this.outputChannel.appendLine('' + reason); } - storeDiscoveredTests(null); - this.disposeCancellationToken(); + const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(this.rootDirectory)).uri; + this.testCollectionStorage.storeTests(wkspace, null); + this.disposeCancellationToken(CancellationTokenType.testDiscovery); return Promise.reject(reason); }); } - abstract discoverTestsImpl(ignoreCache: boolean, debug?: boolean): Promise; - public runTest(testsToRun?: TestsToRun, debug?: boolean): Promise; - public runTest(runFailedTests?: boolean, debug?: boolean): Promise; - public runTest(args: any, debug?: boolean): Promise { - let runFailedTests = false; - let testsToRun: TestsToRun = null; - let moreInfo = { + public runTest(testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { + const moreInfo = { Test_Provider: this.testProvider, Run_Failed_Tests: 'false', Run_Specific_File: 'false', @@ -123,12 +142,11 @@ export abstract class BaseTestManager { Run_Specific_Function: 'false' }; - if (typeof args === 'boolean') { - runFailedTests = args === true; + if (runFailedTests === true) { + // tslint:disable-next-line:prefer-template moreInfo.Run_Failed_Tests = runFailedTests + ''; } - if (typeof args === 'object' && args !== null) { - testsToRun = args; + if (testsToRun && typeof testsToRun === 'object') { if (Array.isArray(testsToRun.testFile) && testsToRun.testFile.length > 0) { moreInfo.Run_Specific_File = 'true'; } @@ -144,39 +162,62 @@ export abstract class BaseTestManager { } this._status = TestStatus.Running; - this.createCancellationToken(); + this.stop(); // If running failed tests, then don't clear the previously build UnitTests // If we do so, then we end up re-discovering the unit tests and clearing previously cached list of failed tests // Similarly, if running a specific test or test file, don't clear the cache (possible tests have some state information retained) const clearDiscoveredTestCache = runFailedTests || moreInfo.Run_Specific_File || moreInfo.Run_Specific_Class || moreInfo.Run_Specific_Function ? false : true; - return this.discoverTests(clearDiscoveredTestCache, true) + return this.discoverTests(clearDiscoveredTestCache, true, true) .catch(reason => { - if (this.cancellationToken && this.cancellationToken.isCancellationRequested) { + if (this.testDiscoveryCancellationToken && this.testDiscoveryCancellationToken.isCancellationRequested) { return Promise.reject(reason); } displayTestErrorMessage('Errors in discovering tests, continuing with tests'); return { - rootTestFolders: [], testFiles: [], testFolders: [], testFunctions: [], testSuits: [], + rootTestFolders: [], testFiles: [], testFolders: [], testFunctions: [], testSuites: [], summary: { errors: 0, failures: 0, passed: 0, skipped: 0 } }; }) .then(tests => { + this.createCancellationToken(CancellationTokenType.testRunner); return this.runTestImpl(tests, testsToRun, runFailedTests, debug); }).then(() => { this._status = TestStatus.Idle; - this.disposeCancellationToken(); + this.disposeCancellationToken(CancellationTokenType.testRunner); return this.tests; }).catch(reason => { - if (this.cancellationToken && this.cancellationToken.isCancellationRequested) { + if (this.testRunnerCancellationToken && this.testRunnerCancellationToken.isCancellationRequested) { reason = CANCELLATION_REASON; this._status = TestStatus.Idle; - } - else { + } else { this._status = TestStatus.Error; } - this.disposeCancellationToken(); + this.disposeCancellationToken(CancellationTokenType.testRunner); return Promise.reject(reason); }); } - abstract runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise; -} \ No newline at end of file + // tslint:disable-next-line:no-any + protected abstract runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise; + protected abstract discoverTestsImpl(ignoreCache: boolean, debug?: boolean): Promise; + private createCancellationToken(tokenType: CancellationTokenType) { + this.disposeCancellationToken(tokenType); + if (tokenType === CancellationTokenType.testDiscovery) { + this.testDiscoveryCancellationTokenSource = new vscode.CancellationTokenSource(); + } else { + this.testRunnerCancellationTokenSource = new vscode.CancellationTokenSource(); + } + } + private disposeCancellationToken(tokenType: CancellationTokenType) { + if (tokenType === CancellationTokenType.testDiscovery) { + if (this.testDiscoveryCancellationTokenSource) { + this.testDiscoveryCancellationTokenSource.dispose(); + } + this.testDiscoveryCancellationTokenSource = null; + } else { + if (this.testRunnerCancellationTokenSource) { + this.testRunnerCancellationTokenSource.dispose(); + } + this.testRunnerCancellationTokenSource = null; + } + } +} diff --git a/src/client/unittests/common/configSettingService.ts b/src/client/unittests/common/configSettingService.ts new file mode 100644 index 000000000000..f6a90619f9fe --- /dev/null +++ b/src/client/unittests/common/configSettingService.ts @@ -0,0 +1,67 @@ +import { ConfigurationTarget, Uri, workspace, WorkspaceConfiguration } from 'vscode'; +import { Product } from '../../common/installer'; +import { ITestConfigSettingsService, UnitTestProduct } from './types'; + +export class TestConfigSettingsService implements ITestConfigSettingsService { + private static getTestArgSetting(product: UnitTestProduct) { + switch (product) { + case Product.unittest: + return 'unitTest.unittestArgs'; + case Product.pytest: + return 'unitTest.pyTestArgs'; + case Product.nosetest: + return 'unitTest.nosetestArgs'; + default: + throw new Error('Invalid Test Product'); + } + } + private static getTestEnablingSetting(product: UnitTestProduct) { + switch (product) { + case Product.unittest: + return 'unitTest.unittestEnabled'; + case Product.pytest: + return 'unitTest.pyTestEnabled'; + case Product.nosetest: + return 'unitTest.nosetestsEnabled'; + default: + throw new Error('Invalid Test Product'); + } + } + // tslint:disable-next-line:no-any + private static async updateSetting(testDirectory: string | Uri, setting: string, value: any) { + let pythonConfig: WorkspaceConfiguration; + let configTarget: ConfigurationTarget; + const resource = typeof testDirectory === 'string' ? Uri.file(testDirectory) : testDirectory; + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + configTarget = ConfigurationTarget.Workspace; + pythonConfig = workspace.getConfiguration('python'); + } else if (workspace.workspaceFolders.length === 1) { + configTarget = ConfigurationTarget.Workspace; + pythonConfig = workspace.getConfiguration('python', workspace.workspaceFolders[0].uri); + } else { + configTarget = ConfigurationTarget.Workspace; + const workspaceFolder = workspace.getWorkspaceFolder(resource); + if (!workspaceFolder) { + throw new Error(`Test directory does not belong to any workspace (${testDirectory})`); + } + // tslint:disable-next-line:no-non-null-assertion + pythonConfig = workspace.getConfiguration('python', workspaceFolder!.uri); + } + + return pythonConfig.update(setting, value); + } + public async updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]) { + const setting = TestConfigSettingsService.getTestArgSetting(product); + return TestConfigSettingsService.updateSetting(testDirectory, setting, args); + } + + public async enable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + const setting = TestConfigSettingsService.getTestEnablingSetting(product); + return TestConfigSettingsService.updateSetting(testDirectory, setting, true); + } + + public async disable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + const setting = TestConfigSettingsService.getTestEnablingSetting(product); + return TestConfigSettingsService.updateSetting(testDirectory, setting, false); + } +} diff --git a/src/client/unittests/common/constants.ts b/src/client/unittests/common/constants.ts new file mode 100644 index 000000000000..3d428cfad97c --- /dev/null +++ b/src/client/unittests/common/constants.ts @@ -0,0 +1 @@ +export const CANCELLATION_REASON = 'cancelled_user_request'; diff --git a/src/client/unittests/common/contracts.ts b/src/client/unittests/common/contracts.ts deleted file mode 100644 index 02dc03c18980..000000000000 --- a/src/client/unittests/common/contracts.ts +++ /dev/null @@ -1,89 +0,0 @@ -export const CANCELLATION_REASON = 'cancelled_user_request'; - -export interface TestFolder extends TestResult { - name: string; - testFiles: TestFile[]; - nameToRun: string; - status?: TestStatus; - folders: TestFolder[]; -} -export interface TestFile extends TestResult { - name: string; - fullPath: string; - functions: TestFunction[]; - suites: TestSuite[]; - nameToRun: string; - xmlName: string; - status?: TestStatus; - errorsWhenDiscovering?: string; -} -export interface TestSuite extends TestResult { - name: string; - functions: TestFunction[]; - suites: TestSuite[]; - isUnitTest: Boolean; - isInstance: Boolean; - nameToRun: string; - xmlName: string; - status?: TestStatus; -} -export interface TestFunction extends TestResult { - name: string; - nameToRun: string; - status?: TestStatus; -} -export interface TestResult extends Node { - passed?: boolean; - time: number; - line?: number; - message?: string; - traceback?: string; - functionsPassed?: number; - functionsFailed?: number; - functionsDidNotRun?: number; -} -export interface Node { - expanded?: Boolean; -} -export interface FlattenedTestFunction { - testFunction: TestFunction; - parentTestSuite?: TestSuite; - parentTestFile: TestFile; - xmlClassName: string; -} -export interface FlattenedTestSuite { - testSuite: TestSuite; - parentTestSuite?: TestSuite; - parentTestFile: TestFile; - xmlClassName: string; -} -export interface TestSummary { - passed: number; - failures: number; - errors: number; - skipped: number; -} -export interface Tests { - summary: TestSummary; - testFiles: TestFile[]; - testFunctions: FlattenedTestFunction[]; - testSuits: FlattenedTestSuite[]; - testFolders: TestFolder[]; - rootTestFolders: TestFolder[]; -} -export enum TestStatus { - Unknown, - Discovering, - Idle, - Running, - Fail, - Error, - Skipped, - Pass -} -export interface TestsToRun { - testFolder?: TestFolder[]; - testFile?: TestFile[]; - testSuite?: TestSuite[]; - testFunction?: TestFunction[]; -} diff --git a/src/client/unittests/common/debugLauncher.ts b/src/client/unittests/common/debugLauncher.ts new file mode 100644 index 000000000000..783dbc24fd7c --- /dev/null +++ b/src/client/unittests/common/debugLauncher.ts @@ -0,0 +1,68 @@ +import * as os from 'os'; +import { CancellationToken, debug, OutputChannel, Uri, workspace } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; +import { createDeferred } from './../../common/helpers'; +import { execPythonFile } from './../../common/utils'; +import { ITestDebugLauncher } from './types'; + +export class DebugLauncher implements ITestDebugLauncher { + public async launchDebugger(rootDirectory: string, testArgs: string[], token?: CancellationToken, outChannel?: OutputChannel) { + const pythonSettings = PythonSettings.getInstance(rootDirectory ? Uri.file(rootDirectory) : undefined); + // tslint:disable-next-line:no-any + const def = createDeferred(); + // tslint:disable-next-line:no-any + const launchDef = createDeferred(); + let outputChannelShown = false; + execPythonFile(rootDirectory, pythonSettings.pythonPath, testArgs, rootDirectory, true, (data: string) => { + if (data.startsWith(`READY${os.EOL}`)) { + // debug socket server has started. + launchDef.resolve(); + data = data.substring((`READY${os.EOL}`).length); + } + + if (!outputChannelShown) { + outputChannelShown = true; + outChannel.show(); + } + outChannel.append(data); + }, token).catch(reason => { + if (!def.rejected && !def.resolved) { + def.reject(reason); + } + }).then(() => { + if (!def.rejected && !def.resolved) { + def.resolve(); + } + }).catch(reason => { + if (!def.rejected && !def.resolved) { + def.reject(reason); + } + }); + + launchDef.promise.then(() => { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + throw new Error('Please open a workspace'); + } + let workspaceFolder = workspace.getWorkspaceFolder(Uri.file(rootDirectory)); + if (!workspaceFolder) { + workspaceFolder = workspace.workspaceFolders[0]; + } + return debug.startDebugging(workspaceFolder, { + name: 'Debug Unit Test', + type: 'python', + request: 'attach', + localRoot: rootDirectory, + remoteRoot: rootDirectory, + port: pythonSettings.unitTest.debugPort, + secret: 'my_secret', + host: 'localhost' + }); + }).catch(reason => { + if (!def.rejected && !def.resolved) { + def.reject(reason); + } + }); + + return def.promise; + } +} diff --git a/src/client/unittests/common/runner.ts b/src/client/unittests/common/runner.ts index 0b4b328d4c3e..de3b90245e6e 100644 --- a/src/client/unittests/common/runner.ts +++ b/src/client/unittests/common/runner.ts @@ -1,44 +1,7 @@ -import { execPythonFile } from './../../common/utils'; import { CancellationToken, OutputChannel, window, workspace } from 'vscode'; -import { getPythonInterpreterDirectory, IS_WINDOWS, PATH_VARIABLE_NAME } from '../../common/utils'; - -let terminal = null; -export function run(file: string, args: string[], cwd: string, token?: CancellationToken, outChannel?: OutputChannel): Promise { - return execPythonFile(file, args, cwd, true, (data: string) => outChannel.append(data), token); +import { IS_WINDOWS, PATH_VARIABLE_NAME } from '../../common/utils'; +import { execPythonFile } from './../../common/utils'; - // Bug, we cannot resolve this - // Resolving here means that tests have completed - // We need a way to determine that the tests have completed succefully.. hmm - // We could use a hack, such as generating a textfile at the end of the command and monitoring.. hack hack hack - // Or we could generate a shell script file and embed all of the hacks in here... hack hack hack... - // return runTestInTerminal(file, args, cwd); +export async function run(file: string, args: string[], cwd: string, token?: CancellationToken, outChannel?: OutputChannel): Promise { + return execPythonFile(cwd, file, args, cwd, true, (data: string) => outChannel.append(data), token); } - -function runTestInTerminal(file: string, args: string[], cwd: string): Promise { - return getPythonInterpreterDirectory().then(pyPath => { - let commands = []; - if (IS_WINDOWS) { - commands.push(`set ${PATH_VARIABLE_NAME}=%${PATH_VARIABLE_NAME}%;${pyPath}`); - } - else { - commands.push(`export ${PATH_VARIABLE_NAME}=$${PATH_VARIABLE_NAME}:${pyPath}`); - } - if (cwd !== workspace.rootPath && typeof cwd === 'string') { - commands.push(`cd ${cwd}`); - } - commands.push(`${file} ${args.join(' ')}`); - terminal = window.createTerminal(`Python Test Log`); - - return new Promise((resolve) => { - setTimeout(function () { - terminal.show(); - terminal.sendText(commands.join(' && ')); - - // Bug, we cannot resolve this - // Resolving here means that tests have completed - // We need a way to determine that the tests have completed succefully.. hmm - resolve(); - }, 1000); - }); - }); -} \ No newline at end of file diff --git a/src/client/unittests/common/storageService.ts b/src/client/unittests/common/storageService.ts new file mode 100644 index 000000000000..775750de1fe9 --- /dev/null +++ b/src/client/unittests/common/storageService.ts @@ -0,0 +1,21 @@ +import { Uri, workspace } from 'vscode'; +import { ITestCollectionStorageService, Tests } from './types'; + +export class TestCollectionStorageService implements ITestCollectionStorageService { + private testsIndexedByWorkspaceUri = new Map(); + public getTests(wkspace: Uri): Tests | undefined { + const workspaceFolder = this.getWorkspaceFolderPath(wkspace) || ''; + return this.testsIndexedByWorkspaceUri.has(workspaceFolder) ? this.testsIndexedByWorkspaceUri.get(workspaceFolder) : undefined; + } + public storeTests(wkspace: Uri, tests: Tests | null | undefined): void { + const workspaceFolder = this.getWorkspaceFolderPath(wkspace) || ''; + this.testsIndexedByWorkspaceUri.set(workspaceFolder, tests); + } + public dispose() { + this.testsIndexedByWorkspaceUri.clear(); + } + private getWorkspaceFolderPath(resource: Uri): string | undefined { + const folder = workspace.getWorkspaceFolder(resource); + return folder ? folder.uri.path : undefined; + } +} diff --git a/src/client/unittests/common/testConfigurationManager.ts b/src/client/unittests/common/testConfigurationManager.ts index 8b031b7a63b2..afdad0192c87 100644 --- a/src/client/unittests/common/testConfigurationManager.ts +++ b/src/client/unittests/common/testConfigurationManager.ts @@ -1,14 +1,26 @@ +import * as path from 'path'; import * as vscode from 'vscode'; +import { Uri } from 'vscode'; import { createDeferred } from '../../common/helpers'; +import { Installer } from '../../common/installer'; import { getSubDirectories } from '../../common/utils'; -import * as path from 'path'; +import { ITestConfigSettingsService, UnitTestProduct } from './types'; export abstract class TestConfigurationManager { - public abstract enable(): Thenable; - public abstract disable(): Thenable; - constructor(protected readonly outputChannel: vscode.OutputChannel) { } - public abstract configure(rootDir: string): Promise; - + constructor(protected workspace: Uri, + protected product: UnitTestProduct, + protected readonly outputChannel: vscode.OutputChannel, + protected installer: Installer, + protected testConfigSettingsService: ITestConfigSettingsService) { } + // tslint:disable-next-line:no-any + public abstract configure(wkspace: Uri): Promise; + public async enable() { + return this.testConfigSettingsService.enable(this.workspace, this.product); + } + // tslint:disable-next-line:no-any + public async disable() { + return this.testConfigSettingsService.enable(this.workspace, this.product); + } protected selectTestDir(rootDir: string, subDirs: string[], customOptions: vscode.QuickPickItem[] = []): Promise { const options = { matchOnDescription: true, @@ -22,7 +34,7 @@ export abstract class TestConfigurationManager { } return { label: dirName, - description: '', + description: '' }; }).filter(item => item !== null); @@ -46,12 +58,12 @@ export abstract class TestConfigurationManager { matchOnDetail: true, placeHolder: 'Select the pattern to identify test files' }; - let items: vscode.QuickPickItem[] = [ - { label: '*test.py', description: `Python Files ending with 'test'` }, - { label: '*_test.py', description: `Python Files ending with '_test'` }, - { label: 'test*.py', description: `Python Files begining with 'test'` }, - { label: 'test_*.py', description: `Python Files begining with 'test_'` }, - { label: '*test*.py', description: `Python Files containing the word 'test'` } + const items: vscode.QuickPickItem[] = [ + { label: '*test.py', description: 'Python Files ending with \'test\'' }, + { label: '*_test.py', description: 'Python Files ending with \'_test\'' }, + { label: 'test*.py', description: 'Python Files begining with \'test\'' }, + { label: 'test_*.py', description: 'Python Files begining with \'test_\'' }, + { label: '*test*.py', description: 'Python Files containing the word \'test\'' } ]; const def = createDeferred(); @@ -65,17 +77,17 @@ export abstract class TestConfigurationManager { return def.promise; } - protected getTestDirs(rootDir): Promise { + protected getTestDirs(rootDir: string): Promise { return getSubDirectories(rootDir).then(subDirs => { subDirs.sort(); - // Find out if there are any dirs with the name test and place them on the top - let possibleTestDirs = subDirs.filter(dir => dir.match(/test/i)); - let nonTestDirs = subDirs.filter(dir => possibleTestDirs.indexOf(dir) === -1); + // Find out if there are any dirs with the name test and place them on the top. + const possibleTestDirs = subDirs.filter(dir => dir.match(/test/i)); + const nonTestDirs = subDirs.filter(dir => possibleTestDirs.indexOf(dir) === -1); possibleTestDirs.push(...nonTestDirs); - // The test dirs are now on top + // The test dirs are now on top. return possibleTestDirs; }); } -} \ No newline at end of file +} diff --git a/src/client/unittests/common/testManagerService.ts b/src/client/unittests/common/testManagerService.ts new file mode 100644 index 000000000000..87cfa5d2c1e4 --- /dev/null +++ b/src/client/unittests/common/testManagerService.ts @@ -0,0 +1,63 @@ +import { OutputChannel, Uri } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; +import { Product } from '../../common/installer'; +import { TestManager as NoseTestManager } from '../nosetest/main'; +import { TestManager as PyTestTestManager } from '../pytest/main'; +import { TestManager as UnitTestTestManager } from '../unittest/main'; +import { BaseTestManager } from './baseTestManager'; +import { ITestCollectionStorageService, ITestDebugLauncher, ITestManagerService, ITestResultsService, ITestsHelper, UnitTestProduct } from './types'; + +type TestManagerInstanceInfo = { instance?: BaseTestManager, create(rootDirectory: string): BaseTestManager }; + +export class TestManagerService implements ITestManagerService { + private testManagers = new Map(); + constructor(private wkspace: Uri, private outChannel: OutputChannel, + testCollectionStorage: ITestCollectionStorageService, testResultsService: ITestResultsService, + testsHelper: ITestsHelper, debugLauncher: ITestDebugLauncher) { + this.testManagers.set(Product.nosetest, { + create: (rootDirectory: string) => new NoseTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper, debugLauncher) + }); + this.testManagers.set(Product.pytest, { + create: (rootDirectory: string) => new PyTestTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper, debugLauncher) + }); + this.testManagers.set(Product.unittest, { + create: (rootDirectory: string) => new UnitTestTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper, debugLauncher) + }); + } + public dispose() { + this.testManagers.forEach(info => { + if (info.instance) { + info.instance.dispose(); + } + }); + } + public getTestManager(): BaseTestManager | undefined { + const preferredTestManager = this.getPreferredTestManager(); + if (typeof preferredTestManager !== 'number') { + return; + } + + // tslint:disable-next-line:no-non-null-assertion + const info = this.testManagers.get(preferredTestManager)!; + if (!info.instance) { + const testDirectory = this.getTestWorkingDirectory(); + info.instance = info.create(testDirectory); + } + return info.instance; + } + public getTestWorkingDirectory() { + const settings = PythonSettings.getInstance(this.wkspace); + return settings.unitTest.cwd && settings.unitTest.cwd.length > 0 ? settings.unitTest.cwd : this.wkspace.fsPath; + } + public getPreferredTestManager(): UnitTestProduct | undefined { + const settings = PythonSettings.getInstance(this.wkspace); + if (settings.unitTest.nosetestsEnabled) { + return Product.nosetest; + } else if (settings.unitTest.pyTestEnabled) { + return Product.pytest; + } else if (settings.unitTest.unittestEnabled) { + return Product.unittest; + } + return undefined; + } +} diff --git a/src/client/unittests/common/testManagerServiceFactory.ts b/src/client/unittests/common/testManagerServiceFactory.ts new file mode 100644 index 000000000000..f10600ef8b45 --- /dev/null +++ b/src/client/unittests/common/testManagerServiceFactory.ts @@ -0,0 +1,12 @@ +import { OutputChannel, Uri } from 'vscode'; +import { TestManagerService } from './testManagerService'; +import { ITestCollectionStorageService, ITestDebugLauncher, ITestManagerService, ITestManagerServiceFactory, ITestResultsService, ITestsHelper } from './types'; + +export class TestManagerServiceFactory implements ITestManagerServiceFactory { + constructor(private outChannel: OutputChannel, private testCollectionStorage: ITestCollectionStorageService, + private testResultsService: ITestResultsService, private testsHelper: ITestsHelper, + private debugLauncher: ITestDebugLauncher) { } + public createTestManagerService(wkspace: Uri): ITestManagerService { + return new TestManagerService(wkspace, this.outChannel, this.testCollectionStorage, this.testResultsService, this.testsHelper, this.debugLauncher); + } +} diff --git a/src/client/unittests/common/testResultsService.ts b/src/client/unittests/common/testResultsService.ts new file mode 100644 index 000000000000..8743b579d879 --- /dev/null +++ b/src/client/unittests/common/testResultsService.ts @@ -0,0 +1,110 @@ +import { TestResultResetVisitor } from './testVisitors/resultResetVisitor'; +import { ITestResultsService, TestFile, TestFolder, Tests, TestStatus, TestSuite } from './types'; + +export class TestResultsService implements ITestResultsService { + public resetResults(tests: Tests): void { + const resultResetVisitor = new TestResultResetVisitor(); + tests.testFolders.forEach(f => resultResetVisitor.visitTestFolder(f)); + tests.testFunctions.forEach(fn => resultResetVisitor.visitTestFunction(fn.testFunction)); + tests.testSuites.forEach(suite => resultResetVisitor.visitTestSuite(suite.testSuite)); + tests.testFiles.forEach(testFile => resultResetVisitor.visitTestFile(testFile)); + } + public updateResults(tests: Tests): void { + tests.testFiles.forEach(test => this.updateTestFileResults(test)); + tests.testFolders.forEach(folder => this.updateTestFolderResults(folder)); + } + private updateTestSuiteResults(test: TestSuite): void { + this.updateTestSuiteAndFileResults(test); + } + private updateTestFileResults(test: TestFile): void { + this.updateTestSuiteAndFileResults(test); + } + private updateTestFolderResults(testFolder: TestFolder): void { + let allFilesPassed = true; + let allFilesRan = true; + + testFolder.testFiles.forEach(fl => { + if (allFilesPassed && typeof fl.passed === 'boolean') { + if (!fl.passed) { + allFilesPassed = false; + } + } else { + allFilesRan = false; + } + + testFolder.functionsFailed += fl.functionsFailed; + testFolder.functionsPassed += fl.functionsPassed; + }); + + let allFoldersPassed = true; + let allFoldersRan = true; + + testFolder.folders.forEach(folder => { + this.updateTestFolderResults(folder); + if (allFoldersPassed && typeof folder.passed === 'boolean') { + if (!folder.passed) { + allFoldersPassed = false; + } + } else { + allFoldersRan = false; + } + + testFolder.functionsFailed += folder.functionsFailed; + testFolder.functionsPassed += folder.functionsPassed; + }); + + if (allFilesRan && allFoldersRan) { + testFolder.passed = allFilesPassed && allFoldersPassed; + testFolder.status = testFolder.passed ? TestStatus.Idle : TestStatus.Fail; + } else { + testFolder.passed = null; + testFolder.status = TestStatus.Unknown; + } + } + private updateTestSuiteAndFileResults(test: TestSuite | TestFile): void { + let totalTime = 0; + let allFunctionsPassed = true; + let allFunctionsRan = true; + + test.functions.forEach(fn => { + totalTime += fn.time; + if (typeof fn.passed === 'boolean') { + if (fn.passed) { + test.functionsPassed += 1; + } else { + test.functionsFailed += 1; + allFunctionsPassed = false; + } + } else { + allFunctionsRan = false; + } + }); + + let allSuitesPassed = true; + let allSuitesRan = true; + + test.suites.forEach(suite => { + this.updateTestSuiteResults(suite); + totalTime += suite.time; + if (allSuitesRan && typeof suite.passed === 'boolean') { + if (!suite.passed) { + allSuitesPassed = false; + } + } else { + allSuitesRan = false; + } + + test.functionsFailed += suite.functionsFailed; + test.functionsPassed += suite.functionsPassed; + }); + + test.time = totalTime; + if (allSuitesRan && allFunctionsRan) { + test.passed = allFunctionsPassed && allSuitesPassed; + test.status = test.passed ? TestStatus.Idle : TestStatus.Error; + } else { + test.passed = null; + test.status = TestStatus.Unknown; + } + } +} diff --git a/src/client/unittests/common/testUtils.ts b/src/client/unittests/common/testUtils.ts index 9df1e2ca73ef..7ce389bdf66f 100644 --- a/src/client/unittests/common/testUtils.ts +++ b/src/client/unittests/common/testUtils.ts @@ -1,9 +1,24 @@ -import {TestFolder, TestsToRun, Tests, TestFile, TestSuite, TestStatus, FlattenedTestFunction, FlattenedTestSuite} from './contracts'; -import * as vscode from 'vscode'; import * as path from 'path'; +import * as vscode from 'vscode'; +import { Uri, workspace } from 'vscode'; +import { window } from 'vscode'; import * as constants from '../../common/constants'; - -let discoveredTests: Tests; +import { TestFlatteningVisitor } from './testVisitors/flatteningVisitor'; +import { TestResultResetVisitor } from './testVisitors/resultResetVisitor'; +import { TestFile, TestFolder, Tests, TestsToRun } from './types'; +import { ITestsHelper } from './types'; + +export async function selectTestWorkspace(): Promise { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return undefined; + } else if (workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri; + } else { + // tslint:disable-next-line:no-any prefer-type-cast + const workspaceFolder = await (window as any).showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); + return workspaceFolder ? workspaceFolder.uri : undefined; + } +} export function displayTestErrorMessage(message: string) { vscode.window.showErrorMessage(message, constants.Button_Text_Tests_View_Output).then(action => { @@ -13,257 +28,90 @@ export function displayTestErrorMessage(message: string) { }); } -export function getDiscoveredTests(): Tests { - return discoveredTests; -} -export function storeDiscoveredTests(tests: Tests) { - discoveredTests = tests; -} - -export function resolveValueAsTestToRun(name: string, rootDirectory: string): TestsToRun { - // TODO: We need a better way to match (currently we have raw name, name, xmlname, etc = which one do we - // use to identify a file given the full file name, similary for a folder and function - // Perhaps something like a parser or methods like TestFunction.fromString()... something) - let tests = getDiscoveredTests(); - if (!tests) { return null; } - const absolutePath = path.isAbsolute(name) ? name : path.resolve(rootDirectory, name); - let testFolders = tests.testFolders.filter(folder => folder.nameToRun === name || folder.name === name || folder.name === absolutePath); - if (testFolders.length > 0) { return { testFolder: testFolders }; }; - - let testFiles = tests.testFiles.filter(file => file.nameToRun === name || file.name === name || file.fullPath === absolutePath); - if (testFiles.length > 0) { return { testFile: testFiles }; }; - let testFns = tests.testFunctions.filter(fn => fn.testFunction.nameToRun === name || fn.testFunction.name === name).map(fn => fn.testFunction); - if (testFns.length > 0) { return { testFunction: testFns }; }; - - // Just return this as a test file - return { testFile: [{ name: name, nameToRun: name, functions: [], suites: [], xmlName: name, fullPath: '', time: 0 }] }; -} export function extractBetweenDelimiters(content: string, startDelimiter: string, endDelimiter: string): string { content = content.substring(content.indexOf(startDelimiter) + startDelimiter.length); return content.substring(0, content.lastIndexOf(endDelimiter)); } export function convertFileToPackage(filePath: string): string { - let lastIndex = filePath.lastIndexOf('.'); + const lastIndex = filePath.lastIndexOf('.'); return filePath.substring(0, lastIndex).replace(/\//g, '.').replace(/\\/g, '.'); } -export function updateResults(tests: Tests) { - tests.testFiles.forEach(updateResultsUpstream); - tests.testFolders.forEach(updateFolderResultsUpstream); -} +export class TestsHelper implements ITestsHelper { + public flattenTestFiles(testFiles: TestFile[]): Tests { + const flatteningVisitor = new TestFlatteningVisitor(); + testFiles.forEach(testFile => flatteningVisitor.visitTestFile(testFile)); -export function updateFolderResultsUpstream(testFolder: TestFolder) { - let allFilesPassed = true; - let allFilesRan = true; - - testFolder.testFiles.forEach(fl => { - if (allFilesPassed && typeof fl.passed === 'boolean') { - if (!fl.passed) { - allFilesPassed = false; - } - } - else { - allFilesRan = false; - } - - testFolder.functionsFailed += fl.functionsFailed; - testFolder.functionsPassed += fl.functionsPassed; - }); + const tests = { + testFiles: testFiles, + testFunctions: flatteningVisitor.flattenedTestFunctions, + testSuites: flatteningVisitor.flattenedTestSuites, + testFolders: [], + rootTestFolders: [], + summary: { passed: 0, failures: 0, errors: 0, skipped: 0 } + }; - let allFoldersPassed = true; - let allFoldersRan = true; + this.placeTestFilesIntoFolders(tests); - testFolder.folders.forEach(folder => { - updateFolderResultsUpstream(folder); - if (allFoldersPassed && typeof folder.passed === 'boolean') { - if (!folder.passed) { - allFoldersPassed = false; - } - } - else { - allFoldersRan = false; - } - - testFolder.functionsFailed += folder.functionsFailed; - testFolder.functionsPassed += folder.functionsPassed; - }); - - if (allFilesRan && allFoldersRan) { - testFolder.passed = allFilesPassed && allFoldersPassed; - testFolder.status = testFolder.passed ? TestStatus.Idle : TestStatus.Fail; - } - else { - testFolder.passed = null; - testFolder.status = TestStatus.Unknown; + return tests; } -} - -export function updateResultsUpstream(test: TestSuite | TestFile) { - let totalTime = 0; - let allFunctionsPassed = true; - let allFunctionsRan = true; - - test.functions.forEach(fn => { - totalTime += fn.time; - if (typeof fn.passed === 'boolean') { - if (fn.passed) { - test.functionsPassed += 1; - } - else { - test.functionsFailed += 1; - allFunctionsPassed = false; - } - } - else { - allFunctionsRan = false; - } - }); - - let allSuitesPassed = true; - let allSuitesRan = true; - - test.suites.forEach(suite => { - updateResultsUpstream(suite); - totalTime += suite.time; - if (allSuitesRan && typeof suite.passed === 'boolean') { - if (!suite.passed) { - allSuitesPassed = false; + public placeTestFilesIntoFolders(tests: Tests): void { + // First get all the unique folders + const folders: string[] = []; + tests.testFiles.forEach(file => { + const dir = path.dirname(file.name); + if (folders.indexOf(dir) === -1) { + folders.push(dir); } - } - else { - allSuitesRan = false; - } - - test.functionsFailed += suite.functionsFailed; - test.functionsPassed += suite.functionsPassed; - }); - - test.time = totalTime; - if (allSuitesRan && allFunctionsRan) { - test.passed = allFunctionsPassed && allSuitesPassed; - test.status = test.passed ? TestStatus.Idle : TestStatus.Error; - } - else { - test.passed = null; - test.status = TestStatus.Unknown; - } -} - -export function placeTestFilesInFolders(tests: Tests) { - // First get all the unique folders - const folders: string[] = []; - tests.testFiles.forEach(file => { - let dir = path.dirname(file.name); - if (folders.indexOf(dir) === -1) { - folders.push(dir); - } - }); - - tests.testFolders = []; - const folderMap = new Map(); - folders.sort(); + }); - folders.forEach(dir => { - dir.split(path.sep).reduce((parentPath, currentName, index, values) => { - let newPath = currentName; - let parentFolder: TestFolder; - if (parentPath.length > 0) { - parentFolder = folderMap.get(parentPath); - newPath = path.join(parentPath, currentName); - } - if (!folderMap.has(newPath)) { - const testFolder: TestFolder = { name: newPath, testFiles: [], folders: [], nameToRun: newPath, time: 0 }; - folderMap.set(newPath, testFolder); - if (parentFolder) { - parentFolder.folders.push(testFolder); + tests.testFolders = []; + const folderMap = new Map(); + folders.sort(); + + folders.forEach(dir => { + dir.split(path.sep).reduce((parentPath, currentName, index, values) => { + let newPath = currentName; + let parentFolder: TestFolder; + if (parentPath.length > 0) { + parentFolder = folderMap.get(parentPath); + newPath = path.join(parentPath, currentName); } - else { - tests.rootTestFolders.push(testFolder); + if (!folderMap.has(newPath)) { + const testFolder: TestFolder = { name: newPath, testFiles: [], folders: [], nameToRun: newPath, time: 0 }; + folderMap.set(newPath, testFolder); + if (parentFolder) { + parentFolder.folders.push(testFolder); + } else { + tests.rootTestFolders.push(testFolder); + } + tests.testFiles.filter(fl => path.dirname(fl.name) === newPath).forEach(testFile => { + testFolder.testFiles.push(testFile); + }); + tests.testFolders.push(testFolder); } - tests.testFiles.filter(fl => path.dirname(fl.name) === newPath).forEach(testFile => { - testFolder.testFiles.push(testFile); - }); - tests.testFolders.push(testFolder); - } - return newPath; - }, ''); - }); -} -export function flattenTestFiles(testFiles: TestFile[]): Tests { - let fns: FlattenedTestFunction[] = []; - let suites: FlattenedTestSuite[] = []; - testFiles.forEach(testFile => { - // sample test_three (file name without extension and all / replaced with ., meaning this is the package) - const packageName = convertFileToPackage(testFile.name); - - testFile.functions.forEach(fn => { - fns.push({ testFunction: fn, xmlClassName: packageName, parentTestFile: testFile }); + return newPath; + }, ''); }); - - testFile.suites.forEach(suite => { - suites.push({ parentTestFile: testFile, testSuite: suite, xmlClassName: suite.xmlName }); - flattenTestSuites(fns, suites, testFile, suite); - }); - }); - - let tests = { - testFiles: testFiles, - testFunctions: fns, testSuits: suites, - testFolders: [], - rootTestFolders: [], - summary: { passed: 0, failures: 0, errors: 0, skipped: 0 } - }; - - placeTestFilesInFolders(tests); - - return tests; -} -export function flattenTestSuites(flattenedFns: FlattenedTestFunction[], flattenedSuites: FlattenedTestSuite[], testFile: TestFile, testSuite: TestSuite) { - testSuite.functions.forEach(fn => { - flattenedFns.push({ testFunction: fn, xmlClassName: testSuite.xmlName, parentTestFile: testFile, parentTestSuite: testSuite }); - }); - - // We may have child classes - testSuite.suites.forEach(suite => { - flattenedSuites.push({ parentTestFile: testFile, testSuite: suite, xmlClassName: suite.xmlName }); - flattenTestSuites(flattenedFns, flattenedSuites, testFile, suite); - }); + } + public parseTestName(name: string, rootDirectory: string, tests: Tests): TestsToRun { + // TODO: We need a better way to match (currently we have raw name, name, xmlname, etc = which one do we. + // Use to identify a file given the full file name, similarly for a folder and function. + // Perhaps something like a parser or methods like TestFunction.fromString()... something). + if (!tests) { return null; } + const absolutePath = path.isAbsolute(name) ? name : path.resolve(rootDirectory, name); + const testFolders = tests.testFolders.filter(folder => folder.nameToRun === name || folder.name === name || folder.name === absolutePath); + if (testFolders.length > 0) { return { testFolder: testFolders }; } + + const testFiles = tests.testFiles.filter(file => file.nameToRun === name || file.name === name || file.fullPath === absolutePath); + if (testFiles.length > 0) { return { testFile: testFiles }; } + + const testFns = tests.testFunctions.filter(fn => fn.testFunction.nameToRun === name || fn.testFunction.name === name).map(fn => fn.testFunction); + if (testFns.length > 0) { return { testFunction: testFns }; } + + // Just return this as a test file. + return { testFile: [{ name: name, nameToRun: name, functions: [], suites: [], xmlName: name, fullPath: '', time: 0 }] }; + } } - -export function resetTestResults(tests: Tests) { - tests.testFolders.forEach(f => { - f.functionsDidNotRun = 0; - f.functionsFailed = 0; - f.functionsPassed = 0; - f.passed = null; - f.status = TestStatus.Unknown; - }); - tests.testFunctions.forEach(fn => { - fn.testFunction.passed = null; - fn.testFunction.time = 0; - fn.testFunction.message = ''; - fn.testFunction.traceback = ''; - fn.testFunction.status = TestStatus.Unknown; - fn.testFunction.functionsFailed = 0; - fn.testFunction.functionsPassed = 0; - fn.testFunction.functionsDidNotRun = 0; - }); - tests.testSuits.forEach(suite => { - suite.testSuite.passed = null; - suite.testSuite.time = 0; - suite.testSuite.status = TestStatus.Unknown; - suite.testSuite.functionsFailed = 0; - suite.testSuite.functionsPassed = 0; - suite.testSuite.functionsDidNotRun = 0; - }); - tests.testFiles.forEach(testFile => { - testFile.passed = null; - testFile.time = 0; - testFile.status = TestStatus.Unknown; - testFile.functionsFailed = 0; - testFile.functionsPassed = 0; - testFile.functionsDidNotRun = 0; - }); -} \ No newline at end of file diff --git a/src/client/unittests/common/testVisitors/flatteningVisitor.ts b/src/client/unittests/common/testVisitors/flatteningVisitor.ts new file mode 100644 index 000000000000..4a8ed96babbc --- /dev/null +++ b/src/client/unittests/common/testVisitors/flatteningVisitor.ts @@ -0,0 +1,65 @@ +import { convertFileToPackage } from '../testUtils'; +import { + FlattenedTestFunction, + FlattenedTestSuite, + ITestVisitor, + TestFile, + TestFolder, + TestFunction, + TestSuite +} from '../types'; + +export class TestFlatteningVisitor implements ITestVisitor { + // tslint:disable-next-line:variable-name + private _flattedTestFunctions = new Map(); + // tslint:disable-next-line:variable-name + private _flattenedTestSuites = new Map(); + public get flattenedTestFunctions(): FlattenedTestFunction[] { + return [...this._flattedTestFunctions.values()]; + } + public get flattenedTestSuites(): FlattenedTestSuite[] { + return [...this._flattenedTestSuites.values()]; + } + // tslint:disable-next-line:no-empty + public visitTestFunction(testFunction: TestFunction): void { } + // tslint:disable-next-line:no-empty + public visitTestSuite(testSuite: TestSuite): void { } + public visitTestFile(testFile: TestFile): void { + // sample test_three (file name without extension and all / replaced with ., meaning this is the package) + const packageName = convertFileToPackage(testFile.name); + + testFile.functions.forEach(fn => this.addTestFunction(fn, testFile, packageName)); + testFile.suites.forEach(suite => this.visitTestSuiteOfAFile(suite, testFile)); + } + // tslint:disable-next-line:no-empty + public visitTestFolder(testFile: TestFolder) { } + private visitTestSuiteOfAFile(testSuite: TestSuite, parentTestFile: TestFile): void { + testSuite.functions.forEach(fn => this.visitTestFunctionOfASuite(fn, testSuite, parentTestFile)); + testSuite.suites.forEach(suite => this.visitTestSuiteOfAFile(suite, parentTestFile)); + this.addTestSuite(testSuite, parentTestFile); + } + private visitTestFunctionOfASuite(testFunction: TestFunction, parentTestSuite: TestSuite, parentTestFile: TestFile) { + const key = `Function:${testFunction.name},Suite:${parentTestSuite.name},SuiteXmlName:${parentTestSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; + if (this._flattenedTestSuites.has(key)) { + return; + } + const flattenedFunction = { testFunction, xmlClassName: parentTestSuite.xmlName, parentTestFile, parentTestSuite }; + this._flattedTestFunctions.set(key, flattenedFunction); + } + private addTestSuite(testSuite: TestSuite, parentTestFile: TestFile) { + const key = `Suite:${testSuite.name},SuiteXmlName:${testSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; + if (this._flattenedTestSuites.has(key)) { + return; + } + const flattenedSuite = { parentTestFile, testSuite, xmlClassName: testSuite.xmlName }; + this._flattenedTestSuites.set(key, flattenedSuite); + } + private addTestFunction(testFunction: TestFunction, parentTestFile: TestFile, parentTestPackage: string) { + const key = `Function:${testFunction.name},ParentFile:${parentTestFile.fullPath}`; + if (this._flattedTestFunctions.has(key)) { + return; + } + const flattendFunction = { testFunction, xmlClassName: parentTestPackage, parentTestFile }; + this._flattedTestFunctions.set(key, flattendFunction); + } +} diff --git a/src/client/unittests/common/testVisitors/folderGenerationVisitor.ts b/src/client/unittests/common/testVisitors/folderGenerationVisitor.ts new file mode 100644 index 000000000000..46956873edd1 --- /dev/null +++ b/src/client/unittests/common/testVisitors/folderGenerationVisitor.ts @@ -0,0 +1,55 @@ +import * as path from 'path'; +import { ITestVisitor, TestFile, TestFolder, TestFunction, TestSuite } from '../types'; + +export class TestFolderGenerationVisitor implements ITestVisitor { + // tslint:disable-next-line:variable-name + private _testFolders: TestFolder[] = []; + // tslint:disable-next-line:variable-name + private _rootTestFolders: TestFolder[] = []; + private folderMap = new Map(); + public get testFolders(): Readonly { + return [...this._testFolders]; + } + public get rootTestFolders(): Readonly { + return [...this._rootTestFolders]; + } + // tslint:disable-next-line:no-empty + public visitTestFunction(testFunction: TestFunction): void { } + // tslint:disable-next-line:no-empty + public visitTestSuite(testSuite: TestSuite): void { } + public visitTestFile(testFile: TestFile): void { + // First get all the unique folders + const folders: string[] = []; + const dir = path.dirname(testFile.name); + if (this.folderMap.has(dir)) { + const folder = this.folderMap.get(dir); + folder.testFiles.push(testFile); + return; + } + + dir.split(path.sep).reduce((accumulatedPath, currentName, index) => { + let newPath = currentName; + let parentFolder: TestFolder; + if (accumulatedPath.length > 0) { + parentFolder = this.folderMap.get(accumulatedPath); + newPath = path.join(accumulatedPath, currentName); + } + if (!this.folderMap.has(newPath)) { + const testFolder: TestFolder = { name: newPath, testFiles: [], folders: [], nameToRun: newPath, time: 0 }; + this.folderMap.set(newPath, testFolder); + if (parentFolder) { + parentFolder.folders.push(testFolder); + } else { + this._rootTestFolders.push(testFolder); + } + this._testFolders.push(testFolder); + } + return newPath; + }, ''); + + // tslint:disable-next-line:no-non-null-assertion + this.folderMap.get(dir)!.testFiles.push(testFile); + } + // tslint:disable-next-line:no-empty + public visitTestFolder(testFile: TestFolder) { } +} diff --git a/src/client/unittests/common/testVisitors/resultResetVisitor.ts b/src/client/unittests/common/testVisitors/resultResetVisitor.ts new file mode 100644 index 000000000000..0d58c1076b04 --- /dev/null +++ b/src/client/unittests/common/testVisitors/resultResetVisitor.ts @@ -0,0 +1,37 @@ +import { ITestVisitor, TestFile, TestFolder, TestFunction, TestStatus, TestSuite } from '../types'; + +export class TestResultResetVisitor implements ITestVisitor { + public visitTestFunction(testFunction: TestFunction): void { + testFunction.passed = null; + testFunction.time = 0; + testFunction.message = ''; + testFunction.traceback = ''; + testFunction.status = TestStatus.Unknown; + testFunction.functionsFailed = 0; + testFunction.functionsPassed = 0; + testFunction.functionsDidNotRun = 0; + } + public visitTestSuite(testSuite: TestSuite): void { + testSuite.passed = null; + testSuite.time = 0; + testSuite.status = TestStatus.Unknown; + testSuite.functionsFailed = 0; + testSuite.functionsPassed = 0; + testSuite.functionsDidNotRun = 0; + } + public visitTestFile(testFile: TestFile): void { + testFile.passed = null; + testFile.time = 0; + testFile.status = TestStatus.Unknown; + testFile.functionsFailed = 0; + testFile.functionsPassed = 0; + testFile.functionsDidNotRun = 0; + } + public visitTestFolder(testFolder: TestFolder) { + testFolder.functionsDidNotRun = 0; + testFolder.functionsFailed = 0; + testFolder.functionsPassed = 0; + testFolder.passed = null; + testFolder.status = TestStatus.Unknown; + } +} diff --git a/src/client/unittests/common/types.ts b/src/client/unittests/common/types.ts new file mode 100644 index 000000000000..5125b5a4c17c --- /dev/null +++ b/src/client/unittests/common/types.ts @@ -0,0 +1,152 @@ +import { CancellationToken, Disposable, OutputChannel, Uri } from 'vscode'; +import { Product } from '../../common/installer'; +import { BaseTestManager } from './baseTestManager'; + +export type TestFolder = TestResult & { + name: string; + testFiles: TestFile[]; + nameToRun: string; + status?: TestStatus; + folders: TestFolder[]; +}; + +export type TestFile = TestResult & { + name: string; + fullPath: string; + functions: TestFunction[]; + suites: TestSuite[]; + nameToRun: string; + xmlName: string; + status?: TestStatus; + errorsWhenDiscovering?: string; +}; + +export type TestSuite = TestResult & { + name: string; + functions: TestFunction[]; + suites: TestSuite[]; + isUnitTest: Boolean; + isInstance: Boolean; + nameToRun: string; + xmlName: string; + status?: TestStatus; +}; + +export type TestFunction = TestResult & { + name: string; + nameToRun: string; + status?: TestStatus; +}; + +export type TestResult = Node & { + passed?: boolean; + time: number; + line?: number; + message?: string; + traceback?: string; + functionsPassed?: number; + functionsFailed?: number; + functionsDidNotRun?: number; +}; + +export type Node = { + expanded?: Boolean; +}; + +export type FlattenedTestFunction = { + testFunction: TestFunction; + parentTestSuite?: TestSuite; + parentTestFile: TestFile; + xmlClassName: string; +}; + +export type FlattenedTestSuite = { + testSuite: TestSuite; + parentTestSuite?: TestSuite; + parentTestFile: TestFile; + xmlClassName: string; +}; + +export type TestSummary = { + passed: number; + failures: number; + errors: number; + skipped: number; +}; + +export type Tests = { + summary: TestSummary; + testFiles: TestFile[]; + testFunctions: FlattenedTestFunction[]; + testSuites: FlattenedTestSuite[]; + testFolders: TestFolder[]; + rootTestFolders: TestFolder[]; +}; + +export enum TestStatus { + Unknown, + Discovering, + Idle, + Running, + Fail, + Error, + Skipped, + Pass +} + +export type TestsToRun = { + testFolder?: TestFolder[]; + testFile?: TestFile[]; + testSuite?: TestSuite[]; + testFunction?: TestFunction[]; +}; + +export type UnitTestProduct = Product.nosetest | Product.pytest | Product.unittest; + +export interface ITestConfigSettingsService { + updateTestArgs(testDirectory: string, product: UnitTestProduct, args: string[]): Promise; + enable(testDirectory: string | Uri, product: UnitTestProduct): Promise; + disable(testDirectory: string | Uri, product: UnitTestProduct): Promise; +} + +export interface ITestManagerService extends Disposable { + getTestManager(): BaseTestManager | undefined; + getTestWorkingDirectory(): string; + getPreferredTestManager(): UnitTestProduct | undefined; +} + +export interface ITestManagerServiceFactory { + createTestManagerService(wkspace: Uri): ITestManagerService; +} + +export interface IWorkspaceTestManagerService extends Disposable { + getTestManager(resource: Uri): BaseTestManager | undefined; + getTestWorkingDirectory(resource: Uri): string; + getPreferredTestManager(resource: Uri): UnitTestProduct | undefined; +} + +export interface ITestsHelper { + flattenTestFiles(testFiles: TestFile[]): Tests; + placeTestFilesIntoFolders(tests: Tests): void; +} + +export interface ITestVisitor { + visitTestFunction(testFunction: TestFunction): void; + visitTestSuite(testSuite: TestSuite): void; + visitTestFile(testFile: TestFile): void; + visitTestFolder(testFile: TestFolder): void; +} + +export interface ITestCollectionStorageService extends Disposable { + getTests(wkspace: Uri): Tests | undefined; + storeTests(wkspace: Uri, tests: Tests | null | undefined): void; +} + +export interface ITestResultsService { + resetResults(tests: Tests): void; + updateResults(tests: Tests): void; +} + +export interface ITestDebugLauncher { + launchDebugger(rootDirectory: string, testArgs: string[], token?: CancellationToken, outChannel?: OutputChannel): Promise; +} diff --git a/src/client/unittests/common/workspaceTestManagerService.ts b/src/client/unittests/common/workspaceTestManagerService.ts new file mode 100644 index 000000000000..ff7e198cb423 --- /dev/null +++ b/src/client/unittests/common/workspaceTestManagerService.ts @@ -0,0 +1,53 @@ +import { Disposable, OutputChannel, Uri, workspace } from 'vscode'; +import { Product } from '../../common/installer'; +import { BaseTestManager } from './baseTestManager'; +import { TestManagerService } from './testManagerService'; +import { ITestManagerService, ITestManagerServiceFactory, IWorkspaceTestManagerService, UnitTestProduct } from './types'; + +export class WorkspaceTestManagerService implements IWorkspaceTestManagerService, Disposable { + private workspaceTestManagers = new Map(); + private disposables: Disposable[] = []; + constructor(private outChannel: OutputChannel, + private testManagerServiceFactory: ITestManagerServiceFactory) { + } + public dispose() { + this.workspaceTestManagers.forEach(info => info.dispose()); + } + public getTestManager(resource: Uri): BaseTestManager | undefined { + const wkspace = this.getWorkspace(resource); + this.ensureTestManagerService(wkspace); + return this.workspaceTestManagers.get(wkspace.fsPath).getTestManager(); + } + public getTestWorkingDirectory(resource: Uri) { + const wkspace = this.getWorkspace(resource); + this.ensureTestManagerService(wkspace); + return this.workspaceTestManagers.get(wkspace.fsPath).getTestWorkingDirectory(); + } + public getPreferredTestManager(resource: Uri): UnitTestProduct { + const wkspace = this.getWorkspace(resource); + this.ensureTestManagerService(wkspace); + return this.workspaceTestManagers.get(wkspace.fsPath).getPreferredTestManager(); + } + private getWorkspace(resource: Uri): Uri { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + const noWkspaceMessage = 'Please open a workspace'; + this.outChannel.appendLine(noWkspaceMessage); + throw new Error(noWkspaceMessage); + } + if (!resource || workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri; + } + const workspaceFolder = workspace.getWorkspaceFolder(resource); + if (workspaceFolder) { + return workspaceFolder.uri; + } + const message = `Resource '${resource.fsPath}' does not belong to any workspace`; + this.outChannel.appendLine(message); + throw new Error(message); + } + private ensureTestManagerService(wkspace: Uri) { + if (!this.workspaceTestManagers.has(wkspace.fsPath)) { + this.workspaceTestManagers.set(wkspace.fsPath, this.testManagerServiceFactory.createTestManagerService(wkspace)); + } + } +} diff --git a/src/client/unittests/common/xUnitParser.ts b/src/client/unittests/common/xUnitParser.ts index 2f02601266a0..6318060a94f7 100644 --- a/src/client/unittests/common/xUnitParser.ts +++ b/src/client/unittests/common/xUnitParser.ts @@ -1,12 +1,12 @@ import * as fs from 'fs'; import * as xml2js from 'xml2js'; -import { Tests, TestStatus } from './contracts'; +import { Tests, TestStatus } from './types'; export enum PassCalculationFormulae { pytest, nosetests } -interface TestSuiteResult { +type TestSuiteResult = { $: { errors: string; failures: string; @@ -17,8 +17,8 @@ interface TestSuiteResult { time: string; }; testcase: TestCaseResult[]; -} -interface TestCaseResult { +}; +type TestCaseResult = { $: { classname: string; file: string; @@ -38,30 +38,32 @@ interface TestCaseResult { _: string; $: { message: string, type: string } }[]; -} +}; +// tslint:disable-next-line:no-any function getSafeInt(value: string, defaultValue: any = 0): number { - const num = parseInt(value); + const num = parseInt(value, 10); if (isNaN(num)) { return defaultValue; } return num; } -export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, passCalculationFormulae: PassCalculationFormulae): Promise { +export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, passCalculationFormulae: PassCalculationFormulae): Promise<{}> { + // tslint:disable-next-line:no-any return new Promise((resolve, reject) => { fs.readFile(outputXmlFile, 'utf8', (err, data) => { if (err) { return reject(err); } - xml2js.parseString(data, (err, result) => { - if (err) { - return reject(err); + xml2js.parseString(data, (error, parserResult) => { + if (error) { + return reject(error); } - let testSuiteResult: TestSuiteResult = result.testsuite; + const testSuiteResult: TestSuiteResult = parserResult.testsuite; tests.summary.errors = getSafeInt(testSuiteResult.$.errors); tests.summary.failures = getSafeInt(testSuiteResult.$.failures); tests.summary.skipped = getSafeInt(testSuiteResult.$.skips ? testSuiteResult.$.skips : testSuiteResult.$.skip); - let testCount = getSafeInt(testSuiteResult.$.tests); + const testCount = getSafeInt(testSuiteResult.$.tests); switch (passCalculationFormulae) { case PassCalculationFormulae.pytest: { @@ -73,7 +75,7 @@ export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, break; } default: { - throw new Error("Unknown Test Pass Calculation"); + throw new Error('Unknown Test Pass Calculation'); } } @@ -83,7 +85,7 @@ export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, testSuiteResult.testcase.forEach((testcase: TestCaseResult) => { const xmlClassName = testcase.$.classname.replace(/\(\)/g, '').replace(/\.\./g, '.').replace(/\.\./g, '.').replace(/\.+$/, ''); - let result = tests.testFunctions.find(fn => fn.xmlClassName === xmlClassName && fn.testFunction.name === testcase.$.name); + const result = tests.testFunctions.find(fn => fn.xmlClassName === xmlClassName && fn.testFunction.name === testcase.$.name); if (!result) { // Possible we're dealing with nosetests, where the file name isn't returned to us // When dealing with nose tests @@ -94,7 +96,7 @@ export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, // fn.parentTestSuite && fn.parentTestSuite.name === testcase.$.classname); // Look for failed file test - let fileTest = testcase.$.file && tests.testFiles.find(file => file.nameToRun === testcase.$.file); + const fileTest = testcase.$.file && tests.testFiles.find(file => file.nameToRun === testcase.$.file); if (fileTest && testcase.error) { fileTest.status = TestStatus.Error; fileTest.passed = false; @@ -109,7 +111,6 @@ export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, result.testFunction.passed = true; result.testFunction.status = TestStatus.Pass; - if (testcase.failure) { result.testFunction.status = TestStatus.Fail; result.testFunction.passed = false; diff --git a/src/client/unittests/configuration.ts b/src/client/unittests/configuration.ts index 0c1f47f7bf45..ecbefeae8d4d 100644 --- a/src/client/unittests/configuration.ts +++ b/src/client/unittests/configuration.ts @@ -1,150 +1,159 @@ 'use strict'; +import * as path from 'path'; import * as vscode from 'vscode'; +import { OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../common/configSettings'; -import { Product } from '../common/installer'; +import { Installer, Product } from '../common/installer'; +import { getSubDirectories } from '../common/utils'; +import { TestConfigSettingsService } from './common/configSettingService'; import { TestConfigurationManager } from './common/testConfigurationManager'; +import { selectTestWorkspace } from './common/testUtils'; +import { UnitTestProduct } from './common/types'; +import { ConfigurationManager } from './nosetest/testConfigurationManager'; import * as nose from './nosetest/testConfigurationManager'; import * as pytest from './pytest/testConfigurationManager'; import * as unittest from './unittest/testConfigurationManager'; -import { getSubDirectories } from '../common/utils'; -import * as path from 'path'; - -const settings = PythonSettings.getInstance(); -function promptToEnableAndConfigureTestFramework(outputChannel: vscode.OutputChannel, messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false): Thenable { - const items = [{ - label: 'unittest', - product: Product.unittest, - description: 'Standard Python test framework', - detail: 'https://docs.python.org/2/library/unittest.html' - }, - { - label: 'pytest', - product: Product.pytest, - description: 'Can run unittest (including trial) and nose test suites out of the box', - detail: 'http://docs.pytest.org/en/latest/' - }, - { - label: 'nose', - product: Product.nosetest, - description: 'nose framework', - detail: 'https://docs.python.org/2/library/unittest.html' - }]; - const options = { - matchOnDescription: true, - matchOnDetail: true, - placeHolder: messageToDisplay - }; - return vscode.window.showQuickPick(items, options).then(item => { - if (!item) { - return Promise.reject(null); +// tslint:disable-next-line:no-any +async function promptToEnableAndConfigureTestFramework(wkspace: Uri, outputChannel: vscode.OutputChannel, messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false) { + const selectedTestRunner = await selectTestRunner(messageToDisplay); + if (typeof selectedTestRunner !== 'number') { + return Promise.reject(null); + } + const configMgr: TestConfigurationManager = createTestConfigurationManager(wkspace, selectedTestRunner, outputChannel); + if (enableOnly) { + // Ensure others are disabled + if (selectedTestRunner !== Product.unittest) { + createTestConfigurationManager(wkspace, Product.unittest, outputChannel).disable(); } - let configMgr: TestConfigurationManager; - switch (item.product) { - case Product.unittest: { - configMgr = new unittest.ConfigurationManager(outputChannel); - break; - } - case Product.pytest: { - configMgr = new pytest.ConfigurationManager(outputChannel); - break; - } - case Product.nosetest: { - configMgr = new nose.ConfigurationManager(outputChannel); - break; - } - default: { - throw new Error('Invalid test configuration'); - } + if (selectedTestRunner !== Product.pytest) { + createTestConfigurationManager(wkspace, Product.pytest, outputChannel).disable(); } - - if (enableOnly) { - // Ensure others are disabled - if (item.product !== Product.unittest) { - (new unittest.ConfigurationManager(outputChannel)).disable(); - } - if (item.product !== Product.pytest) { - (new pytest.ConfigurationManager(outputChannel)).disable(); - } - if (item.product !== Product.nosetest) { - (new nose.ConfigurationManager(outputChannel)).disable(); - } - return configMgr.enable(); + if (selectedTestRunner !== Product.nosetest) { + createTestConfigurationManager(wkspace, Product.nosetest, outputChannel).disable(); } + return configMgr.enable(); + } - // Configure everything before enabling - // Cuz we don't want the test engine (in main.ts file - tests get discovered when config changes are detected) - // to start discovering tests when tests haven't been configured properly - function enableTest(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - if (settings.unitTest.promptToConfigure) { - return configMgr.enable(); - } - return pythonConfig.update('unitTest.promptToConfigure', undefined).then(() => { - return configMgr.enable(); - }, reason => { - return configMgr.enable().then(() => Promise.reject(reason)); - }); - } - return configMgr.configure(vscode.workspace.rootPath).then(() => { - return enableTest(); - }).catch(reason => { - return enableTest().then(() => Promise.reject(reason)); - }); + return configMgr.configure(wkspace).then(() => { + return enableTest(wkspace, configMgr); + }).catch(reason => { + return enableTest(wkspace, configMgr).then(() => Promise.reject(reason)); }); } -export function displayTestFrameworkError(outputChannel: vscode.OutputChannel): Thenable { +export function displayTestFrameworkError(wkspace: Uri, outputChannel: vscode.OutputChannel) { + const settings = PythonSettings.getInstance(); let enabledCount = settings.unitTest.pyTestEnabled ? 1 : 0; enabledCount += settings.unitTest.nosetestsEnabled ? 1 : 0; enabledCount += settings.unitTest.unittestEnabled ? 1 : 0; if (enabledCount > 1) { - return promptToEnableAndConfigureTestFramework(outputChannel, 'Enable only one of the test frameworks (unittest, pytest or nosetest).', true); - } - else { + return promptToEnableAndConfigureTestFramework(wkspace, outputChannel, 'Enable only one of the test frameworks (unittest, pytest or nosetest).', true); + } else { const option = 'Enable and configure a Test Framework'; return vscode.window.showInformationMessage('No test framework configured (unittest, pytest or nosetest)', option).then(item => { if (item === option) { - return promptToEnableAndConfigureTestFramework(outputChannel); + return promptToEnableAndConfigureTestFramework(wkspace, outputChannel); } return Promise.reject(null); }); } } -export function displayPromptToEnableTests(rootDir: string, outputChannel: vscode.OutputChannel): Thenable { +export async function displayPromptToEnableTests(rootDir: string, outputChannel: vscode.OutputChannel) { + const settings = PythonSettings.getInstance(vscode.Uri.file(rootDir)); if (settings.unitTest.pyTestEnabled || settings.unitTest.nosetestsEnabled || settings.unitTest.unittestEnabled) { - return Promise.reject(null); + return; } if (!settings.unitTest.promptToConfigure) { - return Promise.reject(null); + return; } const yes = 'Yes'; - const no = `Later`; - const noNotAgain = `No, don't ask again`; + const no = 'Later'; + const noNotAgain = 'No, don\'t ask again'; - return checkIfHasTestDirs(rootDir).then(hasTests => { - if (!hasTests) { - return Promise.reject(null); - } - return vscode.window.showInformationMessage('You seem to have tests, would you like to enable a test framework?', yes, no, noNotAgain).then(item => { - if (!item || item === no) { - return Promise.reject(null); - } - if (item === yes) { - return promptToEnableAndConfigureTestFramework(outputChannel); - } - else { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.promptToConfigure', false); - } - }); + const hasTests = checkForExistenceOfTests(rootDir); + if (!hasTests) { + return; + } + const item = await vscode.window.showInformationMessage('You seem to have tests, would you like to enable a test framework?', yes, no, noNotAgain); + if (!item || item === no) { + return; + } + if (item === yes) { + await promptToEnableAndConfigureTestFramework(vscode.workspace.getWorkspaceFolder(vscode.Uri.file(rootDir)).uri, outputChannel); + } else { + const pythonConfig = vscode.workspace.getConfiguration('python'); + await pythonConfig.update('unitTest.promptToConfigure', false); + } +} + +// Configure everything before enabling. +// Cuz we don't want the test engine (in main.ts file - tests get discovered when config changes are detected) +// to start discovering tests when tests haven't been configured properly. +function enableTest(wkspace: Uri, configMgr: ConfigurationManager) { + const pythonConfig = vscode.workspace.getConfiguration('python', wkspace); + // tslint:disable-next-line:no-backbone-get-set-outside-model + if (pythonConfig.get('unitTest.promptToConfigure')) { + return configMgr.enable(); + } + return pythonConfig.update('unitTest.promptToConfigure', undefined).then(() => { + return configMgr.enable(); + }, reason => { + return configMgr.enable().then(() => Promise.reject(reason)); }); } -function checkIfHasTestDirs(rootDir: string): Promise { +function checkForExistenceOfTests(rootDir: string): Promise { return getSubDirectories(rootDir).then(subDirs => { return subDirs.map(dir => path.relative(rootDir, dir)).filter(dir => dir.match(/test/i)).length > 0; }); -} \ No newline at end of file +} +function createTestConfigurationManager(wkspace: Uri, product: Product, outputChannel: OutputChannel) { + const installer = new Installer(outputChannel); + const configSettingService = new TestConfigSettingsService(); + switch (product) { + case Product.unittest: { + return new unittest.ConfigurationManager(wkspace, outputChannel, installer, configSettingService); + } + case Product.pytest: { + return new pytest.ConfigurationManager(wkspace, outputChannel, installer, configSettingService); + } + case Product.nosetest: { + return new nose.ConfigurationManager(wkspace, outputChannel, installer, configSettingService); + } + default: { + throw new Error('Invalid test configuration'); + } + } +} +async function selectTestRunner(placeHolderMessage: string): Promise { + const items = [{ + label: 'unittest', + product: Product.unittest, + description: 'Standard Python test framework', + detail: 'https://docs.python.org/2/library/unittest.html' + }, + { + label: 'pytest', + product: Product.pytest, + description: 'Can run unittest (including trial) and nose test suites out of the box', + // tslint:disable-next-line:no-http-string + detail: 'http://docs.pytest.org/en/latest/' + }, + { + label: 'nose', + product: Product.nosetest, + description: 'nose framework', + detail: 'https://docs.python.org/2/library/unittest.html' + }]; + const options = { + matchOnDescription: true, + matchOnDetail: true, + placeHolder: placeHolderMessage + }; + const selectedTestRunner = await vscode.window.showQuickPick(items, options); + // tslint:disable-next-line:prefer-type-cast + return selectedTestRunner ? selectedTestRunner.product as UnitTestProduct : undefined; +} diff --git a/src/client/unittests/display/main.ts b/src/client/unittests/display/main.ts index 7b9ee8a5a534..ab232152e295 100644 --- a/src/client/unittests/display/main.ts +++ b/src/client/unittests/display/main.ts @@ -1,12 +1,18 @@ 'use strict'; import * as vscode from 'vscode'; -import { Tests, CANCELLATION_REASON } from '../common/contracts'; import * as constants from '../../common/constants'; +import { createDeferred, isNotInstalledError } from '../../common/helpers'; +import { CANCELLATION_REASON } from '../common/constants'; import { displayTestErrorMessage } from '../common/testUtils'; -import { isNotInstalledError, createDeferred } from '../../common/helpers'; +import { Tests } from '../common/types'; export class TestResultDisplay { private statusBar: vscode.StatusBarItem; + private discoverCounter = 0; + private ticker = ['|', '/', '-', '|', '/', '-', '\\']; + private progressTimeout; + private progressPrefix: string; + // tslint:disable-next-line:no-any constructor(private outputChannel: vscode.OutputChannel, private onDidChange: vscode.EventEmitter = null) { this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); } @@ -16,19 +22,29 @@ export class TestResultDisplay { public set enabled(enable: boolean) { if (enable) { this.statusBar.show(); - } - else { + } else { this.statusBar.hide(); } } - public DisplayProgressStatus(tests: Promise, debug: boolean = false) { - this.displayProgress('Running Tests', `Running Tests (Click to Stop)`, constants.Commands.Tests_Ask_To_Stop_Test); - tests + public displayProgressStatus(testRunResult: Promise, debug: boolean = false) { + this.displayProgress('Running Tests', 'Running Tests (Click to Stop)', constants.Commands.Tests_Ask_To_Stop_Test); + testRunResult .then(tests => this.updateTestRunWithSuccess(tests, debug)) .catch(this.updateTestRunWithFailure.bind(this)) // We don't care about any other exceptions returned by updateTestRunWithFailure + // tslint:disable-next-line:no-empty .catch(() => { }); } + public displayDiscoverStatus(testDiscovery: Promise) { + this.displayProgress('Discovering Tests', 'Discovering Tests (Click to Stop)', constants.Commands.Tests_Ask_To_Stop_Discovery); + return testDiscovery.then(tests => { + this.updateWithDiscoverSuccess(tests); + return tests; + }).catch(reason => { + this.updateWithDiscoverFailure(reason); + return Promise.reject(reason); + }); + } private updateTestRunWithSuccess(tests: Tests, debug: boolean = false): Tests { this.clearProgressTicker(); @@ -58,7 +74,7 @@ export class TestResultDisplay { toolTip.push(`${tests.summary.errors} Error${tests.summary.errors > 1 ? 's' : ''}`); foreColor = 'yellow'; } - this.statusBar.tooltip = toolTip.length === 0 ? 'No Tests Ran' : toolTip.join(', ') + ' (Tests)'; + this.statusBar.tooltip = toolTip.length === 0 ? 'No Tests Ran' : `${toolTip.join(', ')} (Tests)`; this.statusBar.text = statusText.length === 0 ? 'No Tests Ran' : statusText.join(' '); this.statusBar.color = foreColor; this.statusBar.command = constants.Commands.Tests_View_UI; @@ -71,27 +87,23 @@ export class TestResultDisplay { return tests; } + // tslint:disable-next-line:no-any private updateTestRunWithFailure(reason: any): Promise { this.clearProgressTicker(); this.statusBar.command = constants.Commands.Tests_View_UI; if (reason === CANCELLATION_REASON) { this.statusBar.text = '$(zap) Run Tests'; this.statusBar.tooltip = 'Run Tests'; - } - else { - this.statusBar.text = `$(alert) Tests Failed`; + } else { + this.statusBar.text = '$(alert) Tests Failed'; this.statusBar.tooltip = 'Running Tests Failed'; displayTestErrorMessage('There was an error in running the tests.'); } return Promise.reject(reason); } - private discoverCounter = 0; - private ticker = ['|', '/', '-', '|', '/', '-', '\\']; - private progressTimeout; - private progressPrefix: string; private displayProgress(message: string, tooltip: string, command: string) { - this.progressPrefix = this.statusBar.text = '$(stop) ' + message; + this.progressPrefix = this.statusBar.text = `$(stop) ${message}`; this.statusBar.command = command; this.statusBar.tooltip = tooltip; this.statusBar.show(); @@ -99,7 +111,7 @@ export class TestResultDisplay { this.progressTimeout = setInterval(() => this.updateProgressTicker(), 150); } private updateProgressTicker() { - let text = `${this.progressPrefix} ${this.ticker[this.discoverCounter % 7]}`; + const text = `${this.progressPrefix} ${this.ticker[this.discoverCounter % 7]}`; this.discoverCounter += 1; this.statusBar.text = text; } @@ -111,21 +123,12 @@ export class TestResultDisplay { this.discoverCounter = 0; } - public DisplayDiscoverStatus(tests: Promise) { - this.displayProgress('Discovering Tests', 'Discovering Tests (Click to Stop)', constants.Commands.Tests_Ask_To_Stop_Discovery); - return tests.then(tests => { - this.updateWithDiscoverSuccess(tests); - return tests; - }).catch(reason => { - this.updateWithDiscoverFailure(reason); - return Promise.reject(reason); - }); - } - + // tslint:disable-next-line:no-any private disableTests(): Promise { + // tslint:disable-next-line:no-any const def = createDeferred(); const pythonConfig = vscode.workspace.getConfiguration('python'); - let settingsToDisable = ['unitTest.promptToConfigure', 'unitTest.pyTestEnabled', + const settingsToDisable = ['unitTest.promptToConfigure', 'unitTest.pyTestEnabled', 'unitTest.unittestEnabled', 'unitTest.nosetestsEnabled']; function disableTest() { @@ -154,28 +157,30 @@ export class TestResultDisplay { if (!haveTests) { vscode.window.showInformationMessage('No tests discovered, please check the configuration settings for the tests.', 'Disable Tests').then(item => { if (item === 'Disable Tests') { + // tslint:disable-next-line:no-floating-promises this.disableTests(); } }); } } + // tslint:disable-next-line:no-any private updateWithDiscoverFailure(reason: any) { this.clearProgressTicker(); - this.statusBar.text = `$(zap) Discover Tests`; + this.statusBar.text = '$(zap) Discover Tests'; this.statusBar.tooltip = 'Discover Tests'; this.statusBar.command = constants.Commands.Tests_Discover; this.statusBar.show(); this.statusBar.color = 'yellow'; if (reason !== CANCELLATION_REASON) { - this.statusBar.text = `$(alert) Test discovery failed`; - this.statusBar.tooltip = `Discovering Tests failed (view 'Python Test Log' output panel for details)`; - // TODO: ignore this quitemode, always display the error message (inform the user) + this.statusBar.text = '$(alert) Test discovery failed'; + this.statusBar.tooltip = 'Discovering Tests failed (view \'Python Test Log\' output panel for details)'; + // TODO: ignore this quitemode, always display the error message (inform the user). if (!isNotInstalledError(reason)) { - // TODO: show an option that will invoke a command 'python.test.configureTest' or similar - // This will be hanlded by main.ts that will capture input from user and configure the tests + // TODO: show an option that will invoke a command 'python.test.configureTest' or similar. + // This will be hanlded by main.ts that will capture input from user and configure the tests. vscode.window.showErrorMessage('There was an error in discovering tests, please check the configuration settings for the tests.'); } } } -} \ No newline at end of file +} diff --git a/src/client/unittests/display/picker.ts b/src/client/unittests/display/picker.ts index 3322d3d3c9ca..61c18840ae51 100644 --- a/src/client/unittests/display/picker.ts +++ b/src/client/unittests/display/picker.ts @@ -1,23 +1,22 @@ -import { QuickPickItem, window } from 'vscode'; +import * as path from 'path'; +import { QuickPickItem, Uri, window } from 'vscode'; import * as vscode from 'vscode'; -import { Tests, TestFile, TestFunction, FlattenedTestFunction, TestStatus } from '../common/contracts'; -import { getDiscoveredTests } from '../common/testUtils'; import * as constants from '../../common/constants'; -import * as path from 'path'; +import { FlattenedTestFunction, ITestCollectionStorageService, TestFile, TestFunction, Tests, TestStatus, TestsToRun } from '../common/types'; export class TestDisplay { - constructor() { - } - public displayStopTestUI(message: string) { + constructor(private testCollectionStorage: ITestCollectionStorageService) { } + public displayStopTestUI(workspace: Uri, message: string) { window.showQuickPick([message]).then(item => { if (item === message) { - vscode.commands.executeCommand(constants.Commands.Tests_Stop); + vscode.commands.executeCommand(constants.Commands.Tests_Stop, workspace); } }); } - public displayTestUI(rootDirectory: string) { - const tests = getDiscoveredTests(); - window.showQuickPick(buildItems(rootDirectory, tests), { matchOnDescription: true, matchOnDetail: true }).then(onItemSelected); + public displayTestUI(wkspace: Uri) { + const tests = this.testCollectionStorage.getTests(wkspace); + window.showQuickPick(buildItems(tests), { matchOnDescription: true, matchOnDetail: true }) + .then(item => onItemSelected(wkspace, item, false)); } public selectTestFunction(rootDirectory: string, tests: Tests): Promise { return new Promise((resolve, reject) => { @@ -41,12 +40,13 @@ export class TestDisplay { }, reject); }); } - public displayFunctionTestPickerUI(rootDirectory: string, fileName: string, testFunctions: TestFunction[], debug?: boolean) { - const tests = getDiscoveredTests(); + public displayFunctionTestPickerUI(wkspace: Uri, rootDirectory: string, file: Uri, testFunctions: TestFunction[], debug?: boolean) { + const tests = this.testCollectionStorage.getTests(wkspace); if (!tests) { return; } - const testFile = tests.testFiles.find(file => file.name === fileName || file.fullPath === fileName); + const fileName = file.fsPath; + const testFile = tests.testFiles.find(item => item.name === fileName || item.fullPath === fileName); if (!testFile) { return; } @@ -55,9 +55,9 @@ export class TestDisplay { testFunctions.some(testFunc => testFunc.nameToRun === fn.testFunction.nameToRun); }); - window.showQuickPick(buildItemsForFunctions(rootDirectory, flattenedFunctions), + window.showQuickPick(buildItemsForFunctions(rootDirectory, flattenedFunctions, undefined, undefined, debug), { matchOnDescription: true, matchOnDetail: true }).then(testItem => { - return onItemSelected(testItem, debug); + return onItemSelected(wkspace, testItem, debug); }); } } @@ -72,7 +72,8 @@ enum Type { RunMethod = 6, ViewTestOutput = 7, Null = 8, - SelectAndRunMethod = 9 + SelectAndRunMethod = 9, + DebugMethod = 10 } const statusIconMapping = new Map(); statusIconMapping.set(TestStatus.Pass, constants.Octicons.Test_Pass); @@ -80,14 +81,16 @@ statusIconMapping.set(TestStatus.Fail, constants.Octicons.Test_Fail); statusIconMapping.set(TestStatus.Error, constants.Octicons.Test_Error); statusIconMapping.set(TestStatus.Skipped, constants.Octicons.Test_Skip); -interface TestItem extends QuickPickItem { +type TestItem = QuickPickItem & { type: Type; fn?: FlattenedTestFunction; -} -interface TestFileItem extends QuickPickItem { +}; + +type TestFileItem = QuickPickItem & { type: Type; testFile?: TestFile; -} +}; + function getSummary(tests?: Tests) { if (!tests || !tests.summary) { return ''; @@ -101,20 +104,20 @@ function getSummary(tests?: Tests) { } if (tests.summary.errors > 0) { const plural = tests.summary.errors === 1 ? '' : 's'; - statusText.push(`${constants.Octicons.Test_Error} ${tests.summary.errors} Error` + plural); + statusText.push(`${constants.Octicons.Test_Error} ${tests.summary.errors} Error${plural}`); } if (tests.summary.skipped > 0) { statusText.push(`${constants.Octicons.Test_Skip} ${tests.summary.skipped} Skipped`); } return statusText.join(', ').trim(); } -function buildItems(rootDirectory: string, tests?: Tests): TestItem[] { +function buildItems(tests?: Tests): TestItem[] { const items: TestItem[] = []; items.push({ description: '', label: 'Run All Unit Tests', type: Type.RunAll }); items.push({ description: '', label: 'Discover Unit Tests', type: Type.ReDiscover }); items.push({ description: '', label: 'Run Unit Test Method ...', type: Type.SelectAndRunMethod }); - let summary = getSummary(tests); + const summary = getSummary(tests); items.push({ description: '', label: 'View Unit Test Output', type: Type.ViewTestOutput, detail: summary }); if (tests && tests.summary.failures > 0) { @@ -130,8 +133,8 @@ statusSortPrefix[TestStatus.Fail] = '2'; statusSortPrefix[TestStatus.Skipped] = '3'; statusSortPrefix[TestStatus.Pass] = '4'; -function buildItemsForFunctions(rootDirectory: string, tests: FlattenedTestFunction[], sortBasedOnResults: boolean = false, displayStatusIcons: boolean = false): TestItem[] { - let functionItems: TestItem[] = []; +function buildItemsForFunctions(rootDirectory: string, tests: FlattenedTestFunction[], sortBasedOnResults: boolean = false, displayStatusIcons: boolean = false, debug: boolean = false): TestItem[] { + const functionItems: TestItem[] = []; tests.forEach(fn => { let icon = ''; if (displayStatusIcons && statusIconMapping.has(fn.testFunction.status)) { @@ -142,7 +145,7 @@ function buildItemsForFunctions(rootDirectory: string, tests: FlattenedTestFunct description: '', detail: path.relative(rootDirectory, fn.parentTestFile.fullPath), label: icon + fn.testFunction.name, - type: Type.RunMethod, + type: debug === true ? Type.DebugMethod : Type.RunMethod, fn: fn }); }); @@ -164,15 +167,15 @@ function buildItemsForFunctions(rootDirectory: string, tests: FlattenedTestFunct return functionItems; } function buildItemsForTestFiles(rootDirectory: string, testFiles: TestFile[]): TestFileItem[] { - let fileItems: TestFileItem[] = testFiles.map(testFile => { + const fileItems: TestFileItem[] = testFiles.map(testFile => { return { description: '', detail: path.relative(rootDirectory, testFile.fullPath), type: Type.RunFile, label: path.basename(testFile.fullPath), testFile: testFile - } - }) + }; + }); fileItems.sort((a, b) => { if (a.detail < b.detail) { return -1; @@ -181,15 +184,16 @@ function buildItemsForTestFiles(rootDirectory: string, testFiles: TestFile[]): T return 1; } return 0; - }) + }); return fileItems; } -function onItemSelected(selection: TestItem, debug?: boolean) { +function onItemSelected(wkspace: Uri, selection: TestItem, debug?: boolean) { if (!selection || typeof selection.type !== 'number') { return; } let cmd = ''; - let args = []; + // tslint:disable-next-line:no-any + const args: any[] = [wkspace]; switch (selection.type) { case Type.Null: { return; @@ -216,10 +220,21 @@ function onItemSelected(selection: TestItem, debug?: boolean) { } case Type.RunMethod: { cmd = constants.Commands.Tests_Run; - args.push(selection.fn); + // tslint:disable-next-line:prefer-type-cast + args.push({ testFunction: [selection.fn.testFunction] } as TestsToRun); + break; + } + case Type.DebugMethod: { + cmd = constants.Commands.Tests_Debug; + // tslint:disable-next-line:prefer-type-cast + args.push({ testFunction: [selection.fn.testFunction] } as TestsToRun); + args.push(true); break; } + default: { + return; + } } vscode.commands.executeCommand(cmd, ...args); -} \ No newline at end of file +} diff --git a/src/client/unittests/main.ts b/src/client/unittests/main.ts index 6ab1f22b2e71..adb86808d695 100644 --- a/src/client/unittests/main.ts +++ b/src/client/unittests/main.ts @@ -1,67 +1,79 @@ 'use strict'; +import { Uri, window, workspace } from 'vscode'; import * as vscode from 'vscode'; -import * as nosetests from './nosetest/main'; -import * as pytest from './pytest/main'; -import * as unittest from './unittest/main'; +import { IUnitTestSettings, PythonSettings } from '../common/configSettings'; import * as constants from '../common/constants'; -import { - CANCELLATION_REASON, - FlattenedTestFunction, - TestFile, - TestFunction, - TestStatus, - TestsToRun, -} from './common/contracts'; -import { resolveValueAsTestToRun, getDiscoveredTests } from './common/testUtils'; +import { PythonSymbolProvider } from '../providers/symbolProvider'; +import { activateCodeLenses } from './codeLenses/main'; import { BaseTestManager } from './common/baseTestManager'; -import { PythonSettings } from '../common/configSettings'; +import { CANCELLATION_REASON } from './common/constants'; +import { DebugLauncher } from './common/debugLauncher'; +import { TestCollectionStorageService } from './common/storageService'; +import { TestManagerServiceFactory } from './common/testManagerServiceFactory'; +import { TestResultsService } from './common/testResultsService'; +import { selectTestWorkspace, TestsHelper } from './common/testUtils'; +import { FlattenedTestFunction, ITestCollectionStorageService, IWorkspaceTestManagerService, TestFile, TestFunction, TestStatus, TestsToRun } from './common/types'; +import { WorkspaceTestManagerService } from './common/workspaceTestManagerService'; +import { displayTestFrameworkError } from './configuration'; import { TestResultDisplay } from './display/main'; import { TestDisplay } from './display/picker'; -import { activateCodeLenses } from './codeLenses/main'; -import { displayTestFrameworkError } from './configuration'; -import { PythonSymbolProvider } from '../providers/symbolProvider'; +import * as nosetests from './nosetest/main'; +import * as pytest from './pytest/main'; +import * as unittest from './unittest/main'; -const settings = PythonSettings.getInstance(); -let testManager: BaseTestManager | undefined | null; -let pyTestManager: pytest.TestManager | undefined | null; -let unittestManager: unittest.TestManager | undefined | null; -let nosetestManager: nosetests.TestManager | undefined | null; +let workspaceTestManagerService: IWorkspaceTestManagerService; let testResultDisplay: TestResultDisplay; let testDisplay: TestDisplay; let outChannel: vscode.OutputChannel; -let onDidChange: vscode.EventEmitter = new vscode.EventEmitter(); +const onDidChange: vscode.EventEmitter = new vscode.EventEmitter(); +let testCollectionStorage: ITestCollectionStorageService; export function activate(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, symboldProvider: PythonSymbolProvider) { context.subscriptions.push({ dispose: dispose }); outChannel = outputChannel; - let disposables = registerCommands(); + const disposables = registerCommands(); context.subscriptions.push(...disposables); - if (settings.unitTest.nosetestsEnabled || settings.unitTest.pyTestEnabled || settings.unitTest.unittestEnabled) { - // Ignore the exceptions returned - // This function is invoked via a command which will be invoked else where in the extension - discoverTests(true).catch(() => { - // Ignore the errors - }); - } + testCollectionStorage = new TestCollectionStorageService(); + const testResultsService = new TestResultsService(); + const testsHelper = new TestsHelper(); + const debugLauncher = new DebugLauncher(); + const testManagerServiceFactory = new TestManagerServiceFactory(outChannel, testCollectionStorage, testResultsService, testsHelper, debugLauncher); + workspaceTestManagerService = new WorkspaceTestManagerService(outChannel, testManagerServiceFactory); - settings.addListener('change', onConfigChanged); - context.subscriptions.push(activateCodeLenses(onDidChange, symboldProvider)); + context.subscriptions.push(autoResetTests()); + context.subscriptions.push(activateCodeLenses(onDidChange, symboldProvider, testCollectionStorage)); context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(onDocumentSaved)); -} -function getTestWorkingDirectory() { - return settings.unitTest.cwd && settings.unitTest.cwd.length > 0 ? settings.unitTest.cwd : vscode.workspace.rootPath!; + autoDiscoverTests(); } +async function getTestManager(displayTestNotConfiguredMessage: boolean, resource?: Uri): Promise { + let wkspace: Uri | undefined; + if (resource) { + const wkspaceFolder = workspace.getWorkspaceFolder(resource); + wkspace = wkspaceFolder ? wkspaceFolder.uri : undefined; + } else { + wkspace = await selectTestWorkspace(); + } + if (!wkspace) { + return; + } + const testManager = workspaceTestManagerService.getTestManager(wkspace); + if (testManager) { + return testManager; + } + if (displayTestNotConfiguredMessage) { + await displayTestFrameworkError(wkspace, outChannel); + } +} let timeoutId: number; async function onDocumentSaved(doc: vscode.TextDocument): Promise { - let testManager = getTestRunner(); + const testManager = await getTestManager(false, doc.uri); if (!testManager) { return; } - - let tests = await testManager.discoverTests(false, true); + const tests = await testManager.discoverTests(false, true); if (!tests || !Array.isArray(tests.testFiles) || tests.testFiles.length === 0) { return; } @@ -72,123 +84,158 @@ async function onDocumentSaved(doc: vscode.TextDocument): Promise { if (timeoutId) { clearTimeout(timeoutId); } - timeoutId = setTimeout(() => { discoverTests(true); }, 1000); + timeoutId = setTimeout(() => discoverTests(doc.uri, true), 1000); } function dispose() { - if (pyTestManager) { - pyTestManager.dispose(); - } - if (nosetestManager) { - nosetestManager.dispose(); - } - if (unittestManager) { - unittestManager.dispose(); - } + workspaceTestManagerService.dispose(); + testCollectionStorage.dispose(); } function registerCommands(): vscode.Disposable[] { const disposables = []; - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Discover, () => { - // Ignore the exceptions returned - // This command will be invoked else where in the extension - discoverTests(true).catch(() => { return null; }); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Discover, (resource?: Uri) => { + // Ignore the exceptions returned. + // This command will be invoked else where in the extension. + // tslint:disable-next-line:no-empty + discoverTests(resource, true, true).catch(() => { }); })); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Failed, () => runTestsImpl(true))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run, (testId) => runTestsImpl(testId))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Debug, (testId) => runTestsImpl(testId, true))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Failed, (resource: Uri) => runTestsImpl(resource, undefined, true))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run, (file: Uri, testToRun?: TestsToRun) => runTestsImpl(file, testToRun))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Debug, (file: Uri, testToRun: TestsToRun) => runTestsImpl(file, testToRun, false, true))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_View_UI, () => displayUI())); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI, (file, testFunctions) => displayPickerUI(file, testFunctions))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI, (file: Uri, testFunctions: TestFunction[]) => displayPickerUI(file, testFunctions))); disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI_Debug, (file, testFunctions) => displayPickerUI(file, testFunctions, true))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Stop, () => stopTests())); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Stop, (resource: Uri) => stopTests(resource))); disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_ViewOutput, () => outChannel.show())); disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Ask_To_Stop_Discovery, () => displayStopUI('Stop discovering tests'))); disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Ask_To_Stop_Test, () => displayStopUI('Stop running tests'))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_Method, () => selectAndRunTestMethod())); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Debug_Method, () => selectAndRunTestMethod(true))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_Method, (resource: Uri) => selectAndRunTestMethod(resource))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Debug_Method, (resource: Uri) => selectAndRunTestMethod(resource, true))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_File, () => selectAndRunTestFile())); + // tslint:disable-next-line:no-unnecessary-callback-wrapper disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Current_File, () => runCurrentTestFile())); return disposables; } -function displayUI() { - let testManager = getTestRunner(); +async function displayUI() { + const testManager = await getTestManager(true); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.displayTestUI(getTestWorkingDirectory()); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + testDisplay.displayTestUI(testManager.workspace); } -function displayPickerUI(file: string, testFunctions: TestFunction[], debug?: boolean) { - let testManager = getTestRunner(); +async function displayPickerUI(file: Uri, testFunctions: TestFunction[], debug?: boolean) { + const testManager = await getTestManager(true, file); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.displayFunctionTestPickerUI(getTestWorkingDirectory(), file, testFunctions, debug); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + testDisplay.displayFunctionTestPickerUI(testManager.workspace, testManager.workingDirectory, file, testFunctions, debug); } -function selectAndRunTestMethod(debug?: boolean) { - let testManager = getTestRunner(); +async function selectAndRunTestMethod(resource: Uri, debug?: boolean) { + const testManager = await getTestManager(true, resource); if (!testManager) { - return displayTestFrameworkError(outChannel); - } - testManager.discoverTests(true, true).then(() => { - const tests = getDiscoveredTests(); - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.selectTestFunction(getTestWorkingDirectory(), tests).then(testFn => { - runTestsImpl(testFn, debug); - }).catch(() => { }); - }); + return; + } + try { + await testManager.discoverTests(true, true, true); + } catch (ex) { + return; + } + + const tests = testCollectionStorage.getTests(testManager.workspace); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + const selectedTestFn = await testDisplay.selectTestFunction(testManager.workspace.fsPath, tests); + if (!selectedTestFn) { + return; + } + // tslint:disable-next-line:prefer-type-cast + await runTestsImpl(testManager.workspace, { testFunction: [selectedTestFn.testFunction] } as TestsToRun, debug); } -function selectAndRunTestFile() { - let testManager = getTestRunner(); +async function selectAndRunTestFile() { + const testManager = await getTestManager(true); if (!testManager) { - return displayTestFrameworkError(outChannel); - } - testManager.discoverTests(true, true).then(() => { - const tests = getDiscoveredTests(); - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.selectTestFile(getTestWorkingDirectory(), tests).then(testFile => { - runTestsImpl({ testFile: [testFile] }); - }).catch(() => { }); - }); + return; + } + try { + await testManager.discoverTests(true, true, true); + } catch (ex) { + return; + } + + const tests = testCollectionStorage.getTests(testManager.workspace); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + const selectedFile = await testDisplay.selectTestFile(testManager.workspace.fsPath, tests); + if (!selectedFile) { + return; + } + // tslint:disable-next-line:prefer-type-cast + await runTestsImpl(testManager.workspace, { testFile: [selectedFile] } as TestsToRun); } -function runCurrentTestFile() { - if (!vscode.window.activeTextEditor) { +async function runCurrentTestFile() { + if (!window.activeTextEditor) { return; } - const currentFilePath = vscode.window.activeTextEditor.document.fileName; - let testManager = getTestRunner(); + const testManager = await getTestManager(true, window.activeTextEditor.document.uri); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - testManager.discoverTests(true, true).then(() => { - const tests = getDiscoveredTests(); - const testFiles = tests.testFiles.filter(testFile => { - return testFile.fullPath === currentFilePath; - }); - if (testFiles.length < 1) { - return; - } - runTestsImpl({ testFile: [testFiles[0]] }); + try { + await testManager.discoverTests(true, true, true); + } catch (ex) { + return; + } + const tests = testCollectionStorage.getTests(testManager.workspace); + const testFiles = tests.testFiles.filter(testFile => { + return testFile.fullPath === window.activeTextEditor.document.uri.fsPath; }); + if (testFiles.length < 1) { + return; + } + // tslint:disable-next-line:prefer-type-cast + await runTestsImpl(testManager.workspace, { testFile: [testFiles[0]] } as TestsToRun); } -function displayStopUI(message: string) { - let testManager = getTestRunner(); +async function displayStopUI(message: string) { + const testManager = await getTestManager(true); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.displayStopTestUI(message); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + testDisplay.displayStopTestUI(testManager.workspace, message); } -let uniTestSettingsString = JSON.stringify(settings.unitTest); +let uniTestSettingsString: string; +function autoResetTests() { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length > 1) { + // tslint:disable-next-line:no-empty + return { dispose: () => { } }; + } + + const settings = PythonSettings.getInstance(); + uniTestSettingsString = JSON.stringify(settings.unitTest); + return workspace.onDidChangeConfiguration(() => setTimeout(onConfigChanged, 1000)); +} function onConfigChanged() { - // Possible that a test framework has been enabled or some settings have changed - // Meaning we need to re-load the discovered tests (as something could have changed) + // If there's one workspace, then stop the tests and restart, + // else let the user do this manually. + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length > 1) { + return; + } + const settings = PythonSettings.getInstance(); + + // Possible that a test framework has been enabled or some settings have changed. + // Meaning we need to re-load the discovered tests (as something could have changed). const newSettings = JSON.stringify(settings.unitTest); if (uniTestSettingsString === newSettings) { return; @@ -199,70 +246,47 @@ function onConfigChanged() { if (testResultDisplay) { testResultDisplay.enabled = false; } - - if (testManager) { - testManager.stop(); - testManager = null; - } - if (pyTestManager) { - pyTestManager.dispose(); - pyTestManager = null; - } - if (nosetestManager) { - nosetestManager.dispose(); - nosetestManager = null; - } - if (unittestManager) { - unittestManager.dispose(); - unittestManager = null; - } + workspaceTestManagerService.dispose(); return; } - if (testResultDisplay) { testResultDisplay.enabled = true; } - - // No need to display errors - if (settings.unitTest.nosetestsEnabled || settings.unitTest.pyTestEnabled || settings.unitTest.unittestEnabled) { - discoverTests(true); - } + autoDiscoverTests(); } -function getTestRunner() { - const rootDirectory = getTestWorkingDirectory(); - if (settings.unitTest.nosetestsEnabled) { - return nosetestManager = nosetestManager ? nosetestManager : new nosetests.TestManager(rootDirectory, outChannel); - } - else if (settings.unitTest.pyTestEnabled) { - return pyTestManager = pyTestManager ? pyTestManager : new pytest.TestManager(rootDirectory, outChannel); +function autoDiscoverTests() { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length > 1) { + return; } - else if (settings.unitTest.unittestEnabled) { - return unittestManager = unittestManager ? unittestManager : new unittest.TestManager(rootDirectory, outChannel); + const settings = PythonSettings.getInstance(); + if (!settings.unitTest.nosetestsEnabled && !settings.unitTest.pyTestEnabled && !settings.unitTest.unittestEnabled) { + return; } - return null; -} -function stopTests() { - let testManager = getTestRunner(); + // No need to display errors. + // tslint:disable-next-line:no-empty + discoverTests(workspace.workspaceFolders[0].uri, true).catch(() => { }); +} +async function stopTests(resource: Uri) { + const testManager = await getTestManager(true, resource); if (testManager) { testManager.stop(); } } -function discoverTests(ignoreCache?: boolean) { - let testManager = getTestRunner(); +async function discoverTests(resource?: Uri, ignoreCache?: boolean, userInitiated?: boolean) { + const testManager = await getTestManager(true, resource); if (!testManager) { - displayTestFrameworkError(outChannel); - return Promise.resolve(null); + return; } if (testManager && (testManager.status !== TestStatus.Discovering && testManager.status !== TestStatus.Running)) { testResultDisplay = testResultDisplay ? testResultDisplay : new TestResultDisplay(outChannel, onDidChange); - return testResultDisplay.DisplayDiscoverStatus(testManager.discoverTests(ignoreCache)); - } - else { - return Promise.resolve(null); + const discoveryPromise = testManager.discoverTests(ignoreCache, false, userInitiated); + testResultDisplay.displayDiscoverStatus(discoveryPromise); + await discoveryPromise; } } +// tslint:disable-next-line:no-any function isTestsToRun(arg: any): arg is TestsToRun { if (arg && arg.testFunction && Array.isArray(arg.testFunction)) { return true; @@ -275,46 +299,21 @@ function isTestsToRun(arg: any): arg is TestsToRun { } return false; } -function isUri(arg: any): arg is vscode.Uri { - return arg && arg.fsPath && typeof arg.fsPath === 'string'; -} -function isFlattenedTestFunction(arg: any): arg is FlattenedTestFunction { - return arg && arg.testFunction && typeof arg.xmlClassName === 'string' && - arg.parentTestFile && typeof arg.testFunction.name === 'string'; -} -function identifyTestType(rootDirectory: string, arg?: vscode.Uri | TestsToRun | boolean | FlattenedTestFunction): TestsToRun | boolean | null | undefined { - if (typeof arg === 'boolean') { - return arg === true; - } - if (isTestsToRun(arg)) { - return arg; - } - if (isFlattenedTestFunction(arg)) { - return { testFunction: [arg.testFunction] }; - } - if (isUri(arg)) { - return resolveValueAsTestToRun(arg.fsPath, rootDirectory); - } - return null; -} -function runTestsImpl(arg?: vscode.Uri | TestsToRun | boolean | FlattenedTestFunction, debug: boolean = false) { - let testManager = getTestRunner(); +async function runTestsImpl(resource?: Uri, testsToRun?: TestsToRun, runFailedTests?: boolean, debug: boolean = false) { + const testManager = await getTestManager(true, resource); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - // lastRanTests = testsToRun; - const runInfo = identifyTestType(getTestWorkingDirectory(), arg); - testResultDisplay = testResultDisplay ? testResultDisplay : new TestResultDisplay(outChannel, onDidChange); + const promise = testManager.runTest(testsToRun, runFailedTests, debug) + .catch(reason => { + if (reason !== CANCELLATION_REASON) { + outChannel.appendLine(`Error: ${reason}`); + } + return Promise.reject(reason); + }); - const ret = typeof runInfo === 'boolean' ? testManager.runTest(runInfo, debug) : testManager.runTest(runInfo as TestsToRun, debug); - let runPromise = ret.catch(reason => { - if (reason !== CANCELLATION_REASON) { - outChannel.appendLine('Error: ' + reason); - } - return Promise.reject(reason); - }); - - testResultDisplay.DisplayProgressStatus(runPromise, debug); -} \ No newline at end of file + testResultDisplay.displayProgressStatus(promise, debug); + await promise; +} diff --git a/src/client/unittests/nosetest/collector.ts b/src/client/unittests/nosetest/collector.ts index 7c8888d19459..3a202654e6fd 100644 --- a/src/client/unittests/nosetest/collector.ts +++ b/src/client/unittests/nosetest/collector.ts @@ -1,14 +1,13 @@ 'use strict'; -import * as path from 'path'; -import { execPythonFile } from './../../common/utils'; -import { TestFile, TestSuite, TestFunction, Tests } from '../common/contracts'; import * as os from 'os'; -import { extractBetweenDelimiters, convertFileToPackage, flattenTestFiles } from '../common/testUtils'; +import * as path from 'path'; import { CancellationToken } from 'vscode'; +import { OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; -import { OutputChannel } from 'vscode'; +import { convertFileToPackage, extractBetweenDelimiters } from '../common/testUtils'; +import { ITestsHelper, TestFile, TestFunction, Tests, TestSuite } from '../common/types'; +import { execPythonFile } from './../../common/utils'; -const pythonSettings = PythonSettings.getInstance(); const NOSE_WANT_FILE_PREFIX = 'nose.selector: DEBUG: wantFile '; const NOSE_WANT_FILE_SUFFIX = '.py? True'; const NOSE_WANT_FILE_SUFFIX_WITHOUT_EXT = '? True'; @@ -21,7 +20,7 @@ const argsToExcludeForDiscovery = ['-v', '--verbose', '--failed', '--process-restartworker', '--with-xunit']; const settingsInArgsToExcludeForDiscovery = ['--verbosity']; -export function discoverTests(rootDirectory: string, args: string[], token: CancellationToken, ignoreCache: boolean, outChannel: OutputChannel): Promise { +export function discoverTests(rootDirectory: string, args: string[], token: CancellationToken, ignoreCache: boolean, outChannel: OutputChannel, testsHelper: ITestsHelper): Promise { let logOutputLines: string[] = ['']; let testFiles: TestFile[] = []; @@ -45,8 +44,7 @@ export function discoverTests(rootDirectory: string, args: string[], token: Canc // and starts with nose.selector: DEBUG: want if (logOutputLines[lastLineIndex].endsWith('? True')) { logOutputLines.push(''); - } - else { + } else { // We don't need this line logOutputLines[lastLineIndex] = ''; } @@ -78,14 +76,14 @@ export function discoverTests(rootDirectory: string, args: string[], token: Canc }); } - return execPythonFile(pythonSettings.unitTest.nosetestPath, args.concat(['--collect-only', '-vvv']), rootDirectory, true) + return execPythonFile(rootDirectory, PythonSettings.getInstance(Uri.file(rootDirectory)).unitTest.nosetestPath, args.concat(['--collect-only', '-vvv']), rootDirectory, true) .then(data => { outChannel.appendLine(data); processOutput(data); // Exclude tests that don't have any functions or test suites testFiles = testFiles.filter(testFile => testFile.suites.length > 0 || testFile.functions.length > 0); - return flattenTestFiles(testFiles); + return testsHelper.flattenTestFiles(testFiles); }); } @@ -116,10 +114,10 @@ function parseNoseTestModuleCollectionResult(rootDirectory: string, lines: strin } if (line.startsWith('nose.selector: DEBUG: wantClass ? True'); + const name = extractBetweenDelimiters(line, 'nose.selector: DEBUG: wantClass ? True'); const clsName = path.extname(name).substring(1); const testSuite: TestSuite = { - name: clsName, nameToRun: fileName + `:${clsName}`, + name: clsName, nameToRun: `${fileName}:${clsName}`, functions: [], suites: [], xmlName: name, time: 0, isUnitTest: false, isInstance: false, functionsFailed: 0, functionsPassed: 0 }; @@ -127,7 +125,7 @@ function parseNoseTestModuleCollectionResult(rootDirectory: string, lines: strin return; } if (line.startsWith('nose.selector: DEBUG: wantClass ')) { - let name = extractBetweenDelimiters(line, 'nose.selector: DEBUG: wantClass ', '? True'); + const name = extractBetweenDelimiters(line, 'nose.selector: DEBUG: wantClass ', '? True'); const testSuite: TestSuite = { name: path.extname(name).substring(1), nameToRun: `${fileName}:.${name}`, functions: [], suites: [], xmlName: name, time: 0, isUnitTest: false, @@ -145,6 +143,7 @@ function parseNoseTestModuleCollectionResult(rootDirectory: string, lines: strin time: 0, functionsFailed: 0, functionsPassed: 0 }; + // tslint:disable-next-line:no-non-null-assertion const cls = testFile.suites.find(suite => suite.name === clsName)!; cls.functions.push(fn); return; diff --git a/src/client/unittests/nosetest/main.ts b/src/client/unittests/nosetest/main.ts index 1f3def92cfb8..f670fb2333a3 100644 --- a/src/client/unittests/nosetest/main.ts +++ b/src/client/unittests/nosetest/main.ts @@ -1,31 +1,32 @@ 'use strict'; -import { PythonSettings } from '../../common/configSettings'; import { OutputChannel } from 'vscode'; -import { TestsToRun, Tests } from '../common/contracts'; import * as vscode from 'vscode'; -import { discoverTests } from './collector'; +import { PythonSettings } from '../../common/configSettings'; +import { Product } from '../../common/installer'; import { BaseTestManager } from '../common/baseTestManager'; +import { ITestCollectionStorageService, ITestDebugLauncher, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../common/types'; +import { discoverTests } from './collector'; import { runTest } from './runner'; -import { Product } from '../../common/installer'; - -const settings = PythonSettings.getInstance(); export class TestManager extends BaseTestManager { - constructor(rootDirectory: string, outputChannel: vscode.OutputChannel) { - super('nosetest', Product.nosetest, rootDirectory, outputChannel); + constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, + testCollectionStorage: ITestCollectionStorageService, + testResultsService: ITestResultsService, testsHelper: ITestsHelper, private debugLauncher: ITestDebugLauncher) { + super('nosetest', Product.nosetest, rootDirectory, outputChannel, testCollectionStorage, testResultsService, testsHelper); } - discoverTestsImpl(ignoreCache: boolean): Promise { - let args = settings.unitTest.nosetestArgs.slice(0); - return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel); + public discoverTestsImpl(ignoreCache: boolean): Promise { + const args = this.settings.unitTest.nosetestArgs.slice(0); + return discoverTests(this.rootDirectory, args, this.testDiscoveryCancellationToken, ignoreCache, this.outputChannel, this.testsHelper); } - runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { - let args = settings.unitTest.nosetestArgs.slice(0); + // tslint:disable-next-line:no-any + public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { + const args = this.settings.unitTest.nosetestArgs.slice(0); if (runFailedTests === true && args.indexOf('--failed') === -1) { args.push('--failed'); } if (!runFailedTests && args.indexOf('--with-id') === -1) { args.push('--with-id'); } - return runTest(this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); + return runTest(this.testResultsService, this.debugLauncher, this.rootDirectory, tests, args, testsToRun, this.testRunnerCancellationToken, this.outputChannel, debug); } } diff --git a/src/client/unittests/nosetest/runner.ts b/src/client/unittests/nosetest/runner.ts index 14c0d3251300..ec67b2adc656 100644 --- a/src/client/unittests/nosetest/runner.ts +++ b/src/client/unittests/nosetest/runner.ts @@ -1,22 +1,17 @@ 'use strict'; +import * as path from 'path'; +import { CancellationToken, OutputChannel, Uri } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; import { createTemporaryFile } from '../../common/helpers'; -import { OutputChannel, CancellationToken } from 'vscode'; -import { TestsToRun, Tests } from '../common/contracts'; -import { updateResults } from '../common/testUtils'; -import { updateResultsFromXmlLogFile, PassCalculationFormulae } from '../common/xUnitParser'; import { run } from '../common/runner'; -import { PythonSettings } from '../../common/configSettings'; -import * as vscode from 'vscode'; -import { execPythonFile } from './../../common/utils'; -import { createDeferred } from './../../common/helpers'; -import * as os from 'os'; -import * as path from 'path'; +import { ITestDebugLauncher, ITestResultsService, Tests, TestsToRun } from '../common/types'; +import { PassCalculationFormulae, updateResultsFromXmlLogFile } from '../common/xUnitParser'; -const pythonSettings = PythonSettings.getInstance(); const WITH_XUNIT = '--with-xunit'; const XUNIT_FILE = '--xunit-file'; -export function runTest(rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { +// tslint:disable-next-line:no-any +export function runTest(testResultsService: ITestResultsService, debugLauncher: ITestDebugLauncher, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { let testPaths = []; if (testsToRun && testsToRun.testFolder) { testPaths = testPaths.concat(testsToRun.testFolder.map(f => f.nameToRun)); @@ -32,6 +27,7 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes } let xmlLogFile = ''; + // tslint:disable-next-line:no-empty let xmlLogFileCleanup: Function = () => { }; // Check if '--with-xunit' is in args list @@ -41,7 +37,7 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes } // Check if '--xunit-file' exists, if not generate random xml file - let indexOfXUnitFile = noseTestArgs.findIndex(value => value.indexOf(XUNIT_FILE) === 0); + const indexOfXUnitFile = noseTestArgs.findIndex(value => value.indexOf(XUNIT_FILE) === 0); let promiseToGetXmlLogFile: Promise; if (indexOfXUnitFile === -1) { promiseToGetXmlLogFile = createTemporaryFile('.xml').then(xmlLogResult => { @@ -51,12 +47,10 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes noseTestArgs.push(`${XUNIT_FILE}=${xmlLogFile}`); return xmlLogResult.filePath; }); - } - else { + } else { if (noseTestArgs[indexOfXUnitFile].indexOf('=') === -1) { xmlLogFile = noseTestArgs[indexOfXUnitFile + 1]; - } - else { + } else { xmlLogFile = noseTestArgs[indexOfXUnitFile].substring(noseTestArgs[indexOfXUnitFile].indexOf('=') + 1).trim(); } @@ -64,65 +58,19 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes } return promiseToGetXmlLogFile.then(() => { + const pythonSettings = PythonSettings.getInstance(Uri.file(rootDirectory)); if (debug === true) { - const def = createDeferred(); - const launchDef = createDeferred(); const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py'); - - // start the debug adapter only once we have started the debug process - // pytestlauncherargs const nosetestlauncherargs = [rootDirectory, 'my_secret', pythonSettings.unitTest.debugPort.toString(), 'nose']; - let outputChannelShown = false; - execPythonFile(pythonSettings.pythonPath, [testLauncherFile].concat(nosetestlauncherargs).concat(noseTestArgs.concat(testPaths)), rootDirectory, true, (data: string) => { - if (data.startsWith('READY' + os.EOL)) { - // debug socket server has started - launchDef.resolve(); - data = data.substring(('READY' + os.EOL).length); - } - - if (!outputChannelShown) { - outputChannelShown = true; - outChannel.show(); - } - outChannel.append(data); - }, token).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }).then(() => { - if (!def.rejected && !def.resolved) { - def.resolve(); - } - }).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }); - - launchDef.promise.then(() => { - return vscode.commands.executeCommand('vscode.startDebug', { - "name": "Debug Unit Test", - "type": "python", - "request": "attach", - "localRoot": rootDirectory, - "remoteRoot": rootDirectory, - "port": pythonSettings.unitTest.debugPort, - "secret": "my_secret", - "host": "localhost" - }); - }).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }); - - return def.promise; - } - else { - return run(pythonSettings.unitTest.nosetestPath, noseTestArgs.concat(testPaths), rootDirectory, token, outChannel); + const debuggerArgs = [testLauncherFile].concat(nosetestlauncherargs).concat(noseTestArgs.concat(testPaths)); + // tslint:disable-next-line:prefer-type-cast no-any + return debugLauncher.launchDebugger(rootDirectory, debuggerArgs, token, outChannel) as Promise; + } else { + // tslint:disable-next-line:prefer-type-cast no-any + return run(pythonSettings.unitTest.nosetestPath, noseTestArgs.concat(testPaths), rootDirectory, token, outChannel) as Promise; } }).then(() => { - return updateResultsFromLogFiles(tests, xmlLogFile); + return updateResultsFromLogFiles(tests, xmlLogFile, testResultsService); }).then(result => { xmlLogFileCleanup(); return result; @@ -132,9 +80,10 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes }); } -export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string): Promise { +// tslint:disable-next-line:no-any +export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise { return updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.nosetests).then(() => { - updateResults(tests); + testResultsService.updateResults(tests); return tests; }); } diff --git a/src/client/unittests/nosetest/testConfigurationManager.ts b/src/client/unittests/nosetest/testConfigurationManager.ts index 48826176ab89..7a8b0bedf152 100644 --- a/src/client/unittests/nosetest/testConfigurationManager.ts +++ b/src/client/unittests/nosetest/testConfigurationManager.ts @@ -1,54 +1,44 @@ -import * as vscode from 'vscode'; -import { TestConfigurationManager } from '../common/testConfigurationManager'; import * as fs from 'fs'; import * as path from 'path'; +import * as vscode from 'vscode'; +import { Uri } from 'vscode'; import { Installer, Product } from '../../common/installer'; +import { TestConfigurationManager } from '../common/testConfigurationManager'; +import { ITestConfigSettingsService } from '../common/types'; export class ConfigurationManager extends TestConfigurationManager { - public enable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.nosetestsEnabled', true); - } - public disable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.nosetestsEnabled', false); + constructor(workspace: Uri, outputChannel: vscode.OutputChannel, + installer: Installer, testConfigSettingsService: ITestConfigSettingsService) { + super(workspace, Product.nosetest, outputChannel, installer, testConfigSettingsService); } - - private static configFilesExist(rootDir: string): Promise { + private static async configFilesExist(rootDir: string): Promise { const promises = ['.noserc', 'nose.cfg'].map(cfg => { return new Promise(resolve => { fs.exists(path.join(rootDir, cfg), exists => { resolve(exists ? cfg : ''); }); }); }); - return Promise.all(promises).then(values => { - return values.filter(exists => exists.length > 0); - }); + const values = await Promise.all(promises); + return values.filter(exists => exists.length > 0); } - public configure(rootDir: string): Promise { + // tslint:disable-next-line:no-any + public async configure(wkspace: Uri): Promise { const args: string[] = []; const configFileOptionLabel = 'Use existing config file'; - let installer = new Installer(this.outputChannel); - return ConfigurationManager.configFilesExist(rootDir).then(configFiles => { - if (configFiles.length > 0) { - return Promise.resolve(); - } + const configFiles = await ConfigurationManager.configFilesExist(wkspace.fsPath); + // If a config file exits, there's nothing to be configured. + if (configFiles.length > 0) { + return; + } - return this.getTestDirs(rootDir).then(subDirs => { - return this.selectTestDir(rootDir, subDirs); - }).then(testDir => { - if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { - args.push(testDir); - } - }); - }).then(() => { - return installer.isInstalled(Product.nosetest); - }).then(installed => { - if (!installed) { - return installer.install(Product.nosetest); - } - }).then(() => { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.nosetestArgs', args); - }); + const subDirs = await this.getTestDirs(wkspace.fsPath); + const testDir = await this.selectTestDir(wkspace.fsPath, subDirs); + if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { + args.push(testDir); + } + const installed = await this.installer.isInstalled(Product.nosetest); + if (!installed) { + await this.installer.install(Product.nosetest); + } + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.nosetest, args); } -} \ No newline at end of file +} diff --git a/src/client/unittests/pytest/collector.ts b/src/client/unittests/pytest/collector.ts index af36b2b9fb97..4507f1fdb64c 100644 --- a/src/client/unittests/pytest/collector.ts +++ b/src/client/unittests/pytest/collector.ts @@ -1,14 +1,12 @@ 'use strict'; -import { execPythonFile } from './../../common/utils'; -import { TestFile, TestSuite, TestFunction, Tests } from '../common/contracts'; import * as os from 'os'; -import { extractBetweenDelimiters, flattenTestFiles, convertFileToPackage } from '../common/testUtils'; -import * as vscode from 'vscode'; import * as path from 'path'; -import { PythonSettings } from '../../common/configSettings'; +import * as vscode from 'vscode'; import { OutputChannel } from 'vscode'; - -const pythonSettings = PythonSettings.getInstance(); +import { PythonSettings } from '../../common/configSettings'; +import { convertFileToPackage, extractBetweenDelimiters } from '../common/testUtils'; +import { ITestsHelper, TestFile, TestFunction, Tests, TestSuite } from '../common/types'; +import { execPythonFile } from './../../common/utils'; const argsToExcludeForDiscovery = ['-x', '--exitfirst', '--fixtures-per-test', '--pdb', '--runxfail', @@ -18,10 +16,10 @@ const argsToExcludeForDiscovery = ['-x', '--exitfirst', '--disable-pytest-warnings', '-l', '--showlocals']; const settingsInArgsToExcludeForDiscovery = []; -export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel): Promise { +export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel, testsHelper: ITestsHelper): Promise { let logOutputLines: string[] = ['']; - let testFiles: TestFile[] = []; - let parentNodes: { indent: number, item: TestFile | TestSuite }[] = []; + const testFiles: TestFile[] = []; + const parentNodes: { indent: number, item: TestFile | TestSuite }[] = []; const errorLine = /==*( *)ERRORS( *)=*/; const errorFileLine = /__*( *)ERROR collecting (.*)/; const lastLineWithErrors = /==*.*/; @@ -85,14 +83,14 @@ export function discoverTests(rootDirectory: string, args: string[], token: vsco }); } - return execPythonFile(pythonSettings.unitTest.pyTestPath, args.concat(['--collect-only']), rootDirectory, false, null, token) + return execPythonFile(rootDirectory, PythonSettings.getInstance(vscode.Uri.file(rootDirectory)).unitTest.pyTestPath, args.concat(['--collect-only']), rootDirectory, false, null, token) .then(data => { outChannel.appendLine(data); processOutput(data); if (token && token.isCancellationRequested) { return Promise.reject('cancelled'); } - return flattenTestFiles(testFiles); + return testsHelper.flattenTestFiles(testFiles); }); } @@ -106,7 +104,7 @@ function parsePyTestModuleCollectionError(rootDirectory: string, lines: string[] return; } - let errorFileLine = lines[0]; + const errorFileLine = lines[0]; let fileName = errorFileLine.substring(errorFileLine.indexOf('ERROR collecting') + 'ERROR collecting'.length).trim(); fileName = fileName.substr(0, fileName.lastIndexOf(' ')); @@ -146,22 +144,23 @@ function parsePyTestModuleCollectionResult(rootDirectory: string, lines: string[ if (trimmedLine.startsWith(' 0) { - let parentNode = parentNodes[parentNodes.length - 1]; + const parentNode = parentNodes[parentNodes.length - 1]; if (parentNode.indent < indentOfCurrentItem) { return parentNode; } diff --git a/src/client/unittests/pytest/main.ts b/src/client/unittests/pytest/main.ts index 8be3477c1b81..ee53c93de207 100644 --- a/src/client/unittests/pytest/main.ts +++ b/src/client/unittests/pytest/main.ts @@ -1,26 +1,26 @@ 'use strict'; -import { PythonSettings } from '../../common/configSettings'; -import { TestsToRun, Tests } from '../common/contracts'; -import { runTest } from './runner'; import * as vscode from 'vscode'; -import { discoverTests } from './collector'; -import { BaseTestManager } from '../common/baseTestManager'; import { Product } from '../../common/installer'; +import { BaseTestManager } from '../common/baseTestManager'; +import { ITestCollectionStorageService, ITestDebugLauncher, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../common/types'; +import { discoverTests } from './collector'; +import { runTest } from './runner'; -const settings = PythonSettings.getInstance(); export class TestManager extends BaseTestManager { - constructor(rootDirectory: string, outputChannel: vscode.OutputChannel) { - super('pytest', Product.pytest, rootDirectory, outputChannel); + constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, + testCollectionStorage: ITestCollectionStorageService, + testResultsService: ITestResultsService, testsHelper: ITestsHelper, private debugLauncher: ITestDebugLauncher) { + super('pytest', Product.pytest, rootDirectory, outputChannel, testCollectionStorage, testResultsService, testsHelper); } - discoverTestsImpl(ignoreCache: boolean): Promise { - let args = settings.unitTest.pyTestArgs.slice(0); - return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel); + public discoverTestsImpl(ignoreCache: boolean): Promise { + const args = this.settings.unitTest.pyTestArgs.slice(0); + return discoverTests(this.rootDirectory, args, this.testDiscoveryCancellationToken, ignoreCache, this.outputChannel, this.testsHelper); } - runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { - let args = settings.unitTest.pyTestArgs.slice(0); + public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<{}> { + const args = this.settings.unitTest.pyTestArgs.slice(0); if (runFailedTests === true && args.indexOf('--lf') === -1 && args.indexOf('--last-failed') === -1) { args.push('--last-failed'); } - return runTest(this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); + return runTest(this.testResultsService, this.debugLauncher, this.rootDirectory, tests, args, testsToRun, this.testRunnerCancellationToken, this.outputChannel, debug); } } diff --git a/src/client/unittests/pytest/runner.ts b/src/client/unittests/pytest/runner.ts index 40c5137653d0..3e314b305cab 100644 --- a/src/client/unittests/pytest/runner.ts +++ b/src/client/unittests/pytest/runner.ts @@ -1,22 +1,13 @@ -/// - 'use strict'; +import * as path from 'path'; +import { CancellationToken, OutputChannel, Uri } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; import { createTemporaryFile } from '../../common/helpers'; -import { TestsToRun, Tests } from '../common/contracts'; -import { updateResults } from '../common/testUtils'; -import { CancellationToken, OutputChannel } from 'vscode'; -import { updateResultsFromXmlLogFile, PassCalculationFormulae } from '../common/xUnitParser'; import { run } from '../common/runner'; -import { PythonSettings } from '../../common/configSettings'; -import * as vscode from 'vscode'; -import { execPythonFile } from './../../common/utils'; -import { createDeferred } from './../../common/helpers'; -import * as os from 'os'; -import * as path from 'path'; - -const pythonSettings = PythonSettings.getInstance(); +import { ITestDebugLauncher, ITestResultsService, Tests, TestsToRun } from '../common/types'; +import { PassCalculationFormulae, updateResultsFromXmlLogFile } from '../common/xUnitParser'; -export function runTest(rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { +export function runTest(testResultsService: ITestResultsService, debugLauncher: ITestDebugLauncher, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { let testPaths = []; if (testsToRun && testsToRun.testFolder) { testPaths = testPaths.concat(testsToRun.testFolder.map(f => f.nameToRun)); @@ -42,65 +33,19 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes args = args.filter(arg => arg.trim().startsWith('-')); } const testArgs = testPaths.concat(args, [`--junitxml=${xmlLogFile}`]); + const pythonSettings = PythonSettings.getInstance(Uri.file(rootDirectory)); if (debug) { - const def = createDeferred(); - const launchDef = createDeferred(); const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py'); - - // start the debug adapter only once we have started the debug process - // pytestlauncherargs const pytestlauncherargs = [rootDirectory, 'my_secret', pythonSettings.unitTest.debugPort.toString(), 'pytest']; - let outputChannelShown = false; - execPythonFile(pythonSettings.pythonPath, [testLauncherFile].concat(pytestlauncherargs).concat(testArgs), rootDirectory, true, (data: string) => { - if (data.startsWith('READY' + os.EOL)) { - // debug socket server has started - launchDef.resolve(); - data = data.substring(('READY' + os.EOL).length); - } - - if (!outputChannelShown) { - outputChannelShown = true; - outChannel.show(); - } - outChannel.append(data); - }, token).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }).then(() => { - if (!def.rejected && !def.resolved) { - def.resolve(); - } - }).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }); - - launchDef.promise.then(() => { - return vscode.commands.executeCommand('vscode.startDebug', { - "name": "Debug Unit Test", - "type": "python", - "request": "attach", - "localRoot": rootDirectory, - "remoteRoot": rootDirectory, - "port": pythonSettings.unitTest.debugPort, - "secret": "my_secret", - "host": "localhost" - }); - }).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }); - - return def.promise; - } - else { - return run(pythonSettings.unitTest.pyTestPath, testArgs, rootDirectory, token, outChannel); + const debuggerArgs = [testLauncherFile].concat(pytestlauncherargs).concat(testArgs); + // tslint:disable-next-line:prefer-type-cast no-any + return debugLauncher.launchDebugger(rootDirectory, debuggerArgs, token, outChannel) as Promise; + } else { + // tslint:disable-next-line:prefer-type-cast no-any + return run(pythonSettings.unitTest.pyTestPath, testArgs, rootDirectory, token, outChannel) as Promise; } }).then(() => { - return updateResultsFromLogFiles(tests, xmlLogFile); + return updateResultsFromLogFiles(tests, xmlLogFile, testResultsService); }).then(result => { xmlLogFileCleanup(); return result; @@ -110,9 +55,9 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes }); } -export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string): Promise { +export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise { return updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.pytest).then(() => { - updateResults(tests); + testResultsService.updateResults(tests); return tests; }); -} \ No newline at end of file +} diff --git a/src/client/unittests/pytest/testConfigurationManager.ts b/src/client/unittests/pytest/testConfigurationManager.ts index 09ae098ede9a..8017835d7b95 100644 --- a/src/client/unittests/pytest/testConfigurationManager.ts +++ b/src/client/unittests/pytest/testConfigurationManager.ts @@ -1,61 +1,51 @@ -import * as vscode from 'vscode'; -import { TestConfigurationManager } from '../common/testConfigurationManager'; import * as fs from 'fs'; import * as path from 'path'; +import * as vscode from 'vscode'; +import { Uri } from 'vscode'; import { Installer, Product } from '../../common/installer'; +import { TestConfigurationManager } from '../common/testConfigurationManager'; +import { ITestConfigSettingsService } from '../common/types'; export class ConfigurationManager extends TestConfigurationManager { - public enable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.pyTestEnabled', true); + constructor(workspace: Uri, outputChannel: vscode.OutputChannel, + installer: Installer, testConfigSettingsService: ITestConfigSettingsService) { + super(workspace, Product.pytest, outputChannel, installer, testConfigSettingsService); } - public disable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.pyTestEnabled', false); - } - - private static configFilesExist(rootDir: string): Promise { + private static async configFilesExist(rootDir: string): Promise { const promises = ['pytest.ini', 'tox.ini', 'setup.cfg'].map(cfg => { return new Promise(resolve => { fs.exists(path.join(rootDir, cfg), exists => { resolve(exists ? cfg : ''); }); }); }); - return Promise.all(promises).then(values => { - return values.filter(exists => exists.length > 0); - }); + const values = await Promise.all(promises); + return values.filter(exists => exists.length > 0); } - public configure(rootDir: string): Promise { + // tslint:disable-next-line:no-any + public async configure(wkspace: Uri) { const args = []; const configFileOptionLabel = 'Use existing config file'; const options: vscode.QuickPickItem[] = []; - let installer = new Installer(this.outputChannel); - return ConfigurationManager.configFilesExist(rootDir).then(configFiles => { - if (configFiles.length > 0 && configFiles.length !== 1 && configFiles[0] !== 'setup.cfg') { - return Promise.resolve(); - } + const configFiles = await ConfigurationManager.configFilesExist(wkspace.fsPath); + // If a config file exits, there's nothing to be configured. + if (configFiles.length > 0 && configFiles.length !== 1 && configFiles[0] !== 'setup.cfg') { + return; + } - if (configFiles.length === 1 && configFiles[0] === 'setup.cfg') { - options.push({ - label: configFileOptionLabel, - description: 'setup.cfg' - }); - } - return this.getTestDirs(rootDir).then(subDirs => { - return this.selectTestDir(rootDir, subDirs, options); - }).then(testDir => { - if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { - args.push(testDir); - } + if (configFiles.length === 1 && configFiles[0] === 'setup.cfg') { + options.push({ + label: configFileOptionLabel, + description: 'setup.cfg' }); - }).then(() => { - return installer.isInstalled(Product.pytest); - }).then(installed => { - if (!installed) { - return installer.install(Product.pytest); - } - }).then(() => { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.pyTestArgs', args); - }); + } + const subDirs = await this.getTestDirs(wkspace.fsPath); + const testDir = await this.selectTestDir(wkspace.fsPath, subDirs, options); + if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { + args.push(testDir); + } + const installed = await this.installer.isInstalled(Product.pytest); + if (!installed) { + await this.installer.install(Product.pytest); + } + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.pytest, args); } -} \ No newline at end of file +} diff --git a/src/client/unittests/unittest/collector.ts b/src/client/unittests/unittest/collector.ts index 91affb058814..50c6edc19dec 100644 --- a/src/client/unittests/unittest/collector.ts +++ b/src/client/unittests/unittest/collector.ts @@ -1,15 +1,12 @@ 'use strict'; -import { execPythonFile } from './../../common/utils'; -import { TestFile, Tests, TestStatus } from '../common/contracts'; -import { flattenTestFiles } from '../common/testUtils'; -import * as vscode from 'vscode'; import * as path from 'path'; -import { PythonSettings } from '../../common/configSettings'; +import * as vscode from 'vscode'; import { OutputChannel } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; +import { ITestsHelper, TestFile, TestFunction, Tests, TestStatus, TestSuite } from '../common/types'; +import { execPythonFile } from './../../common/utils'; -const pythonSettings = PythonSettings.getInstance(); - -export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel): Promise { +export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel, testsHelper: ITestsHelper): Promise { let startDirectory = '.'; let pattern = 'test*.py'; const indexOfStartDir = args.findIndex(arg => arg.indexOf('-s') === 0); @@ -18,8 +15,7 @@ export function discoverTests(rootDirectory: string, args: string[], token: vsco if (startDir.trim() === '-s' && args.length >= indexOfStartDir) { // Assume the next items is the directory startDirectory = args[indexOfStartDir + 1]; - } - else { + } else { startDirectory = startDir.substring(2).trim(); if (startDirectory.startsWith('=') || startDirectory.startsWith(' ')) { startDirectory = startDirectory.substring(1); @@ -32,8 +28,7 @@ export function discoverTests(rootDirectory: string, args: string[], token: vsco if (patternValue.trim() === '-p' && args.length >= indexOfPattern) { // Assume the next items is the directory pattern = args[indexOfPattern + 1]; - } - else { + } else { pattern = patternValue.substring(2).trim(); if (pattern.startsWith('=')) { pattern = pattern.substring(1); @@ -53,7 +48,7 @@ for suite in suites._tests: pass`; let startedCollecting = false; - let testItems: string[] = []; + const testItems: string[] = []; function processOutput(output: string) { output.split(/\r?\n/g).forEach((line, index, lines) => { if (token && token.isCancellationRequested) { @@ -73,7 +68,7 @@ for suite in suites._tests: }); } args = []; - return execPythonFile(pythonSettings.pythonPath, args.concat(['-c', pythonScript]), rootDirectory, true, null, token) + return execPythonFile(rootDirectory, PythonSettings.getInstance(vscode.Uri.file(rootDirectory)).pythonPath, args.concat(['-c', pythonScript]), rootDirectory, true, null, token) .then(data => { outChannel.appendLine(data); processOutput(data); @@ -85,17 +80,17 @@ for suite in suites._tests: if (startDirectory.length > 1) { testsDirectory = path.isAbsolute(startDirectory) ? startDirectory : path.resolve(rootDirectory, startDirectory); } - return parseTestIds(testsDirectory, testItems); + return parseTestIds(testsDirectory, testItems, testsHelper); }); } -function parseTestIds(rootDirectory: string, testIds: string[]): Tests { +function parseTestIds(rootDirectory: string, testIds: string[], testsHelper: ITestsHelper): Tests { const testFiles: TestFile[] = []; testIds.forEach(testId => { addTestId(rootDirectory, testId, testFiles); }); - return flattenTestFiles(testFiles); + return testsHelper.flattenTestFiles(testFiles); } function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) { @@ -106,7 +101,7 @@ function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) } const paths = testIdParts.slice(0, testIdParts.length - 2); - const filePath = path.join(rootDirectory, ...paths) + '.py'; + const filePath = `${path.join(rootDirectory, ...paths)}.py`; const functionName = testIdParts.pop(); const className = testIdParts.pop(); @@ -116,8 +111,10 @@ function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) testFile = { name: path.basename(filePath), fullPath: filePath, - functions: [], - suites: [], + // tslint:disable-next-line:prefer-type-cast + functions: [] as TestFunction[], + // tslint:disable-next-line:prefer-type-cast + suites: [] as TestSuite[], nameToRun: `${className}.${functionName}`, xmlName: '', status: TestStatus.Idle, @@ -132,8 +129,10 @@ function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) if (!testSuite) { testSuite = { name: className, - functions: [], - suites: [], + // tslint:disable-next-line:prefer-type-cast + functions: [] as TestFunction[], + // tslint:disable-next-line:prefer-type-cast + suites: [] as TestSuite[], isUnitTest: true, isInstance: false, nameToRun: classNameToRun, @@ -144,7 +143,7 @@ function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) testFile.suites.push(testSuite); } - const testFunction = { + const testFunction: TestFunction = { name: functionName, nameToRun: testId, status: TestStatus.Idle, @@ -152,4 +151,4 @@ function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) }; testSuite.functions.push(testFunction); -} \ No newline at end of file +} diff --git a/src/client/unittests/unittest/main.ts b/src/client/unittests/unittest/main.ts index 51cee1b32ac1..162ec396b6ef 100644 --- a/src/client/unittests/unittest/main.ts +++ b/src/client/unittests/unittest/main.ts @@ -1,31 +1,32 @@ 'use strict'; -import { PythonSettings } from '../../common/configSettings'; -import { TestsToRun, Tests, TestStatus } from '../common/contracts'; -import { runTest } from './runner'; import * as vscode from 'vscode'; -import { discoverTests } from './collector'; -import { BaseTestManager } from '../common/baseTestManager'; +import { PythonSettings } from '../../common/configSettings'; import { Product } from '../../common/installer'; - -const settings = PythonSettings.getInstance(); +import { BaseTestManager } from '../common/baseTestManager'; +import { ITestCollectionStorageService, ITestDebugLauncher, ITestResultsService, ITestsHelper, Tests, TestStatus, TestsToRun } from '../common/types'; +import { discoverTests } from './collector'; +import { runTest } from './runner'; export class TestManager extends BaseTestManager { - constructor(rootDirectory: string, outputChannel: vscode.OutputChannel) { - super('unitest', Product.unittest, rootDirectory, outputChannel); + constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, + testCollectionStorage: ITestCollectionStorageService, + testResultsService: ITestResultsService, testsHelper: ITestsHelper, private debugLauncher: ITestDebugLauncher) { + super('unitest', Product.unittest, rootDirectory, outputChannel, testCollectionStorage, testResultsService, testsHelper); } - configure() { + // tslint:disable-next-line:no-empty + public configure() { } - discoverTestsImpl(ignoreCache: boolean): Promise { - let args = settings.unitTest.unittestArgs.slice(0); - return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel); + public discoverTestsImpl(ignoreCache: boolean): Promise { + const args = this.settings.unitTest.unittestArgs.slice(0); + return discoverTests(this.rootDirectory, args, this.testDiscoveryCancellationToken, ignoreCache, this.outputChannel, this.testsHelper); } - runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { - let args = settings.unitTest.unittestArgs.slice(0); + public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<{}> { + const args = this.settings.unitTest.unittestArgs.slice(0); if (runFailedTests === true) { testsToRun = { testFile: [], testFolder: [], testSuite: [], testFunction: [] }; testsToRun.testFunction = tests.testFunctions.filter(fn => { return fn.testFunction.status === TestStatus.Error || fn.testFunction.status === TestStatus.Fail; }).map(fn => fn.testFunction); } - return runTest(this, this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); + return runTest(this, this.testResultsService, this.debugLauncher, this.rootDirectory, tests, args, testsToRun, this.testRunnerCancellationToken, this.outputChannel, debug); } } diff --git a/src/client/unittests/unittest/runner.ts b/src/client/unittests/unittest/runner.ts index b451af48db23..db72c77b14ef 100644 --- a/src/client/unittests/unittest/runner.ts +++ b/src/client/unittests/unittest/runner.ts @@ -1,29 +1,24 @@ -/// - 'use strict'; import * as path from 'path'; -import { TestsToRun, Tests, TestStatus } from '../common/contracts'; -import { updateResults } from '../common/testUtils'; +import { CancellationToken, OutputChannel, Uri } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; import { BaseTestManager } from '../common/baseTestManager'; -import { CancellationToken, OutputChannel } from 'vscode'; import { run } from '../common/runner'; +import { ITestDebugLauncher, ITestResultsService, Tests, TestStatus, TestsToRun } from '../common/types'; import { Server } from './socketServer'; -import { PythonSettings } from '../../common/configSettings'; -import * as vscode from 'vscode'; -import { execPythonFile } from './../../common/utils'; -import { createDeferred } from './../../common/helpers'; -import * as os from 'os'; - -const settings = PythonSettings.getInstance(); -interface TestStatusMap { +type TestStatusMap = { status: TestStatus; summaryProperty: string; -} +}; const outcomeMapping = new Map(); +// tslint:disable-next-line:no-backbone-get-set-outside-model outcomeMapping.set('passed', { status: TestStatus.Pass, summaryProperty: 'passed' }); +// tslint:disable-next-line:no-backbone-get-set-outside-model outcomeMapping.set('failed', { status: TestStatus.Fail, summaryProperty: 'failures' }); +// tslint:disable-next-line:no-backbone-get-set-outside-model outcomeMapping.set('error', { status: TestStatus.Error, summaryProperty: 'errors' }); +// tslint:disable-next-line:no-backbone-get-set-outside-model outcomeMapping.set('skipped', { status: TestStatus.Skipped, summaryProperty: 'skipped' }); interface ITestData { @@ -33,7 +28,8 @@ interface ITestData { traceback: string; } -export function runTest(testManager: BaseTestManager, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { +// tslint:disable-next-line:max-func-body-length +export function runTest(testManager: BaseTestManager, testResultsService: ITestResultsService, debugLauncher: ITestDebugLauncher, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { tests.summary.errors = 0; tests.summary.failures = 0; tests.summary.passed = 0; @@ -42,12 +38,16 @@ export function runTest(testManager: BaseTestManager, rootDirectory: string, tes const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'visualstudio_py_testlauncher.py'); const server = new Server(); server.on('error', (message: string, ...data: string[]) => { + // tslint:disable-next-line:no-console console.log(`${message} ${data.join(' ')}`); }); + // tslint:disable-next-line:no-empty server.on('log', (message: string, ...data: string[]) => { }); + // tslint:disable-next-line:no-empty server.on('connect', (data) => { }); + // tslint:disable-next-line:no-empty server.on('start', (data: { test: string }) => { }); server.on('result', (data: ITestData) => { @@ -62,31 +62,32 @@ export function runTest(testManager: BaseTestManager, rootDirectory: string, tes if (failFast && (statusDetails.summaryProperty === 'failures' || statusDetails.summaryProperty === 'errors')) { testManager.stop(); } - } - else { + } else { if (statusDetails) { tests.summary[statusDetails.summaryProperty] += 1; } } }); + // tslint:disable-next-line:no-empty server.on('socket.disconnected', (data) => { }); return server.start().then(port => { - let testPaths: string[] = getIdsOfTestsToRun(tests, testsToRun); - for (let counter = 0; counter < testPaths.length; counter++) { - testPaths[counter] = '-t' + testPaths[counter].trim(); + const testPaths: string[] = getIdsOfTestsToRun(tests, testsToRun); + for (let counter = 0; counter < testPaths.length; counter += 1) { + testPaths[counter] = `-t${testPaths[counter].trim()}`; } const startTestDiscoveryDirectory = getStartDirectory(args); - function runTest(testFile: string = '', testId: string = '') { + function runTestInternal(testFile: string = '', testId: string = '') { let testArgs = buildTestArgs(args); failFast = testArgs.indexOf('--uf') >= 0; testArgs = testArgs.filter(arg => arg !== '--uf'); testArgs.push(`--result-port=${port}`); if (debug === true) { - testArgs.push(...[`--secret=my_secret`, `--port=3000`]); + const debugPort = PythonSettings.getInstance(Uri.file(rootDirectory)).unitTest.debugPort; + testArgs.push(...['--secret=my_secret', `--port=${debugPort}`]); } testArgs.push(`--us=${startTestDiscoveryDirectory}`); if (testId.length > 0) { @@ -96,90 +97,45 @@ export function runTest(testManager: BaseTestManager, rootDirectory: string, tes testArgs.push(`--testFile=${testFile}`); } if (debug === true) { - const def = createDeferred(); - const launchDef = createDeferred(); - let outputChannelShown = false; - - // start the debug adapter only once we have started the debug process - execPythonFile(settings.pythonPath, [testLauncherFile].concat(testArgs), rootDirectory, true, (data: string) => { - if (data.startsWith('READY' + os.EOL)) { - // debug socket server has started - launchDef.resolve(); - data = data.substring(('READY' + os.EOL).length); - } - - if (!outputChannelShown) { - outputChannelShown = true; - outChannel.show(); - } - outChannel.append(data); - }, token).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }).then(() => { - if (!def.rejected && !def.resolved) { - def.resolve(); - } - }).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }); - - launchDef.promise - .then(() => { - return vscode.commands.executeCommand('vscode.startDebug', { - "name": "Debug Unit Test", - "type": "python", - "request": "attach", - "localRoot": rootDirectory, - "remoteRoot": rootDirectory, - "port": settings.unitTest.debugPort, - "secret": "my_secret", - "host": "localhost" - }); - }) - .catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }); - - return def.promise; - } - else { - return run(settings.pythonPath, [testLauncherFile].concat(testArgs), rootDirectory, token, outChannel); + // tslint:disable-next-line:prefer-type-cast no-any + return debugLauncher.launchDebugger(rootDirectory, [testLauncherFile].concat(testArgs), token, outChannel); + } else { + // tslint:disable-next-line:prefer-type-cast no-any + return run(PythonSettings.getInstance(Uri.file(rootDirectory)).pythonPath, [testLauncherFile].concat(testArgs), rootDirectory, token, outChannel); } } // Test everything if (testPaths.length === 0) { - return runTest(); + return runTestInternal(); } // Ok, the ptvs test runner can only work with one test at a time let promise = Promise.resolve(''); if (Array.isArray(testsToRun.testFile)) { testsToRun.testFile.forEach(testFile => { - promise = promise.then(() => runTest(testFile.fullPath, testFile.nameToRun)); + // tslint:disable-next-line:prefer-type-cast no-any + promise = promise.then(() => runTestInternal(testFile.fullPath, testFile.nameToRun) as Promise); }); } if (Array.isArray(testsToRun.testSuite)) { testsToRun.testSuite.forEach(testSuite => { - const testFileName = tests.testSuits.find(t => t.testSuite === testSuite).parentTestFile.fullPath; - promise = promise.then(() => runTest(testFileName, testSuite.nameToRun)); + const testFileName = tests.testSuites.find(t => t.testSuite === testSuite).parentTestFile.fullPath; + // tslint:disable-next-line:prefer-type-cast no-any + promise = promise.then(() => runTestInternal(testFileName, testSuite.nameToRun) as Promise); }); } if (Array.isArray(testsToRun.testFunction)) { testsToRun.testFunction.forEach(testFn => { const testFileName = tests.testFunctions.find(t => t.testFunction === testFn).parentTestFile.fullPath; - promise = promise.then(() => runTest(testFileName, testFn.nameToRun)); + // tslint:disable-next-line:prefer-type-cast no-any + promise = promise.then(() => runTestInternal(testFileName, testFn.nameToRun) as Promise); }); } - return promise; + // tslint:disable-next-line:prefer-type-cast no-any + return promise as Promise; }).then(() => { - updateResults(tests); + testResultsService.updateResults(tests); return tests; }).catch(reason => { return Promise.reject(reason); @@ -194,8 +150,7 @@ function getStartDirectory(args: string[]): string { if ((startDir.trim() === '-s' || startDir.trim() === '--start-directory') && args.length >= indexOfStartDir) { // Assume the next items is the directory startDirectory = args[indexOfStartDir + 1]; - } - else { + } else { const lenToStartFrom = startDir.startsWith('-s') ? '-s'.length : '--start-directory'.length; startDirectory = startDir.substring(lenToStartFrom).trim(); if (startDirectory.startsWith('=')) { @@ -214,8 +169,7 @@ function buildTestArgs(args: string[]): string[] { if ((patternValue.trim() === '-p' || patternValue.trim() === '--pattern') && args.length >= indexOfPattern) { // Assume the next items is the directory pattern = args[indexOfPattern + 1]; - } - else { + } else { const lenToStartFrom = patternValue.startsWith('-p') ? '-p'.length : '--pattern'.length; pattern = patternValue.substring(lenToStartFrom).trim(); if (pattern.startsWith('=')) { @@ -253,4 +207,4 @@ function getIdsOfTestsToRun(tests: Tests, testsToRun: TestsToRun): string[] { testIds.push(...testsToRun.testFunction.map(f => f.nameToRun)); } return testIds; -} \ No newline at end of file +} diff --git a/src/client/unittests/unittest/socketServer.ts b/src/client/unittests/unittest/socketServer.ts index 6c4d3bc6166b..334ae6b85c4e 100644 --- a/src/client/unittests/unittest/socketServer.ts +++ b/src/client/unittests/unittest/socketServer.ts @@ -1,16 +1,19 @@ 'use strict'; -import * as net from 'net'; +import { EventEmitter } from 'events'; import * as fs from 'fs'; +import * as net from 'net'; import * as os from 'os'; -import { Disposable } from 'vscode' +import { Disposable } from 'vscode'; import { createDeferred, Deferred } from '../../common/helpers'; -import { EventEmitter } from 'events'; +// tslint:disable-next-line:variable-name const MaxConnections = 100; function getIPType() { const networkInterfaces = os.networkInterfaces(); + // tslint:disable-next-line:variable-name let IPType = ''; + // tslint:disable-next-line:prefer-type-cast no-any if (networkInterfaces && Array.isArray(networkInterfaces) && (networkInterfaces as any).length > 0) { // getting the family of first network interface available IPType = networkInterfaces[Object.keys(networkInterfaces)[0]][0].family; @@ -41,7 +44,7 @@ export class Server extends EventEmitter implements Disposable { } } public start(): Promise { - this.startedDef = createDeferred() + this.startedDef = createDeferred(); fs.unlink(this.path, () => { this.server = net.createServer(this.connectionListener.bind(this)); this.server.maxConnections = MaxConnections; @@ -72,16 +75,17 @@ export class Server extends EventEmitter implements Disposable { this.emit('error', err); }); socket.on('data', (data) => { - let sock = socket; + const sock = socket; // Assume we have just one client socket connection let dataStr = this.ipcBuffer += data; + // tslint:disable-next-line:no-constant-condition while (true) { const startIndex = dataStr.indexOf('{'); if (startIndex === -1) { return; } - const lengthOfMessage = parseInt(dataStr.slice(dataStr.indexOf(':') + 1, dataStr.indexOf('{')).trim()); + const lengthOfMessage = parseInt(dataStr.slice(dataStr.indexOf(':') + 1, dataStr.indexOf('{')).trim(), 10); if (dataStr.length < startIndex + lengthOfMessage) { return; } @@ -97,13 +101,16 @@ export class Server extends EventEmitter implements Disposable { this.emit('log', message, ...data); } private onCloseSocket() { - for (let i = 0, count = this.sockets.length; i < count; i++) { - let socket = this.sockets[i]; + // tslint:disable-next-line:one-variable-per-declaration + for (let i = 0, count = this.sockets.length; i < count; i += 1) { + const socket = this.sockets[i]; let destroyedSocketId = false; if (socket && socket.readable) { continue; } + // tslint:disable-next-line:no-any prefer-type-cast if ((socket as any).id) { + // tslint:disable-next-line:no-any prefer-type-cast destroyedSocketId = (socket as any).id; } this.log('socket disconnected', destroyedSocketId.toString()); @@ -115,4 +122,4 @@ export class Server extends EventEmitter implements Disposable { return; } } -} \ No newline at end of file +} diff --git a/src/client/unittests/unittest/testConfigurationManager.ts b/src/client/unittests/unittest/testConfigurationManager.ts index 85041676e06b..87334cd72746 100644 --- a/src/client/unittests/unittest/testConfigurationManager.ts +++ b/src/client/unittests/unittest/testConfigurationManager.ts @@ -1,42 +1,33 @@ -import * as vscode from 'vscode'; import * as path from 'path'; +import { OutputChannel, Uri } from 'vscode'; +import { Installer, Product } from '../../common/installer'; import { TestConfigurationManager } from '../common/testConfigurationManager'; +import { ITestConfigSettingsService } from '../common/types'; export class ConfigurationManager extends TestConfigurationManager { - public enable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.unittestEnabled', true); + constructor(workspace: Uri, outputChannel: OutputChannel, + installer: Installer, testConfigSettingsService: ITestConfigSettingsService) { + super(workspace, Product.unittest, outputChannel, installer, testConfigSettingsService); } - public disable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.unittestEnabled', false); - } - - public configure(rootDir: string): Promise { + // tslint:disable-next-line:no-any + public async configure(wkspace: Uri) { const args = ['-v']; - return this.getTestDirs(rootDir).then(subDirs => { - return this.selectTestDir(rootDir, subDirs); - }).then(testDir => { - args.push('-s'); - if (typeof testDir === 'string' && testDir !== '.') { - args.push(`.${path.sep}${testDir}`); - } - else { - args.push('.'); - } + const subDirs = await this.getTestDirs(wkspace.fsPath); + const testDir = await this.selectTestDir(wkspace.fsPath, subDirs); + args.push('-s'); + if (typeof testDir === 'string' && testDir !== '.') { + args.push(`./${testDir}`); + } else { + args.push('.'); + } - return this.selectTestFilePattern(); - }).then(testfilePattern => { - args.push('-p'); - if (typeof testfilePattern === 'string') { - args.push(testfilePattern); - } - else { - args.push('test*.py'); - } - }).then(() => { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.unittestArgs', args); - }); + const testfilePattern = await this.selectTestFilePattern(); + args.push('-p'); + if (typeof testfilePattern === 'string') { + args.push(testfilePattern); + } else { + args.push('test*.py'); + } + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.unittest, args); } -} \ No newline at end of file +} diff --git a/src/client/workspaceSymbols/generator.ts b/src/client/workspaceSymbols/generator.ts index 6f75a67cfed5..dcb1b5ae3d35 100644 --- a/src/client/workspaceSymbols/generator.ts +++ b/src/client/workspaceSymbols/generator.ts @@ -2,17 +2,22 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; import * as child_process from 'child_process'; -import { PythonSettings } from '../common/configSettings'; - -const pythonSettings = PythonSettings.getInstance(); +import { IPythonSettings, PythonSettings } from '../common/configSettings'; export class Generator implements vscode.Disposable { private optionsFile: string; private disposables: vscode.Disposable[]; - - constructor(private output: vscode.OutputChannel) { + private pythonSettings: IPythonSettings; + public get tagFilePath(): string { + return this.pythonSettings.workspaceSymbols.tagFilePath; + } + public get enabled(): boolean { + return this.pythonSettings.workspaceSymbols.enabled; + } + constructor(public readonly workspaceFolder: vscode.Uri, private output: vscode.OutputChannel) { this.disposables = []; this.optionsFile = path.join(__dirname, '..', '..', '..', 'resources', 'ctagOptions'); + this.pythonSettings = PythonSettings.getInstance(workspaceFolder); } dispose() { @@ -21,20 +26,25 @@ export class Generator implements vscode.Disposable { private buildCmdArgs(): string[] { const optionsFile = this.optionsFile.indexOf(' ') > 0 ? `"${this.optionsFile}"` : this.optionsFile; - const exclusions = pythonSettings.workspaceSymbols.exclusionPatterns; + const exclusions = this.pythonSettings.workspaceSymbols.exclusionPatterns; const excludes = exclusions.length === 0 ? [] : exclusions.map(pattern => `--exclude=${pattern}`); return [`--options=${optionsFile}`, '--languages=Python'].concat(excludes); } - generateWorkspaceTags(): Promise { - const tagFile = path.normalize(pythonSettings.workspaceSymbols.tagFilePath); - return this.generateTags(tagFile, { directory: vscode.workspace.rootPath }); + async generateWorkspaceTags(): Promise { + if (!this.pythonSettings.workspaceSymbols.enabled) { + return; + } + return await this.generateTags({ directory: this.workspaceFolder.fsPath }); } - private generateTags(outputFile: string, source: { directory?: string, file?: string }): Promise { - const cmd = pythonSettings.workspaceSymbols.ctagsPath; + private generateTags(source: { directory?: string, file?: string }): Promise { + const tagFile = path.normalize(this.pythonSettings.workspaceSymbols.tagFilePath); + const cmd = this.pythonSettings.workspaceSymbols.ctagsPath; const args = this.buildCmdArgs(); + + let outputFile = tagFile; if (source.file && source.file.length > 0) { source.directory = path.dirname(source.file); } @@ -43,14 +53,14 @@ export class Generator implements vscode.Disposable { outputFile = path.basename(outputFile); } const outputDir = path.dirname(outputFile); - if (!fs.existsSync(outputDir)){ + if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir); } outputFile = outputFile.indexOf(' ') > 0 ? `"${outputFile}"` : outputFile; args.push(`-o ${outputFile}`, '.'); this.output.appendLine('-'.repeat(10) + 'Generating Tags' + '-'.repeat(10)); this.output.appendLine(`${cmd} ${args.join(' ')}`); - const promise = new Promise((resolve, reject) => { + const promise = new Promise((resolve, reject) => { let options: child_process.SpawnOptions = { cwd: source.directory }; @@ -75,7 +85,7 @@ export class Generator implements vscode.Disposable { reject(errorMsg); } else { - resolve(outputFile); + resolve(); } }); }); diff --git a/src/client/workspaceSymbols/main.ts b/src/client/workspaceSymbols/main.ts index 835c7cec545c..1be92e608df4 100644 --- a/src/client/workspaceSymbols/main.ts +++ b/src/client/workspaceSymbols/main.ts @@ -1,30 +1,40 @@ import * as vscode from 'vscode'; import { Generator } from './generator'; -import { Product, Installer } from '../common/installer'; -import { PythonSettings } from '../common/configSettings'; +import { Installer, InstallerResponse, Product } from '../common/installer'; import { fsExistsAsync } from '../common/utils'; import { isNotInstalledError } from '../common/helpers'; import { PythonLanguage, Commands } from '../common/constants'; import { WorkspaceSymbolProvider } from './provider'; +import { workspace } from 'vscode'; -const pythonSettings = PythonSettings.getInstance(); +const MAX_NUMBER_OF_ATTEMPTS_TO_INSTALL_AND_BUILD = 2; export class WorkspaceSymbols implements vscode.Disposable { private disposables: vscode.Disposable[]; - private generator: Generator; + private generators: Generator[] = []; private installer: Installer; constructor(private outputChannel: vscode.OutputChannel) { this.disposables = []; this.disposables.push(this.outputChannel); - this.generator = new Generator(this.outputChannel); - this.disposables.push(this.generator); this.installer = new Installer(); this.disposables.push(this.installer); this.registerCommands(); - - // The extension has just loaded, so lets rebuild the tags - vscode.languages.registerWorkspaceSymbolProvider(new WorkspaceSymbolProvider(this.generator, this.outputChannel)); + this.initializeGenerators(); + vscode.languages.registerWorkspaceSymbolProvider(new WorkspaceSymbolProvider(this.generators, this.outputChannel)); this.buildWorkspaceSymbols(true); + this.disposables.push(vscode.workspace.onDidChangeWorkspaceFolders(() => this.initializeGenerators())); + } + private initializeGenerators() { + while (this.generators.length > 0) { + const generator = this.generators.shift(); + generator.dispose(); + } + + if (Array.isArray(vscode.workspace.workspaceFolders)) { + vscode.workspace.workspaceFolders.forEach(wkSpc => { + this.generators.push(new Generator(wkSpc.uri, this.outputChannel)); + }); + } } registerCommands() { this.disposables.push(vscode.commands.registerCommand(Commands.Build_Workspace_Symbols, (rebuild: boolean = true, token?: vscode.CancellationToken) => { @@ -52,53 +62,53 @@ export class WorkspaceSymbols implements vscode.Disposable { dispose() { this.disposables.forEach(d => d.dispose()); } - disableDocumentLanguageProvider(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('python.workspaceSymbols.enabled', false); - - } - buildWorkspaceSymbols(rebuild: boolean = true, token?: vscode.CancellationToken): Promise { - if (!pythonSettings.workspaceSymbols.enabled || (token && token.isCancellationRequested)) { + async buildWorkspaceSymbols(rebuild: boolean = true, token?: vscode.CancellationToken): Promise { + if (token && token.isCancellationRequested) { return Promise.resolve([]); } - if (!vscode.workspace || typeof vscode.workspace.rootPath !== 'string' || vscode.workspace.rootPath.length === 0) { + if (this.generators.length === 0) { return Promise.resolve([]); } - return fsExistsAsync(pythonSettings.workspaceSymbols.tagFilePath).then(exits => { - let promise = Promise.resolve(); + let promptPromise: Promise; + let promptResponse: InstallerResponse; + return this.generators.map(async generator => { + if (!generator.enabled) { + return; + } + const exists = await fsExistsAsync(generator.tagFilePath); // if file doesn't exist, then run the ctag generator // Or check if required to rebuild - if (rebuild || !exits) { - promise = this.generator.generateWorkspaceTags(); + if (!rebuild && exists) { + return; } - - return promise.catch(reason => { - if (!isNotInstalledError(reason)) { - this.outputChannel.show(); - return Promise.reject(reason); + for (let counter = 0; counter < MAX_NUMBER_OF_ATTEMPTS_TO_INSTALL_AND_BUILD; counter++) { + try { + await generator.generateWorkspaceTags(); + return; + } + catch (error) { + if (!isNotInstalledError(error)) { + this.outputChannel.show(); + return; + } } if (!token || token.isCancellationRequested) { return; } - return new Promise((resolve, reject) => { - vscode.window.showErrorMessage('CTags needs to be installed to get support for Python workspace symbols', - 'Install', `Don't ask again`).then(item => { - switch (item) { - case 'Install': { - this.installer.install(Product.ctags).then(() => { - return this.buildWorkspaceSymbols(rebuild, token); - }).catch(reason => reject(reason)); - break; - } - case `Don't ask again`: { - this.disableDocumentLanguageProvider().then(() => resolve(), reason => reject(reason)); - break; - } - } - }); - }); - }); + // Display prompt once for all workspaces + if (promptPromise) { + promptResponse = await promptPromise; + continue; + } + else { + promptPromise = this.installer.promptToInstall(Product.ctags, workspace.workspaceFolders[0].uri); + promptResponse = await promptPromise; + } + if (promptResponse !== InstallerResponse.Installed || (!token || token.isCancellationRequested)) { + return; + } + } }); } } diff --git a/src/client/workspaceSymbols/parser.ts b/src/client/workspaceSymbols/parser.ts index e13d340434ef..7be6e51183d4 100644 --- a/src/client/workspaceSymbols/parser.ts +++ b/src/client/workspaceSymbols/parser.ts @@ -1,14 +1,12 @@ import * as vscode from 'vscode'; import { Tag } from './contracts'; import * as path from 'path'; -import { PythonSettings } from '../common/configSettings'; import { fsExistsAsync } from '../common/utils'; const LineByLineReader = require("line-by-line"); const NamedRegexp = require('named-js-regexp'); const fuzzy = require('fuzzy'); -const pythonSettings = PythonSettings.getInstance(); const IsFileRegEx = /\tkind:file\tline:\d+$/g; const LINE_REGEX = '(?\\w+)\\t(?.*)\\t\\/\\^(?.*)\\$\\/;"\\tkind:(?\\w+)\\tline:(?\\d+)$'; @@ -102,15 +100,14 @@ Object.keys(newValuesAndKeys).forEach(key => { CTagKinMapping.set(key, newValuesAndKeys[key]); }); -export function parseTags(query: string, token: vscode.CancellationToken, maxItems: number = 200): Promise { - const file = pythonSettings.workspaceSymbols.tagFilePath; - return fsExistsAsync(file).then(exists => { +export function parseTags(workspaceFolder: string, tagFile: string, query: string, token: vscode.CancellationToken, maxItems: number = 200): Promise { + return fsExistsAsync(tagFile).then(exists => { if (!exists) { return null; } return new Promise((resolve, reject) => { - let lr = new LineByLineReader(file); + let lr = new LineByLineReader(tagFile); let lineNumber = 0; let tags: Tag[] = []; @@ -124,7 +121,7 @@ export function parseTags(query: string, token: vscode.CancellationToken, maxIte lr.close(); return; } - const tag = parseTagsLine(line, query); + const tag = parseTagsLine(workspaceFolder, line, query); if (tag) { tags.push(tag); } @@ -139,7 +136,7 @@ export function parseTags(query: string, token: vscode.CancellationToken, maxIte }); }); } -function parseTagsLine(line: string, searchPattern: string): Tag { +function parseTagsLine(workspaceFolder: string, line: string, searchPattern: string): Tag { if (IsFileRegEx.test(line)) { return; } @@ -152,7 +149,7 @@ function parseTagsLine(line: string, searchPattern: string): Tag { } let file = match.file; if (!path.isAbsolute(file)) { - file = path.resolve(vscode.workspace.rootPath, '.vscode', file); + file = path.resolve(workspaceFolder, '.vscode', file); } const symbolKind = CTagKinMapping.has(match.type) ? CTagKinMapping.get(match.type) : vscode.SymbolKind.Null; diff --git a/src/client/workspaceSymbols/provider.ts b/src/client/workspaceSymbols/provider.ts index af02e06136c5..e9124212e9a8 100644 --- a/src/client/workspaceSymbols/provider.ts +++ b/src/client/workspaceSymbols/provider.ts @@ -1,36 +1,43 @@ import * as vscode from 'vscode'; +import * as _ from 'lodash'; import { Generator } from './generator'; -import { PythonSettings } from '../common/configSettings'; import { parseTags } from './parser'; import { fsExistsAsync } from '../common/utils'; -import { createDeferred } from '../common/helpers'; import { Commands } from '../common/constants'; -const pythonSettings = PythonSettings.getInstance(); export class WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider { - public constructor(private tagGenerator: Generator, private outputChannel: vscode.OutputChannel) { + public constructor(private tagGenerators: Generator[], private outputChannel: vscode.OutputChannel) { } async provideWorkspaceSymbols(query: string, token: vscode.CancellationToken): Promise { - if (!pythonSettings.workspaceSymbols.enabled) { + if (this.tagGenerators.length === 0) { return []; } - if (!vscode.workspace || typeof vscode.workspace.rootPath !== 'string' || vscode.workspace.rootPath.length === 0) { - return Promise.resolve([]); - } - // check whether tag file needs to be built - const tagFileExists = await fsExistsAsync(pythonSettings.workspaceSymbols.tagFilePath); - if (!tagFileExists) { + const generatorsWithTagFiles = await Promise.all(this.tagGenerators.map(generator => fsExistsAsync(generator.tagFilePath))); + if (generatorsWithTagFiles.filter(exists => exists).length !== this.tagGenerators.length) { await vscode.commands.executeCommand(Commands.Build_Workspace_Symbols, true, token); } - // load tags - const items = await parseTags(query, token); - if (!Array.isArray(items)) { - return []; - } - return items.map(item => new vscode.SymbolInformation( - item.symbolName, item.symbolKind, '', - new vscode.Location(vscode.Uri.file(item.fileName), item.position) - )); + + const generators = await Promise.all(this.tagGenerators.map(async generator => { + const tagFileExists = await fsExistsAsync(generator.tagFilePath); + return tagFileExists ? generator : undefined; + })); + + const promises = generators + .filter(generator => generator !== undefined && generator.enabled) + .map(async generator => { + // load tags + const items = await parseTags(generator.workspaceFolder.fsPath, generator.tagFilePath, query, token); + if (!Array.isArray(items)) { + return []; + } + return items.map(item => new vscode.SymbolInformation( + item.symbolName, item.symbolKind, '', + new vscode.Location(vscode.Uri.file(item.fileName), item.position) + )); + }); + + const symbols = await Promise.all(promises); + return _.flatten(symbols); } } diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json new file mode 100644 index 000000000000..2218e2cecd87 --- /dev/null +++ b/src/test/.vscode/settings.json @@ -0,0 +1,25 @@ +{ + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": false, + "python.workspaceSymbols.enabled": false, + "python.unitTest.nosetestArgs": [], + "python.unitTest.pyTestArgs": [], + "python.unitTest.unittestArgs": [ + "-v", + "-s", + ".", + "-p", + "*test*.py" + ], + "python.formatting.formatOnSave": false, + "python.sortImports.args": [], + "python.linting.lintOnSave": false, + "python.linting.lintOnTextChange": false, + "python.linting.enabled": true, + "python.linting.pep8Enabled": false, + "python.linting.prospectorEnabled": false, + "python.linting.pydocstyleEnabled": false, + "python.linting.pylamaEnabled": false, + "python.linting.mypyEnabled": false, + "python.formatting.provider": "yapf" +} \ No newline at end of file diff --git a/src/test/.vscode/tags b/src/test/.vscode/tags new file mode 100644 index 000000000000..c4371e74af04 --- /dev/null +++ b/src/test/.vscode/tags @@ -0,0 +1,721 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +A ..\\pythonFiles\\autocomp\\pep526.py /^class A:$/;" kind:class line:13 +A ..\\pythonFiles\\definition\\await.test.py /^class A:$/;" kind:class line:3 +B ..\\pythonFiles\\autocomp\\pep526.py /^class B:$/;" kind:class line:17 +B ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^class B(Exception):$/;" kind:class line:19 +B ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^class B(Exception):$/;" kind:class line:19 +B ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^class B(Exception):$/;" kind:class line:19 +BaseRefactoring ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class BaseRefactoring(object):$/;" kind:class line:54 +BoundedQueue ..\\pythonFiles\\autocomp\\misc.py /^ class BoundedQueue(_Verbose):$/;" kind:class line:1250 +BoundedSemaphore ..\\pythonFiles\\autocomp\\misc.py /^def BoundedSemaphore(*args, **kwargs):$/;" kind:function line:497 +C ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^class C(B):$/;" kind:class line:22 +C ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^class C(B):$/;" kind:class line:22 +C ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^class C(B):$/;" kind:class line:22 +Change ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class Change():$/;" kind:class line:41 +ChangeType ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class ChangeType():$/;" kind:class line:32 +Child2Class ..\\pythonFiles\\symbolFiles\\childFile.py /^class Child2Class(object):$/;" kind:class line:5 +Class1 ..\\pythonFiles\\autocomp\\one.py /^class Class1(object):$/;" kind:class line:6 +Class1 ..\\pythonFiles\\definition\\one.py /^class Class1(object):$/;" kind:class line:6 +Condition ..\\pythonFiles\\autocomp\\misc.py /^def Condition(*args, **kwargs):$/;" kind:function line:242 +ConsumerThread ..\\pythonFiles\\autocomp\\misc.py /^ class ConsumerThread(Thread):$/;" kind:class line:1298 +D ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^class D(C):$/;" kind:class line:25 +D ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^class D(C):$/;" kind:class line:25 +D ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^class D(C):$/;" kind:class line:25 +DELETE ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ DELETE = 2$/;" kind:variable line:38 +DELETE ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ DELETE = 2$/;" kind:variable line:46 +Decorator ..\\pythonFiles\\autocomp\\deco.py /^class Decorator(metaclass=abc.ABCMeta):$/;" kind:class line:3 +DoSomething ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^class DoSomething():$/;" kind:class line:200 +DoSomething ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^class DoSomething():$/;" kind:class line:200 +DoSomething ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^class DoSomething():$/;" kind:class line:200 +EDIT ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ EDIT = 0$/;" kind:variable line:36 +EDIT ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ EDIT = 0$/;" kind:variable line:44 +Event ..\\pythonFiles\\autocomp\\misc.py /^def Event(*args, **kwargs):$/;" kind:function line:542 +Example3 ..\\pythonFiles\\formatting\\fileToFormat.py /^class Example3( object ):$/;" kind:class line:12 +ExtractMethodRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class ExtractMethodRefactor(ExtractVariableRefactor):$/;" kind:class line:144 +ExtractVariableRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class ExtractVariableRefactor(BaseRefactoring):$/;" kind:class line:120 +Foo ..\\multiRootWkspc\\disableLinters\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\multiRootWkspc\\parent\\child\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\multiRootWkspc\\workspace1\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\multiRootWkspc\\workspace2\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\multiRootWkspc\\workspace3\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\autocomp\\four.py /^class Foo(object):$/;" kind:class line:7 +Foo ..\\pythonFiles\\definition\\four.py /^class Foo(object):$/;" kind:class line:7 +Foo ..\\pythonFiles\\linting\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\linting\\flake8config\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\linting\\pep8config\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\linting\\pylintconfig\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\symbolFiles\\file.py /^class Foo(object):$/;" kind:class line:5 +Gaussian ..\\pythonFiles\\jupyter\\cells.py /^class Gaussian(object):$/;" kind:class line:100 +Lock ..\\pythonFiles\\autocomp\\misc.py /^Lock = _allocate_lock$/;" kind:variable line:112 +N ..\\pythonFiles\\jupyter\\cells.py /^N = 50$/;" kind:variable line:42 +NEW ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ NEW = 1$/;" kind:variable line:37 +NEW ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ NEW = 1$/;" kind:variable line:45 +PEP_484_style ..\\pythonFiles\\autocomp\\pep526.py /^PEP_484_style = SOMETHING # type: str$/;" kind:variable line:5 +PEP_526_style ..\\pythonFiles\\autocomp\\pep526.py /^PEP_526_style: str = "hello world"$/;" kind:variable line:3 +ProducerThread ..\\pythonFiles\\autocomp\\misc.py /^ class ProducerThread(Thread):$/;" kind:class line:1282 +RLock ..\\pythonFiles\\autocomp\\misc.py /^def RLock(*args, **kwargs):$/;" kind:function line:114 +ROPE_PROJECT_FOLDER ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ROPE_PROJECT_FOLDER = sys.argv[2]$/;" kind:variable line:18 +ROPE_PROJECT_FOLDER ..\\pythonFiles\\sorting\\noconfig\\after.py /^ROPE_PROJECT_FOLDER = sys.argv[2]$/;" kind:variable line:12 +ROPE_PROJECT_FOLDER ..\\pythonFiles\\sorting\\noconfig\\before.py /^ROPE_PROJECT_FOLDER = sys.argv[2]$/;" kind:variable line:9 +ROPE_PROJECT_FOLDER ..\\pythonFiles\\sorting\\noconfig\\original.py /^ROPE_PROJECT_FOLDER = sys.argv[2]$/;" kind:variable line:9 +Random ..\\pythonFiles\\autocomp\\misc.py /^class Random(_random.Random):$/;" kind:class line:1331 +RefactorProgress ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class RefactorProgress():$/;" kind:class line:21 +RenameRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class RenameRefactor(BaseRefactoring):$/;" kind:class line:101 +RopeRefactoring ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class RopeRefactoring(object):$/;" kind:class line:162 +Semaphore ..\\pythonFiles\\autocomp\\misc.py /^def Semaphore(*args, **kwargs):$/;" kind:function line:412 +TOOLS ..\\pythonFiles\\jupyter\\cells.py /^TOOLS = "pan,wheel_zoom,box_zoom,reset,save,box_select"$/;" kind:variable line:68 +Test_CheckMyApp ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^class Test_CheckMyApp:$/;" kind:class line:6 +Test_CheckMyApp ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^class Test_CheckMyApp:$/;" kind:class line:6 +Test_CheckMyApp ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^class Test_CheckMyApp:$/;" kind:class line:6 +Test_Current_Working_Directory ..\\pythonFiles\\testFiles\\cwd\\src\\tests\\test_cwd.py /^class Test_Current_Working_Directory(unittest.TestCase):$/;" kind:class line:6 +Test_NestedClassA ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ class Test_NestedClassA:$/;" kind:class line:13 +Test_NestedClassA ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ class Test_NestedClassA:$/;" kind:class line:13 +Test_NestedClassA ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ class Test_NestedClassA:$/;" kind:class line:13 +Test_Root_test1 ..\\pythonFiles\\testFiles\\single\\test_root.py /^class Test_Root_test1(unittest.TestCase):$/;" kind:class line:6 +Test_Root_test1 ..\\pythonFiles\\testFiles\\standard\\test_root.py /^class Test_Root_test1(unittest.TestCase):$/;" kind:class line:6 +Test_Root_test1 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\test_root.py /^class Test_Root_test1(unittest.TestCase):$/;" kind:class line:6 +Test_nested_classB_Of_A ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ class Test_nested_classB_Of_A:$/;" kind:class line:16 +Test_nested_classB_Of_A ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ class Test_nested_classB_Of_A:$/;" kind:class line:16 +Test_nested_classB_Of_A ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ class Test_nested_classB_Of_A:$/;" kind:class line:16 +Test_test1 ..\\pythonFiles\\testFiles\\single\\tests\\test_one.py /^class Test_test1(unittest.TestCase):$/;" kind:class line:6 +Test_test1 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_one.py /^class Test_test1(unittest.TestCase):$/;" kind:class line:6 +Test_test1 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_unittest_one.py /^class Test_test1(unittest.TestCase):$/;" kind:class line:6 +Test_test1 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_one.py /^class Test_test1(unittest.TestCase):$/;" kind:class line:6 +Test_test2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^class Test_test2(unittest.TestCase):$/;" kind:class line:3 +Test_test2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^class Test_test2(unittest.TestCase):$/;" kind:class line:3 +Test_test2a ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^class Test_test2a(unittest.TestCase):$/;" kind:class line:17 +Test_test2a ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^class Test_test2a(unittest.TestCase):$/;" kind:class line:17 +Test_test2a1 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ class Test_test2a1(unittest.TestCase):$/;" kind:class line:24 +Test_test2a1 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ class Test_test2a1(unittest.TestCase):$/;" kind:class line:24 +Test_test3 ..\\pythonFiles\\testFiles\\standard\\tests\\unittest_three_test.py /^class Test_test3(unittest.TestCase):$/;" kind:class line:4 +Test_test3 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\unittest_three_test.py /^class Test_test3(unittest.TestCase):$/;" kind:class line:4 +Test_test_one_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^class Test_test_one_1(unittest.TestCase):$/;" kind:class line:3 +Test_test_one_2 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^class Test_test_one_2(unittest.TestCase):$/;" kind:class line:14 +Test_test_two_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^class Test_test_two_1(unittest.TestCase):$/;" kind:class line:3 +Test_test_two_2 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^class Test_test_two_2(unittest.TestCase):$/;" kind:class line:14 +Thread ..\\pythonFiles\\autocomp\\misc.py /^class Thread(_Verbose):$/;" kind:class line:640 +ThreadError ..\\pythonFiles\\autocomp\\misc.py /^ThreadError = thread.error$/;" kind:variable line:38 +Timer ..\\pythonFiles\\autocomp\\misc.py /^def Timer(*args, **kwargs):$/;" kind:function line:1046 +VERSION ..\\pythonFiles\\autocomp\\misc.py /^ VERSION = 3 # used by getstate\/setstate$/;" kind:variable line:1345 +WORKSPACE_ROOT ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^WORKSPACE_ROOT = sys.argv[1]$/;" kind:variable line:17 +WORKSPACE_ROOT ..\\pythonFiles\\sorting\\noconfig\\after.py /^WORKSPACE_ROOT = sys.argv[1]$/;" kind:variable line:11 +WORKSPACE_ROOT ..\\pythonFiles\\sorting\\noconfig\\before.py /^WORKSPACE_ROOT = sys.argv[1]$/;" kind:variable line:8 +WORKSPACE_ROOT ..\\pythonFiles\\sorting\\noconfig\\original.py /^WORKSPACE_ROOT = sys.argv[1]$/;" kind:variable line:8 +Workspace2Class ..\\pythonFiles\\symbolFiles\\workspace2File.py /^class Workspace2Class(object):$/;" kind:class line:5 +_BoundedSemaphore ..\\pythonFiles\\autocomp\\misc.py /^class _BoundedSemaphore(_Semaphore):$/;" kind:class line:515 +_Condition ..\\pythonFiles\\autocomp\\misc.py /^class _Condition(_Verbose):$/;" kind:class line:255 +_DummyThread ..\\pythonFiles\\autocomp\\misc.py /^class _DummyThread(Thread):$/;" kind:class line:1128 +_Event ..\\pythonFiles\\autocomp\\misc.py /^class _Event(_Verbose):$/;" kind:class line:552 +_MainThread ..\\pythonFiles\\autocomp\\misc.py /^class _MainThread(Thread):$/;" kind:class line:1088 +_RLock ..\\pythonFiles\\autocomp\\misc.py /^class _RLock(_Verbose):$/;" kind:class line:125 +_Semaphore ..\\pythonFiles\\autocomp\\misc.py /^class _Semaphore(_Verbose):$/;" kind:class line:423 +_Timer ..\\pythonFiles\\autocomp\\misc.py /^class _Timer(Thread):$/;" kind:class line:1058 +_VERBOSE ..\\pythonFiles\\autocomp\\misc.py /^_VERBOSE = False$/;" kind:variable line:53 +_Verbose ..\\pythonFiles\\autocomp\\misc.py /^ class _Verbose(object):$/;" kind:class line:57 +_Verbose ..\\pythonFiles\\autocomp\\misc.py /^ class _Verbose(object):$/;" kind:class line:79 +__all__ ..\\pythonFiles\\autocomp\\misc.py /^__all__ = ['activeCount', 'active_count', 'Condition', 'currentThread',$/;" kind:variable line:30 +__bootstrap ..\\pythonFiles\\autocomp\\misc.py /^ def __bootstrap(self):$/;" kind:member line:769 +__bootstrap_inner ..\\pythonFiles\\autocomp\\misc.py /^ def __bootstrap_inner(self):$/;" kind:member line:792 +__delete ..\\pythonFiles\\autocomp\\misc.py /^ def __delete(self):$/;" kind:member line:876 +__enter__ ..\\pythonFiles\\autocomp\\misc.py /^ __enter__ = acquire$/;" kind:variable line:185 +__enter__ ..\\pythonFiles\\autocomp\\misc.py /^ __enter__ = acquire$/;" kind:variable line:477 +__enter__ ..\\pythonFiles\\autocomp\\misc.py /^ def __enter__(self):$/;" kind:member line:285 +__exc_clear ..\\pythonFiles\\autocomp\\misc.py /^ __exc_clear = _sys.exc_clear$/;" kind:variable line:654 +__exc_info ..\\pythonFiles\\autocomp\\misc.py /^ __exc_info = _sys.exc_info$/;" kind:variable line:651 +__exit__ ..\\pythonFiles\\autocomp\\misc.py /^ def __exit__(self, *args):$/;" kind:member line:288 +__exit__ ..\\pythonFiles\\autocomp\\misc.py /^ def __exit__(self, t, v, tb):$/;" kind:member line:215 +__exit__ ..\\pythonFiles\\autocomp\\misc.py /^ def __exit__(self, t, v, tb):$/;" kind:member line:493 +__getstate__ ..\\pythonFiles\\autocomp\\misc.py /^ def __getstate__(self): # for pickle$/;" kind:member line:1422 +__init__ ..\\multiRootWkspc\\disableLinters\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\multiRootWkspc\\parent\\child\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\multiRootWkspc\\workspace1\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\multiRootWkspc\\workspace2\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\multiRootWkspc\\workspace3\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, limit):$/;" kind:member line:1252 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, queue, count):$/;" kind:member line:1300 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, queue, quota):$/;" kind:member line:1284 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, verbose=None):$/;" kind:member line:59 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, verbose=None):$/;" kind:member line:80 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self):$/;" kind:member line:1090 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self):$/;" kind:member line:1130 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, group=None, target=None, name=None,$/;" kind:member line:656 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, interval, function, args=[], kwargs={}):$/;" kind:member line:1067 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, lock=None, verbose=None):$/;" kind:member line:260 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, value=1, verbose=None):$/;" kind:member line:433 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, value=1, verbose=None):$/;" kind:member line:521 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, verbose=None):$/;" kind:member line:132 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, verbose=None):$/;" kind:member line:561 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, x=None):$/;" kind:member line:1347 +__init__ ..\\pythonFiles\\autocomp\\one.py /^ def __init__(self, file_path=None, file_contents=None):$/;" kind:member line:14 +__init__ ..\\pythonFiles\\definition\\await.test.py /^ def __init__(self):$/;" kind:member line:4 +__init__ ..\\pythonFiles\\definition\\one.py /^ def __init__(self, file_path=None, file_contents=None):$/;" kind:member line:14 +__init__ ..\\pythonFiles\\formatting\\fileToFormat.py /^ def __init__ ( self, bar ):$/;" kind:member line:13 +__init__ ..\\pythonFiles\\jupyter\\cells.py /^ def __init__(self, mean=0.0, std=1, size=1000):$/;" kind:member line:104 +__init__ ..\\pythonFiles\\linting\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\linting\\flake8config\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\linting\\pep8config\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self):$/;" kind:member line:164 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, filePath, fileMode=ChangeType.EDIT, diff=""):$/;" kind:member line:48 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, name='Task Name', message=None, percent=0):$/;" kind:member line:26 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, project, resource, name="Extract Method", progressCallback=None, startOff/;" kind:member line:146 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, project, resource, name="Extract Variable", progressCallback=None, startO/;" kind:member line:122 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, project, resource, name="Refactor", progressCallback=None):$/;" kind:member line:59 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, project, resource, name="Rename", progressCallback=None, startOffset=None/;" kind:member line:103 +__init__ ..\\pythonFiles\\symbolFiles\\childFile.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\symbolFiles\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\symbolFiles\\workspace2File.py /^ def __init__(self):$/;" kind:member line:8 +__init__.py ..\\pythonFiles\\autoimport\\two\\__init__.py 1;" kind:file line:1 +__initialized ..\\pythonFiles\\autocomp\\misc.py /^ __initialized = False$/;" kind:variable line:646 +__reduce__ ..\\pythonFiles\\autocomp\\misc.py /^ def __reduce__(self):$/;" kind:member line:1428 +__repr__ ..\\pythonFiles\\autocomp\\misc.py /^ def __repr__(self):$/;" kind:member line:138 +__repr__ ..\\pythonFiles\\autocomp\\misc.py /^ def __repr__(self):$/;" kind:member line:291 +__repr__ ..\\pythonFiles\\autocomp\\misc.py /^ def __repr__(self):$/;" kind:member line:713 +__revision__ ..\\multiRootWkspc\\disableLinters\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\multiRootWkspc\\parent\\child\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\multiRootWkspc\\workspace1\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\multiRootWkspc\\workspace2\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\multiRootWkspc\\workspace3\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\linting\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\linting\\flake8config\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\linting\\pep8config\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\linting\\pylintconfig\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\symbolFiles\\childFile.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\symbolFiles\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\symbolFiles\\workspace2File.py /^__revision__ = None$/;" kind:variable line:3 +__setstate__ ..\\pythonFiles\\autocomp\\misc.py /^ def __setstate__(self, state): # for pickle$/;" kind:member line:1425 +__stop ..\\pythonFiles\\autocomp\\misc.py /^ def __stop(self):$/;" kind:member line:866 +_acquire_restore ..\\pythonFiles\\autocomp\\misc.py /^ def _acquire_restore(self, count_owner):$/;" kind:member line:220 +_acquire_restore ..\\pythonFiles\\autocomp\\misc.py /^ def _acquire_restore(self, x):$/;" kind:member line:297 +_active ..\\pythonFiles\\autocomp\\misc.py /^_active = {} # maps thread id to Thread object$/;" kind:variable line:634 +_active_limbo_lock ..\\pythonFiles\\autocomp\\misc.py /^_active_limbo_lock = _allocate_lock()$/;" kind:variable line:633 +_after_fork ..\\pythonFiles\\autocomp\\misc.py /^def _after_fork():$/;" kind:function line:1211 +_allocate_lock ..\\pythonFiles\\autocomp\\misc.py /^_allocate_lock = thread.allocate_lock$/;" kind:variable line:36 +_block ..\\pythonFiles\\autocomp\\misc.py /^ def _block(self):$/;" kind:member line:705 +_count ..\\pythonFiles\\autocomp\\misc.py /^from itertools import count as _count$/;" kind:unknown line:14 +_counter ..\\pythonFiles\\autocomp\\misc.py /^_counter = _count().next$/;" kind:variable line:627 +_deque ..\\pythonFiles\\autocomp\\misc.py /^from collections import deque as _deque$/;" kind:unknown line:13 +_deserialize ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def _deserialize(self, request):$/;" kind:member line:204 +_enumerate ..\\pythonFiles\\autocomp\\misc.py /^def _enumerate():$/;" kind:function line:1179 +_exitfunc ..\\pythonFiles\\autocomp\\misc.py /^ def _exitfunc(self):$/;" kind:member line:1100 +_extractMethod ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def _extractMethod(self, filePath, start, end, newName):$/;" kind:member line:183 +_extractVariable ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def _extractVariable(self, filePath, start, end, newName):$/;" kind:member line:168 +_figure_data ..\\pythonFiles\\jupyter\\cells.py /^ def _figure_data(self, format):$/;" kind:member line:112 +_format_exc ..\\pythonFiles\\autocomp\\misc.py /^from traceback import format_exc as _format_exc$/;" kind:unknown line:16 +_get_ident ..\\pythonFiles\\autocomp\\misc.py /^_get_ident = thread.get_ident$/;" kind:variable line:37 +_is_owned ..\\pythonFiles\\autocomp\\misc.py /^ def _is_owned(self):$/;" kind:member line:238 +_is_owned ..\\pythonFiles\\autocomp\\misc.py /^ def _is_owned(self):$/;" kind:member line:300 +_limbo ..\\pythonFiles\\autocomp\\misc.py /^_limbo = {}$/;" kind:variable line:635 +_newname ..\\pythonFiles\\autocomp\\misc.py /^def _newname(template="Thread-%d"):$/;" kind:function line:629 +_note ..\\pythonFiles\\autocomp\\misc.py /^ def _note(self, *args):$/;" kind:member line:82 +_note ..\\pythonFiles\\autocomp\\misc.py /^ def _note(self, format, *args):$/;" kind:member line:64 +_pickSomeNonDaemonThread ..\\pythonFiles\\autocomp\\misc.py /^def _pickSomeNonDaemonThread():$/;" kind:function line:1113 +_process_request ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def _process_request(self, request):$/;" kind:member line:215 +_profile_hook ..\\pythonFiles\\autocomp\\misc.py /^_profile_hook = None$/;" kind:variable line:87 +_randbelow ..\\pythonFiles\\autocomp\\misc.py /^ def _randbelow(self, n, int=int, maxsize=1< int:$/;" kind:function line:6 +after.py ..\\pythonFiles\\sorting\\noconfig\\after.py 1;" kind:file line:1 +after.py ..\\pythonFiles\\sorting\\withconfig\\after.py 1;" kind:file line:1 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^ def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:member line:263 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:function line:124 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:member line:263 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:function line:124 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:member line:263 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:function line:124 +await.test.py ..\\pythonFiles\\definition\\await.test.py 1;" kind:file line:1 +ax ..\\pythonFiles\\jupyter\\cells.py /^fig, ax = plt.subplots(subplot_kw=dict(axisbg='#EEEEEE'))$/;" kind:variable line:39 +b ..\\pythonFiles\\autocomp\\pep526.py /^ b: int = 0$/;" kind:variable line:18 +b ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLine2.py /^ b = 3$/;" kind:variable line:3 +b ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLine4.py /^ b = 3$/;" kind:variable line:3 +b ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLineTab.py /^ b = 3$/;" kind:variable line:3 +bar ..\\pythonFiles\\autocomp\\four.py /^ def bar():$/;" kind:member line:11 +bar ..\\pythonFiles\\definition\\four.py /^ def bar():$/;" kind:member line:11 +before.1.py ..\\pythonFiles\\sorting\\withconfig\\before.1.py 1;" kind:file line:1 +before.py ..\\pythonFiles\\sorting\\noconfig\\before.py 1;" kind:file line:1 +before.py ..\\pythonFiles\\sorting\\withconfig\\before.py 1;" kind:file line:1 +betavariate ..\\pythonFiles\\autocomp\\misc.py /^ def betavariate(self, alpha, beta):$/;" kind:member line:1862 +calculate_cash_flows ..\\pythonFiles\\definition\\decorators.py /^def calculate_cash_flows(remaining_loan_term, remaining_io_term,$/;" kind:function line:20 +cancel ..\\pythonFiles\\autocomp\\misc.py /^ def cancel(self):$/;" kind:member line:1075 +cells.py ..\\pythonFiles\\jupyter\\cells.py 1;" kind:file line:1 +childFile.py ..\\pythonFiles\\symbolFiles\\childFile.py 1;" kind:file line:1 +choice ..\\pythonFiles\\autocomp\\misc.py /^ def choice(self, seq):$/;" kind:member line:1513 +clear ..\\pythonFiles\\autocomp\\misc.py /^ def clear(self):$/;" kind:member line:590 +content ..\\pythonFiles\\autocomp\\doc.py /^ content = line.upper()$/;" kind:variable line:6 +ct ..\\pythonFiles\\autocomp\\two.py /^class ct:$/;" kind:class line:1 +ct ..\\pythonFiles\\definition\\two.py /^class ct:$/;" kind:class line:1 +currentThread ..\\pythonFiles\\autocomp\\misc.py /^def currentThread():$/;" kind:function line:1152 +current_thread ..\\pythonFiles\\autocomp\\misc.py /^current_thread = currentThread$/;" kind:variable line:1165 +daemon ..\\pythonFiles\\autocomp\\misc.py /^ def daemon(self):$/;" kind:member line:1009 +daemon ..\\pythonFiles\\autocomp\\misc.py /^ def daemon(self, daemonic):$/;" kind:member line:1025 +deco.py ..\\pythonFiles\\autocomp\\deco.py 1;" kind:file line:1 +decorators.py ..\\pythonFiles\\definition\\decorators.py 1;" kind:file line:1 +description ..\\pythonFiles\\autocomp\\one.py /^ description = "Run isort on modules registered in setuptools"$/;" kind:variable line:11 +description ..\\pythonFiles\\definition\\one.py /^ description = "Run isort on modules registered in setuptools"$/;" kind:variable line:11 +df ..\\pythonFiles\\jupyter\\cells.py /^df = df.cumsum()$/;" kind:variable line:87 +df ..\\pythonFiles\\jupyter\\cells.py /^df = pd.DataFrame(np.random.randn(1000, 4), index=ts.index,$/;" kind:variable line:85 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^ def divide(x, y):$/;" kind:member line:329 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^def divide(x, y):$/;" kind:function line:190 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ def divide(x, y):$/;" kind:member line:329 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^def divide(x, y):$/;" kind:function line:190 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ def divide(x, y):$/;" kind:member line:329 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^def divide(x, y):$/;" kind:function line:190 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def divide(x, y):$/;" kind:function line:188 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def divide(x, y):$/;" kind:function line:199 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def divide(x, y):$/;" kind:function line:188 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def divide(x, y):$/;" kind:function line:199 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def divide(x, y):$/;" kind:function line:188 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def divide(x, y):$/;" kind:function line:199 +doc.py ..\\pythonFiles\\autocomp\\doc.py 1;" kind:file line:1 +dummy.py ..\\pythonFiles\\dummy.py 1;" kind:file line:1 +elseBlocks2.py ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py 1;" kind:file line:1 +elseBlocks4.py ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py 1;" kind:file line:1 +elseBlocksFirstLine2.py ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLine2.py 1;" kind:file line:1 +elseBlocksFirstLine4.py ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLine4.py 1;" kind:file line:1 +elseBlocksFirstLineTab.py ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLineTab.py 1;" kind:file line:1 +elseBlocksTab.py ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py 1;" kind:file line:1 +enumerate ..\\pythonFiles\\autocomp\\misc.py /^def enumerate():$/;" kind:function line:1183 +example1 ..\\pythonFiles\\formatting\\fileToFormat.py /^def example1():$/;" kind:function line:3 +example2 ..\\pythonFiles\\formatting\\fileToFormat.py /^def example2(): return {'has_key() is deprecated':True}.has_key({'f':2}.has_key(''));$/;" kind:function line:11 +expovariate ..\\pythonFiles\\autocomp\\misc.py /^ def expovariate(self, lambd):$/;" kind:member line:1670 +fig ..\\pythonFiles\\jupyter\\cells.py /^fig, ax = plt.subplots(subplot_kw=dict(axisbg='#EEEEEE'))$/;" kind:variable line:39 +file.py ..\\multiRootWkspc\\disableLinters\\file.py 1;" kind:file line:1 +file.py ..\\multiRootWkspc\\parent\\child\\file.py 1;" kind:file line:1 +file.py ..\\multiRootWkspc\\workspace1\\file.py 1;" kind:file line:1 +file.py ..\\multiRootWkspc\\workspace2\\file.py 1;" kind:file line:1 +file.py ..\\multiRootWkspc\\workspace3\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\linting\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\linting\\flake8config\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\linting\\pep8config\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\linting\\pylintconfig\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\symbolFiles\\file.py 1;" kind:file line:1 +fileToFormat.py ..\\pythonFiles\\formatting\\fileToFormat.py 1;" kind:file line:1 +five.py ..\\pythonFiles\\autocomp\\five.py 1;" kind:file line:1 +five.py ..\\pythonFiles\\definition\\five.py 1;" kind:file line:1 +four.py ..\\pythonFiles\\autocomp\\four.py 1;" kind:file line:1 +four.py ..\\pythonFiles\\definition\\four.py 1;" kind:file line:1 +fun ..\\pythonFiles\\autocomp\\two.py /^ def fun():$/;" kind:member line:2 +fun ..\\pythonFiles\\definition\\two.py /^ def fun():$/;" kind:member line:2 +function1 ..\\pythonFiles\\definition\\one.py /^def function1():$/;" kind:function line:33 +function2 ..\\pythonFiles\\definition\\one.py /^def function2():$/;" kind:function line:37 +function3 ..\\pythonFiles\\definition\\one.py /^def function3():$/;" kind:function line:40 +function4 ..\\pythonFiles\\definition\\one.py /^def function4():$/;" kind:function line:43 +gammavariate ..\\pythonFiles\\autocomp\\misc.py /^ def gammavariate(self, alpha, beta):$/;" kind:member line:1737 +gauss ..\\pythonFiles\\autocomp\\misc.py /^ def gauss(self, mu, sigma):$/;" kind:member line:1809 +get ..\\pythonFiles\\autocomp\\misc.py /^ def get(self):$/;" kind:member line:1271 +getName ..\\pythonFiles\\autocomp\\misc.py /^ def getName(self):$/;" kind:member line:1038 +getstate ..\\pythonFiles\\autocomp\\misc.py /^ def getstate(self):$/;" kind:member line:1388 +greeting ..\\pythonFiles\\autocomp\\pep484.py /^def greeting(name: str) -> str:$/;" kind:function line:2 +hoverTest.py ..\\pythonFiles\\autocomp\\hoverTest.py 1;" kind:file line:1 +ident ..\\pythonFiles\\autocomp\\misc.py /^ def ident(self):$/;" kind:member line:984 +identity ..\\pythonFiles\\definition\\decorators.py /^def identity(ob):$/;" kind:function line:1 +imp.py ..\\pythonFiles\\autocomp\\imp.py 1;" kind:file line:1 +instant_print ..\\pythonFiles\\autocomp\\lamb.py /^instant_print = lambda x: [print(x), sys.stdout.flush(), sys.stderr.flush()]$/;" kind:function line:1 +isAlive ..\\pythonFiles\\autocomp\\misc.py /^ def isAlive(self):$/;" kind:member line:995 +isDaemon ..\\pythonFiles\\autocomp\\misc.py /^ def isDaemon(self):$/;" kind:member line:1032 +isSet ..\\pythonFiles\\autocomp\\misc.py /^ def isSet(self):$/;" kind:member line:570 +is_alive ..\\pythonFiles\\autocomp\\misc.py /^ is_alive = isAlive$/;" kind:variable line:1006 +is_set ..\\pythonFiles\\autocomp\\misc.py /^ is_set = isSet$/;" kind:variable line:574 +join ..\\pythonFiles\\autocomp\\misc.py /^ def join(self, timeout=None):$/;" kind:member line:1146 +join ..\\pythonFiles\\autocomp\\misc.py /^ def join(self, timeout=None):$/;" kind:member line:911 +lamb.py ..\\pythonFiles\\autocomp\\lamb.py 1;" kind:file line:1 +local ..\\pythonFiles\\autocomp\\misc.py /^ from thread import _local as local$/;" kind:unknown line:1206 +lognormvariate ..\\pythonFiles\\autocomp\\misc.py /^ def lognormvariate(self, mu, sigma):$/;" kind:member line:1658 +meth1 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\linting\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\linting\\pep8config\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1OfChild ..\\pythonFiles\\symbolFiles\\childFile.py /^ def meth1OfChild(self, arg):$/;" kind:member line:11 +meth1OfWorkspace2 ..\\pythonFiles\\symbolFiles\\workspace2File.py /^ def meth1OfWorkspace2(self, arg):$/;" kind:member line:11 +meth2 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\linting\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\linting\\pep8config\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\linting\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\linting\\pep8config\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\linting\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\linting\\pep8config\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\linting\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\linting\\pep8config\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\linting\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\linting\\pep8config\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\linting\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\linting\\pep8config\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\linting\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\linting\\pep8config\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth8(self):$/;" kind:member line:80 +method1 ..\\pythonFiles\\autocomp\\one.py /^ def method1(self):$/;" kind:member line:18 +method1 ..\\pythonFiles\\definition\\one.py /^ def method1(self):$/;" kind:member line:18 +method2 ..\\pythonFiles\\autocomp\\one.py /^ def method2(self):$/;" kind:member line:24 +method2 ..\\pythonFiles\\definition\\one.py /^ def method2(self):$/;" kind:member line:24 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^ def minus():$/;" kind:member line:287 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^def minus():$/;" kind:function line:148 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ def minus():$/;" kind:member line:287 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^def minus():$/;" kind:function line:148 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ def minus():$/;" kind:member line:287 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^def minus():$/;" kind:function line:148 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def minus():$/;" kind:function line:100 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def minus():$/;" kind:function line:91 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def minus():$/;" kind:function line:100 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def minus():$/;" kind:function line:91 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def minus():$/;" kind:function line:100 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def minus():$/;" kind:function line:91 +misc.py ..\\pythonFiles\\autocomp\\misc.py 1;" kind:file line:1 +mpl ..\\pythonFiles\\jupyter\\cells.py /^import matplotlib as mpl$/;" kind:namespace line:4 +mpl ..\\pythonFiles\\jupyter\\cells.py /^import matplotlib as mpl$/;" kind:namespace line:94 +myfunc ..\\pythonFiles\\definition\\decorators.py /^def myfunc():$/;" kind:function line:5 +name ..\\pythonFiles\\autocomp\\misc.py /^ def name(self):$/;" kind:member line:968 +name ..\\pythonFiles\\autocomp\\misc.py /^ def name(self, name):$/;" kind:member line:979 +non_parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_another_pytest.py /^def non_parametrized_username(request):$/;" kind:function line:10 +non_parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^def non_parametrized_username(request):$/;" kind:function line:33 +non_parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^def non_parametrized_username(request):$/;" kind:function line:33 +non_parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_another_pytest.py /^def non_parametrized_username(request):$/;" kind:function line:10 +non_parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^def non_parametrized_username(request):$/;" kind:function line:33 +normalvariate ..\\pythonFiles\\autocomp\\misc.py /^ def normalvariate(self, mu, sigma):$/;" kind:member line:1633 +notify ..\\pythonFiles\\autocomp\\misc.py /^ def notify(self, n=1):$/;" kind:member line:373 +notifyAll ..\\pythonFiles\\autocomp\\misc.py /^ def notifyAll(self):$/;" kind:member line:400 +notify_all ..\\pythonFiles\\autocomp\\misc.py /^ notify_all = notifyAll$/;" kind:variable line:409 +np ..\\pythonFiles\\jupyter\\cells.py /^import numpy as np$/;" kind:namespace line:34 +np ..\\pythonFiles\\jupyter\\cells.py /^import numpy as np$/;" kind:namespace line:5 +np ..\\pythonFiles\\jupyter\\cells.py /^import numpy as np$/;" kind:namespace line:63 +np ..\\pythonFiles\\jupyter\\cells.py /^import numpy as np$/;" kind:namespace line:78 +np ..\\pythonFiles\\jupyter\\cells.py /^import numpy as np$/;" kind:namespace line:97 +obj ..\\pythonFiles\\autocomp\\one.py /^obj = Class1()$/;" kind:variable line:30 +obj ..\\pythonFiles\\definition\\one.py /^obj = Class1()$/;" kind:variable line:30 +onRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def onRefactor(self):$/;" kind:member line:109 +onRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def onRefactor(self):$/;" kind:member line:131 +onRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def onRefactor(self):$/;" kind:member line:149 +onRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def onRefactor(self):$/;" kind:member line:94 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def one():$/;" kind:function line:134 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def one():$/;" kind:function line:150 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def one():$/;" kind:function line:134 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def one():$/;" kind:function line:150 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def one():$/;" kind:function line:134 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def one():$/;" kind:function line:150 +one.py ..\\pythonFiles\\autocomp\\one.py 1;" kind:file line:1 +one.py ..\\pythonFiles\\autoimport\\one.py 1;" kind:file line:1 +one.py ..\\pythonFiles\\definition\\one.py 1;" kind:file line:1 +one.py ..\\pythonFiles\\docstrings\\one.py 1;" kind:file line:1 +original.1.py ..\\pythonFiles\\sorting\\withconfig\\original.1.py 1;" kind:file line:1 +original.py ..\\pythonFiles\\sorting\\noconfig\\original.py 1;" kind:file line:1 +original.py ..\\pythonFiles\\sorting\\withconfig\\original.py 1;" kind:file line:1 +p1 ..\\pythonFiles\\jupyter\\cells.py /^p1 = figure(title="Legend Example", tools=TOOLS)$/;" kind:variable line:70 +parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_another_pytest.py /^def parametrized_username():$/;" kind:function line:6 +parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^def parametrized_username():$/;" kind:function line:29 +parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^def parametrized_username():$/;" kind:function line:29 +parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_another_pytest.py /^def parametrized_username():$/;" kind:function line:6 +parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^def parametrized_username():$/;" kind:function line:29 +paretovariate ..\\pythonFiles\\autocomp\\misc.py /^ def paretovariate(self, alpha):$/;" kind:member line:1880 +pd ..\\pythonFiles\\jupyter\\cells.py /^import pandas as pd$/;" kind:namespace line:77 +pep484.py ..\\pythonFiles\\autocomp\\pep484.py 1;" kind:file line:1 +pep526.py ..\\pythonFiles\\autocomp\\pep526.py 1;" kind:file line:1 +plain.py ..\\pythonFiles\\shebang\\plain.py 1;" kind:file line:1 +plt ..\\pythonFiles\\jupyter\\cells.py /^from matplotlib import pyplot as plt$/;" kind:unknown line:80 +plt ..\\pythonFiles\\jupyter\\cells.py /^import matplotlib.pyplot as plt$/;" kind:namespace line:3 +plt ..\\pythonFiles\\jupyter\\cells.py /^import matplotlib.pyplot as plt$/;" kind:namespace line:33 +plt ..\\pythonFiles\\jupyter\\cells.py /^import matplotlib.pyplot as plt$/;" kind:namespace line:93 +print_hello ..\\pythonFiles\\hover\\stringFormat.py /^def print_hello(name):$/;" kind:function line:2 +put ..\\pythonFiles\\autocomp\\misc.py /^ def put(self, item):$/;" kind:member line:1260 +randint ..\\pythonFiles\\autocomp\\misc.py /^ def randint(self, a, b):$/;" kind:member line:1477 +randrange ..\\pythonFiles\\autocomp\\misc.py /^ def randrange(self, start, stop=None, step=1, _int=int):$/;" kind:member line:1433 +refactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def refactor(self):$/;" kind:member line:87 +refactor.py ..\\pythonFiles\\refactoring\\standAlone\\refactor.py 1;" kind:file line:1 +release ..\\pythonFiles\\autocomp\\misc.py /^ def release(self):$/;" kind:member line:187 +release ..\\pythonFiles\\autocomp\\misc.py /^ def release(self):$/;" kind:member line:479 +release ..\\pythonFiles\\autocomp\\misc.py /^ def release(self):$/;" kind:member line:525 +rnd ..\\pythonFiles\\autocomp\\hoverTest.py /^rnd = random.Random()$/;" kind:variable line:7 +rnd2 ..\\pythonFiles\\autocomp\\hoverTest.py /^rnd2 = misc.Random()$/;" kind:variable line:12 +run ..\\pythonFiles\\autocomp\\misc.py /^ def run(self):$/;" kind:member line:1289 +run ..\\pythonFiles\\autocomp\\misc.py /^ def run(self):$/;" kind:member line:1305 +run ..\\pythonFiles\\autocomp\\misc.py /^ def run(self):$/;" kind:member line:1079 +run ..\\pythonFiles\\autocomp\\misc.py /^ def run(self):$/;" kind:member line:752 +sample ..\\pythonFiles\\autocomp\\misc.py /^ def sample(self, population, k):$/;" kind:member line:1543 +scatter ..\\pythonFiles\\jupyter\\cells.py /^scatter = ax.scatter(np.random.normal(size=N),$/;" kind:variable line:43 +seed ..\\pythonFiles\\autocomp\\misc.py /^ def seed(self, a=None, version=2):$/;" kind:member line:1356 +set ..\\pythonFiles\\autocomp\\misc.py /^ def set(self):$/;" kind:member line:576 +setDaemon ..\\pythonFiles\\autocomp\\misc.py /^ def setDaemon(self, daemonic):$/;" kind:member line:1035 +setName ..\\pythonFiles\\autocomp\\misc.py /^ def setName(self, name):$/;" kind:member line:1041 +setprofile ..\\pythonFiles\\autocomp\\misc.py /^def setprofile(func):$/;" kind:function line:90 +setstate ..\\pythonFiles\\autocomp\\misc.py /^ def setstate(self, state):$/;" kind:member line:1392 +settrace ..\\pythonFiles\\autocomp\\misc.py /^def settrace(func):$/;" kind:function line:100 +shebang.py ..\\pythonFiles\\shebang\\shebang.py 1;" kind:file line:1 +shebangEnv.py ..\\pythonFiles\\shebang\\shebangEnv.py 1;" kind:file line:1 +shebangInvalid.py ..\\pythonFiles\\shebang\\shebangInvalid.py 1;" kind:file line:1 +showMessage ..\\pythonFiles\\autocomp\\four.py /^def showMessage():$/;" kind:function line:19 +showMessage ..\\pythonFiles\\definition\\four.py /^def showMessage():$/;" kind:function line:19 +shuffle ..\\pythonFiles\\autocomp\\misc.py /^ def shuffle(self, x, random=None):$/;" kind:member line:1521 +start ..\\pythonFiles\\autocomp\\misc.py /^ def start(self):$/;" kind:member line:726 +stop ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def stop(self):$/;" kind:member line:84 +stringFormat.py ..\\pythonFiles\\hover\\stringFormat.py 1;" kind:file line:1 +t ..\\pythonFiles\\autocomp\\hoverTest.py /^t = misc.Thread()$/;" kind:variable line:15 +test ..\\pythonFiles\\definition\\await.test.py /^ async def test(self):$/;" kind:member line:7 +test ..\\pythonFiles\\sorting\\noconfig\\after.py /^def test():$/;" kind:function line:15 +test ..\\pythonFiles\\sorting\\noconfig\\before.py /^def test():$/;" kind:function line:12 +test ..\\pythonFiles\\sorting\\noconfig\\original.py /^def test():$/;" kind:function line:12 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^ def test():$/;" kind:member line:201 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^def test():$/;" kind:function line:62 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ def test():$/;" kind:member line:201 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^def test():$/;" kind:function line:62 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ def test():$/;" kind:member line:201 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^def test():$/;" kind:function line:62 +test2 ..\\pythonFiles\\definition\\await.test.py /^ async def test2(self):$/;" kind:member line:10 +test_1_1_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^ def test_1_1_1(self):$/;" kind:member line:4 +test_1_1_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^ def test_1_1_1(self):$/;" kind:member line:4 +test_1_1_2 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^ def test_1_1_2(self):$/;" kind:member line:7 +test_1_1_2 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^ def test_1_1_2(self):$/;" kind:member line:7 +test_1_1_3 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^ def test_1_1_3(self):$/;" kind:member line:11 +test_1_1_3 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^ def test_1_1_3(self):$/;" kind:member line:11 +test_1_2_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^ def test_1_2_1(self):$/;" kind:member line:15 +test_222A2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_222A2(self):$/;" kind:member line:18 +test_222A2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ def test_222A2(self):$/;" kind:member line:18 +test_222A2wow ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_222A2wow(self):$/;" kind:member line:25 +test_222A2wow ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ def test_222A2wow(self):$/;" kind:member line:25 +test_222B2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_222B2(self):$/;" kind:member line:21 +test_222B2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ def test_222B2(self):$/;" kind:member line:21 +test_222B2wow ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_222B2wow(self):$/;" kind:member line:28 +test_222B2wow ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ def test_222B2wow(self):$/;" kind:member line:28 +test_2_1_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^ def test_2_1_1(self):$/;" kind:member line:15 +test_A ..\\pythonFiles\\testFiles\\single\\tests\\test_one.py /^ def test_A(self):$/;" kind:member line:7 +test_A ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_one.py /^ def test_A(self):$/;" kind:member line:7 +test_A ..\\pythonFiles\\testFiles\\standard\\tests\\unittest_three_test.py /^ def test_A(self):$/;" kind:member line:5 +test_A ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_unittest_one.py /^ def test_A(self):$/;" kind:member line:7 +test_A ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_one.py /^ def test_A(self):$/;" kind:member line:7 +test_A ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\unittest_three_test.py /^ def test_A(self):$/;" kind:member line:5 +test_A2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_A2(self):$/;" kind:member line:4 +test_A2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ def test_A2(self):$/;" kind:member line:4 +test_B ..\\pythonFiles\\testFiles\\single\\tests\\test_one.py /^ def test_B(self):$/;" kind:member line:10 +test_B ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_one.py /^ def test_B(self):$/;" kind:member line:10 +test_B ..\\pythonFiles\\testFiles\\standard\\tests\\unittest_three_test.py /^ def test_B(self):$/;" kind:member line:8 +test_B ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_unittest_one.py /^ def test_B(self):$/;" kind:member line:10 +test_B ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_one.py /^ def test_B(self):$/;" kind:member line:10 +test_B ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\unittest_three_test.py /^ def test_B(self):$/;" kind:member line:8 +test_B2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_B2(self):$/;" kind:member line:7 +test_B2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ def test_B2(self):$/;" kind:member line:7 +test_C2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_C2(self):$/;" kind:member line:10 +test_C2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ def test_C2(self):$/;" kind:member line:10 +test_D2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_D2(self):$/;" kind:member line:13 +test_D2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ def test_D2(self):$/;" kind:member line:13 +test_Root_A ..\\pythonFiles\\testFiles\\single\\test_root.py /^ def test_Root_A(self):$/;" kind:member line:7 +test_Root_A ..\\pythonFiles\\testFiles\\standard\\test_root.py /^ def test_Root_A(self):$/;" kind:member line:7 +test_Root_A ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\test_root.py /^ def test_Root_A(self):$/;" kind:member line:7 +test_Root_B ..\\pythonFiles\\testFiles\\single\\test_root.py /^ def test_Root_B(self):$/;" kind:member line:10 +test_Root_B ..\\pythonFiles\\testFiles\\standard\\test_root.py /^ def test_Root_B(self):$/;" kind:member line:10 +test_Root_B ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\test_root.py /^ def test_Root_B(self):$/;" kind:member line:10 +test_Root_c ..\\pythonFiles\\testFiles\\single\\test_root.py /^ def test_Root_c(self):$/;" kind:member line:14 +test_Root_c ..\\pythonFiles\\testFiles\\standard\\test_root.py /^ def test_Root_c(self):$/;" kind:member line:14 +test_Root_c ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\test_root.py /^ def test_Root_c(self):$/;" kind:member line:14 +test_another_pytest.py ..\\pythonFiles\\testFiles\\standard\\tests\\test_another_pytest.py 1;" kind:file line:1 +test_another_pytest.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_another_pytest.py 1;" kind:file line:1 +test_c ..\\pythonFiles\\testFiles\\single\\tests\\test_one.py /^ def test_c(self):$/;" kind:member line:14 +test_c ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_one.py /^ def test_c(self):$/;" kind:member line:14 +test_c ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_unittest_one.py /^ def test_c(self):$/;" kind:member line:14 +test_c ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_one.py /^ def test_c(self):$/;" kind:member line:14 +test_complex_check ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_complex_check(self):$/;" kind:member line:10 +test_complex_check ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_complex_check(self):$/;" kind:member line:10 +test_complex_check ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ def test_complex_check(self):$/;" kind:member line:10 +test_complex_check2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_complex_check2(self):$/;" kind:member line:24 +test_complex_check2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_complex_check2(self):$/;" kind:member line:24 +test_complex_check2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ def test_complex_check2(self):$/;" kind:member line:24 +test_cwd ..\\pythonFiles\\testFiles\\cwd\\src\\tests\\test_cwd.py /^ def test_cwd(self):$/;" kind:member line:7 +test_cwd.py ..\\pythonFiles\\testFiles\\cwd\\src\\tests\\test_cwd.py 1;" kind:file line:1 +test_d ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_d(self):$/;" kind:member line:17 +test_d ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_d(self):$/;" kind:member line:17 +test_d ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ def test_d(self):$/;" kind:member line:17 +test_nested_class_methodB ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_nested_class_methodB(self):$/;" kind:member line:14 +test_nested_class_methodB ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_nested_class_methodB(self):$/;" kind:member line:14 +test_nested_class_methodB ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ def test_nested_class_methodB(self):$/;" kind:member line:14 +test_nested_class_methodC ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_nested_class_methodC(self):$/;" kind:member line:19 +test_nested_class_methodC ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_nested_class_methodC(self):$/;" kind:member line:19 +test_nested_class_methodC ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ def test_nested_class_methodC(self):$/;" kind:member line:19 +test_one.py ..\\pythonFiles\\testFiles\\single\\tests\\test_one.py 1;" kind:file line:1 +test_parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_another_pytest.py /^def test_parametrized_username(non_parametrized_username):$/;" kind:function line:16 +test_parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^def test_parametrized_username(non_parametrized_username):$/;" kind:function line:39 +test_parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^def test_parametrized_username(non_parametrized_username):$/;" kind:function line:39 +test_parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_another_pytest.py /^def test_parametrized_username(non_parametrized_username):$/;" kind:function line:16 +test_parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^def test_parametrized_username(non_parametrized_username):$/;" kind:function line:39 +test_pytest.py ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py 1;" kind:file line:1 +test_pytest.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py 1;" kind:file line:1 +test_pytest.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py 1;" kind:file line:1 +test_root.py ..\\pythonFiles\\testFiles\\single\\test_root.py 1;" kind:file line:1 +test_root.py ..\\pythonFiles\\testFiles\\standard\\test_root.py 1;" kind:file line:1 +test_root.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\test_root.py 1;" kind:file line:1 +test_simple_check ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_simple_check(self):$/;" kind:member line:8 +test_simple_check ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_simple_check(self):$/;" kind:member line:8 +test_simple_check ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ def test_simple_check(self):$/;" kind:member line:8 +test_simple_check2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_simple_check2(self):$/;" kind:member line:22 +test_simple_check2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_simple_check2(self):$/;" kind:member line:22 +test_simple_check2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ def test_simple_check2(self):$/;" kind:member line:22 +test_unittest_one.py ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py 1;" kind:file line:1 +test_unittest_one.py ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_one.py 1;" kind:file line:1 +test_unittest_one.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_unittest_one.py 1;" kind:file line:1 +test_unittest_one.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_one.py 1;" kind:file line:1 +test_unittest_two.py ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py 1;" kind:file line:1 +test_unittest_two.py ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py 1;" kind:file line:1 +test_unittest_two.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py 1;" kind:file line:1 +test_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_another_pytest.py /^def test_username(parametrized_username):$/;" kind:function line:13 +test_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^def test_username(parametrized_username):$/;" kind:function line:36 +test_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^def test_username(parametrized_username):$/;" kind:function line:36 +test_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_another_pytest.py /^def test_username(parametrized_username):$/;" kind:function line:13 +test_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^def test_username(parametrized_username):$/;" kind:function line:36 +testthis ..\\pythonFiles\\definition\\await.test.py /^async def testthis():$/;" kind:function line:13 +three.py ..\\pythonFiles\\autocomp\\three.py 1;" kind:file line:1 +three.py ..\\pythonFiles\\autoimport\\two\\three.py 1;" kind:file line:1 +three.py ..\\pythonFiles\\definition\\three.py 1;" kind:file line:1 +triangular ..\\pythonFiles\\autocomp\\misc.py /^ def triangular(self, low=0.0, high=1.0, mode=None):$/;" kind:member line:1611 +tryBlocks2.py ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py 1;" kind:file line:1 +tryBlocks4.py ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py 1;" kind:file line:1 +tryBlocksTab.py ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py 1;" kind:file line:1 +ts ..\\pythonFiles\\jupyter\\cells.py /^ts = pd.Series(np.random.randn(1000),$/;" kind:variable line:82 +ts ..\\pythonFiles\\jupyter\\cells.py /^ts = ts.cumsum()$/;" kind:variable line:84 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^ def two():$/;" kind:member line:308 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^def two():$/;" kind:function line:169 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ def two():$/;" kind:member line:308 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^def two():$/;" kind:function line:169 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ def two():$/;" kind:member line:308 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^def two():$/;" kind:function line:169 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def two():$/;" kind:function line:166 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def two():$/;" kind:function line:177 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def two():$/;" kind:function line:166 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def two():$/;" kind:function line:177 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def two():$/;" kind:function line:166 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def two():$/;" kind:function line:177 +two.py ..\\pythonFiles\\autocomp\\two.py 1;" kind:file line:1 +two.py ..\\pythonFiles\\definition\\two.py 1;" kind:file line:1 +uniform ..\\pythonFiles\\autocomp\\misc.py /^ def uniform(self, a, b):$/;" kind:member line:1605 +unittest_three_test.py ..\\pythonFiles\\testFiles\\standard\\tests\\unittest_three_test.py 1;" kind:file line:1 +unittest_three_test.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\unittest_three_test.py 1;" kind:file line:1 +user_options ..\\pythonFiles\\autocomp\\one.py /^ user_options = []$/;" kind:variable line:12 +user_options ..\\pythonFiles\\definition\\one.py /^ user_options = []$/;" kind:variable line:12 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^var = 100$/;" kind:variable line:1 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^var = 100$/;" kind:variable line:15 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^var = 100$/;" kind:variable line:29 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^var = 100$/;" kind:variable line:339 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^var = 100$/;" kind:variable line:353 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ var = 100$/;" kind:variable line:339 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^var = 100$/;" kind:variable line:1 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^var = 100$/;" kind:variable line:15 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^var = 100$/;" kind:variable line:29 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ var = 100$/;" kind:variable line:339 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^var = 100$/;" kind:variable line:1 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^var = 100$/;" kind:variable line:15 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^var = 100$/;" kind:variable line:29 +vonmisesvariate ..\\pythonFiles\\autocomp\\misc.py /^ def vonmisesvariate(self, mu, kappa):$/;" kind:member line:1689 +wait ..\\pythonFiles\\autocomp\\misc.py /^ def wait(self, timeout=None):$/;" kind:member line:309 +wait ..\\pythonFiles\\autocomp\\misc.py /^ def wait(self, timeout=None):$/;" kind:member line:603 +watch ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def watch(self):$/;" kind:member line:234 +weibullvariate ..\\pythonFiles\\autocomp\\misc.py /^ def weibullvariate(self, alpha, beta):$/;" kind:member line:1889 +workspace2File.py ..\\pythonFiles\\symbolFiles\\workspace2File.py 1;" kind:file line:1 +x ..\\pythonFiles\\jupyter\\cells.py /^x = Gaussian(2.0, 1.0)$/;" kind:variable line:131 +x ..\\pythonFiles\\jupyter\\cells.py /^x = np.linspace(0, 20, 100)$/;" kind:variable line:7 +x ..\\pythonFiles\\jupyter\\cells.py /^x = np.linspace(0, 4 * np.pi, 100)$/;" kind:variable line:65 +y ..\\pythonFiles\\jupyter\\cells.py /^y = np.sin(x)$/;" kind:variable line:66 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def zero():$/;" kind:function line:110 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def zero():$/;" kind:function line:122 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def zero():$/;" kind:function line:110 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def zero():$/;" kind:function line:122 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def zero():$/;" kind:function line:110 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def zero():$/;" kind:function line:122 diff --git a/src/test/autocomplete/base.test.ts b/src/test/autocomplete/base.test.ts index 609bed153456..a5398b7beb43 100644 --- a/src/test/autocomplete/base.test.ts +++ b/src/test/autocomplete/base.test.ts @@ -10,10 +10,11 @@ import { EOL } from 'os'; import * as vscode from 'vscode'; import * as path from 'path'; import * as settings from '../../client/common/configSettings'; -import { initialize, closeActiveWindows } from '../initialize'; +import { initialize, closeActiveWindows, initializeTest } from '../initialize'; import { execPythonFile } from '../../client/common/utils'; +import { PythonSettings } from '../../client/common/configSettings'; +import { rootWorkspaceUri } from '../common'; -const pythonSettings = settings.PythonSettings.getInstance(); const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); const fileOne = path.join(autoCompPath, 'one.py'); const fileImport = path.join(autoCompPath, 'imp.py'); @@ -27,10 +28,10 @@ suite('Autocomplete', () => { let isPython3: Promise; suiteSetup(async () => { await initialize(); - let version = await execPythonFile(pythonSettings.pythonPath, ['--version'], __dirname, true); + const version = await execPythonFile(rootWorkspaceUri, PythonSettings.getInstance(rootWorkspaceUri).pythonPath, ['--version'], __dirname, true); isPython3 = Promise.resolve(version.indexOf('3.') >= 0); }); - + setup(() => initializeTest()); suiteTeardown(() => closeActiveWindows()); teardown(() => closeActiveWindows()); @@ -114,7 +115,7 @@ suite('Autocomplete', () => { const position = new vscode.Position(10, 9); const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); assert.notEqual(list.items.filter(item => item.label === 'sleep').length, 0, 'sleep not found'); - assert.notEqual(list.items.filter(item => item.documentation.startsWith("Delay execution for a given number of seconds. The argument may be")).length, 0, 'Documentation incorrect'); + assert.notEqual(list.items.filter(item => item.documentation.toString().startsWith("Delay execution for a given number of seconds. The argument may be")).length, 0, 'Documentation incorrect'); }); test('For custom class', done => { diff --git a/src/test/autocomplete/pep484.test.ts b/src/test/autocomplete/pep484.test.ts index 6108b3e8c1b4..c0c327bdf7d1 100644 --- a/src/test/autocomplete/pep484.test.ts +++ b/src/test/autocomplete/pep484.test.ts @@ -11,9 +11,10 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as settings from '../../client/common/configSettings'; import { execPythonFile } from '../../client/common/utils'; -import { initialize, closeActiveWindows } from '../initialize'; +import { initialize, closeActiveWindows, initializeTest } from '../initialize'; +import { PythonSettings } from '../../client/common/configSettings'; +import { rootWorkspaceUri } from '../common'; -const pythonSettings = settings.PythonSettings.getInstance(); const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); const filePep484 = path.join(autoCompPath, 'pep484.py'); @@ -21,9 +22,10 @@ suite('Autocomplete PEP 484', () => { let isPython3: Promise; suiteSetup(async () => { await initialize(); - const version = await execPythonFile(pythonSettings.pythonPath, ['--version'], __dirname, true); + const version = await execPythonFile(rootWorkspaceUri, PythonSettings.getInstance(rootWorkspaceUri).pythonPath, ['--version'], __dirname, true); isPython3 = Promise.resolve(version.indexOf('3.') >= 0); }); + setup(() => initializeTest()); suiteTeardown(() => closeActiveWindows()); teardown(() => closeActiveWindows()); diff --git a/src/test/autocomplete/pep526.test.ts b/src/test/autocomplete/pep526.test.ts index 1d09d8168682..01dc988c000f 100644 --- a/src/test/autocomplete/pep526.test.ts +++ b/src/test/autocomplete/pep526.test.ts @@ -11,9 +11,10 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as settings from '../../client/common/configSettings'; import { execPythonFile } from '../../client/common/utils'; -import { initialize, closeActiveWindows } from '../initialize'; +import { initialize, closeActiveWindows, initializeTest } from '../initialize'; +import { PythonSettings } from '../../client/common/configSettings'; +import { rootWorkspaceUri } from '../common'; -const pythonSettings = settings.PythonSettings.getInstance(); const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); const filePep526 = path.join(autoCompPath, 'pep526.py'); @@ -21,9 +22,10 @@ suite('Autocomplete PEP 526', () => { let isPython3: Promise; suiteSetup(async () => { await initialize(); - const version = await execPythonFile(pythonSettings.pythonPath, ['--version'], __dirname, true); + const version = await execPythonFile(rootWorkspaceUri, PythonSettings.getInstance(rootWorkspaceUri).pythonPath, ['--version'], __dirname, true); isPython3 = Promise.resolve(version.indexOf('3.') >= 0); }); + setup(() => initializeTest()); suiteTeardown(() => closeActiveWindows()); teardown(() => closeActiveWindows()); diff --git a/src/test/common.ts b/src/test/common.ts new file mode 100644 index 000000000000..fe8bc2b35547 --- /dev/null +++ b/src/test/common.ts @@ -0,0 +1,112 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { PythonSettings } from '../client/common/configSettings'; +import { IS_MULTI_ROOT_TEST } from './initialize'; + +const fileInNonRootWorkspace = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); +export const rootWorkspaceUri = getWorkspaceRoot(); + +export type PythonSettingKeys = 'workspaceSymbols.enabled' | 'pythonPath' | + 'linting.lintOnSave' | 'linting.lintOnTextChange' | + 'linting.enabled' | 'linting.pylintEnabled' | + 'linting.flake8Enabled' | 'linting.pep8Enabled' | 'linting.pylamaEnabled' | + 'linting.prospectorEnabled' | 'linting.pydocstyleEnabled' | 'linting.mypyEnabled' | + 'unitTest.nosetestArgs' | 'unitTest.pyTestArgs' | 'unitTest.unittestArgs' | + 'formatting.formatOnSave' | 'formatting.provider' | 'sortImports.args' | + 'unitTest.nosetestsEnabled' | 'unitTest.pyTestEnabled' | 'unitTest.unittestEnabled'; + +export async function updateSetting(setting: PythonSettingKeys, value: {}, resource: Uri, configTarget: ConfigurationTarget) { + const settings = workspace.getConfiguration('python', resource); + const currentValue = settings.inspect(setting); + if (currentValue !== undefined && ((configTarget === ConfigurationTarget.Global && currentValue.globalValue === value) || + (configTarget === ConfigurationTarget.Workspace && currentValue.workspaceValue === value) || + (configTarget === ConfigurationTarget.WorkspaceFolder && currentValue.workspaceFolderValue === value))) { + PythonSettings.dispose(); + return; + } + // tslint:disable-next-line:await-promise + await settings.update(setting, value, configTarget); + PythonSettings.dispose(); +} + +function getWorkspaceRoot() { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return Uri.file(path.join(__dirname, '..', '..', 'src', 'test')); + } + if (workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri; + } + const workspaceFolder = workspace.getWorkspaceFolder(Uri.file(fileInNonRootWorkspace)); + return workspaceFolder ? workspaceFolder.uri : workspace.workspaceFolders[0].uri; +} + +// tslint:disable-next-line:no-any +export function retryAsync(wrapped: Function, retryCount: number = 2) { + // tslint:disable-next-line:no-any + return async (...args: any[]) => { + return new Promise((resolve, reject) => { + // tslint:disable-next-line:no-any + const reasons: any[] = []; + + const makeCall = () => { + // tslint:disable-next-line:no-unsafe-any no-any + // tslint:disable-next-line:no-invalid-this + wrapped.call(this, ...args) + // tslint:disable-next-line:no-unsafe-any no-any + .then(resolve, (reason: any) => { + reasons.push(reason); + if (reasons.length >= retryCount) { + reject(reasons); + } else { + // If failed once, lets wait for some time before trying again. + // tslint:disable-next-line:no-string-based-set-timeout + setTimeout(makeCall, 500); + } + }); + }; + + makeCall(); + }); + }; +} + +async function setPythonPathInWorkspace(resource: string | Uri | undefined, config: ConfigurationTarget, pythonPath?: string) { + if (config === ConfigurationTarget.WorkspaceFolder && !IS_MULTI_ROOT_TEST) { + return; + } + const resourceUri = typeof resource === 'string' ? Uri.file(resource) : resource; + const settings = workspace.getConfiguration('python', resourceUri); + const value = settings.inspect('pythonPath'); + const prop: 'workspaceFolderValue' | 'workspaceValue' = config === ConfigurationTarget.Workspace ? 'workspaceValue' : 'workspaceFolderValue'; + if (value && value[prop] !== pythonPath) { + await settings.update('pythonPath', pythonPath, config); + PythonSettings.dispose(); + } +} +async function restoreGlobalPythonPathSetting(): Promise { + const pythonConfig = workspace.getConfiguration('python'); + const currentGlobalPythonPathSetting = pythonConfig.inspect('pythonPath').globalValue; + if (globalPythonPathSetting !== currentGlobalPythonPathSetting) { + await pythonConfig.update('pythonPath', undefined, true); + } + PythonSettings.dispose(); +} + +export async function deleteDirectory(dir: string) { + const exists = await fs.pathExists(dir); + if (exists) { + await fs.remove(dir); + } +} +export async function deleteFile(file: string) { + const exists = await fs.pathExists(file); + if (exists) { + await fs.remove(file); + } +} + +const globalPythonPathSetting = workspace.getConfiguration('python').inspect('pythonPath').globalValue; +export const clearPythonPathInWorkspaceFolder = async (resource: string | Uri) => retryAsync(setPythonPathInWorkspace)(resource, ConfigurationTarget.WorkspaceFolder); +export const setPythonPathInWorkspaceRoot = async (pythonPath: string) => retryAsync(setPythonPathInWorkspace)(undefined, ConfigurationTarget.Workspace, pythonPath); +export const resetGlobalPythonPathSetting = async () => retryAsync(restoreGlobalPythonPathSetting)(); diff --git a/src/test/common/common.test.ts b/src/test/common/common.test.ts index 3a53107f3c2d..0e72ac770b71 100644 --- a/src/test/common/common.test.ts +++ b/src/test/common/common.test.ts @@ -1,30 +1,23 @@ -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// - -// The module 'assert' provides assertion methods from node import * as assert from 'assert'; -import * as vscode from 'vscode'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import { initialize } from './../initialize'; -import { execPythonFile } from '../../client/common/utils'; import { EOL } from 'os'; +import * as vscode from 'vscode'; import { createDeferred } from '../../client/common/helpers'; +import { execPythonFile, getInterpreterDisplayName } from '../../client/common/utils'; +import { initialize } from './../initialize'; // Defines a Mocha test suite to group tests of similar kind together suite('ChildProc', () => { - setup(() => initialize()); + setup(initialize); + teardown(initialize); test('Standard Response', done => { - execPythonFile('python', ['-c', 'print(1)'], __dirname, false).then(data => { - assert.ok(data === '1' + EOL); + execPythonFile(undefined, 'python', ['-c', 'print(1)'], __dirname, false).then(data => { + assert.ok(data === `1${EOL}`); }).then(done).catch(done); }); test('Error Response', done => { + // tslint:disable-next-line:no-any const def = createDeferred(); - execPythonFile('python', ['-c', 'print(1'], __dirname, false).then(() => { + execPythonFile(undefined, 'python', ['-c', 'print(1'], __dirname, false).then(() => { def.reject('Should have failed'); }).catch(() => { def.resolve(); @@ -38,32 +31,45 @@ suite('ChildProc', () => { function handleOutput(data: string) { output.push(data); } - execPythonFile('python', ['-c', 'print(1)'], __dirname, false, handleOutput).then(() => { + execPythonFile(undefined, 'python', ['-c', 'print(1)'], __dirname, false, handleOutput).then(() => { assert.equal(output.length, 1, 'Ouput length incorrect'); - assert.equal(output[0], '1' + EOL, 'Ouput value incorrect'); + assert.equal(output[0], `1${EOL}`, 'Ouput value incorrect'); }).then(done).catch(done); }); - test('Stream Stdout with Threads', done => { + test('Stream Stdout (Unicode)', async () => { + const output: string[] = []; + function handleOutput(data: string) { + output.push(data); + } + await execPythonFile(undefined, 'python', ['-c', 'print(\'öä\')'], __dirname, false, handleOutput); + assert.equal(output.length, 1, 'Ouput length incorrect'); + assert.equal(output[0], `öä${EOL}`, 'Ouput value incorrect'); + }); + + test('Stream Stdout with Threads', function (done) { + // tslint:disable-next-line:no-invalid-this + this.timeout(6000); const output: string[] = []; function handleOutput(data: string) { output.push(data); } - execPythonFile('python', ['-c', 'import sys\nprint(1)\nsys.__stdout__.flush()\nimport time\ntime.sleep(5)\nprint(2)'], __dirname, false, handleOutput).then(() => { + execPythonFile(undefined, 'python', ['-c', 'import sys\nprint(1)\nsys.__stdout__.flush()\nimport time\ntime.sleep(5)\nprint(2)'], __dirname, false, handleOutput).then(() => { assert.equal(output.length, 2, 'Ouput length incorrect'); - assert.equal(output[0], '1' + EOL, 'First Ouput value incorrect'); - assert.equal(output[1], '2' + EOL, 'Second Ouput value incorrect'); + assert.equal(output[0], `1${EOL}`, 'First Ouput value incorrect'); + assert.equal(output[1], `2${EOL}`, 'Second Ouput value incorrect'); }).then(done).catch(done); }); test('Kill', done => { + // tslint:disable-next-line:no-any const def = createDeferred(); const output: string[] = []; function handleOutput(data: string) { output.push(data); } const cancellation = new vscode.CancellationTokenSource(); - execPythonFile('python', ['-c', 'import sys\nprint(1)\nsys.__stdout__.flush()\nimport time\ntime.sleep(5)\nprint(2)'], __dirname, false, handleOutput, cancellation.token).then(() => { + execPythonFile(undefined, 'python', ['-c', 'import sys\nprint(1)\nsys.__stdout__.flush()\nimport time\ntime.sleep(5)\nprint(2)'], __dirname, false, handleOutput, cancellation.token).then(() => { def.reject('Should not have completed'); }).catch(() => { def.resolve(); @@ -75,4 +81,10 @@ suite('ChildProc', () => { def.promise.then(done).catch(done); }); + + test('Get Python display name', async () => { + const displayName = await getInterpreterDisplayName('python'); + assert.equal(typeof displayName, 'string', 'Display name not returned'); + assert.notEqual(displayName.length, 0, 'Display name cannot be empty'); + }); }); diff --git a/src/test/common/configSettings.multiroot.test.ts b/src/test/common/configSettings.multiroot.test.ts new file mode 100644 index 000000000000..dc7ef9590885 --- /dev/null +++ b/src/test/common/configSettings.multiroot.test.ts @@ -0,0 +1,184 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; +import { clearPythonPathInWorkspaceFolder } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; + +const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); + +// tslint:disable-next-line:max-func-body-length +suite('Multiroot Config Settings', () => { + suiteSetup(async function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await clearPythonPathInWorkspaceFolder(Uri.file(path.join(multirootPath, 'workspace1'))); + await initialize(); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + await clearPythonPathInWorkspaceFolder(Uri.file(path.join(multirootPath, 'workspace1'))); + await initializeTest(); + }); + + async function enableDisableLinterSetting(resource: Uri, configTarget: ConfigurationTarget, setting: string, enabled: boolean | undefined): Promise { + const settings = workspace.getConfiguration('python.linting', resource); + const cfgValue = settings.inspect(setting); + if (configTarget === ConfigurationTarget.Workspace && cfgValue && cfgValue.workspaceValue === enabled) { + return; + } + if (configTarget === ConfigurationTarget.WorkspaceFolder && cfgValue && cfgValue.workspaceFolderValue === enabled) { + return; + } + await settings.update(setting, enabled, configTarget); + PythonSettings.dispose(); + } + + test('Workspace folder should inherit Python Path from workspace root', async () => { + const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); + let settings = workspace.getConfiguration('python', workspaceUri); + const pythonPath = `x${new Date().getTime()}`; + await settings.update('pythonPath', pythonPath, ConfigurationTarget.Workspace); + const value = settings.inspect('pythonPath'); + if (value && typeof value.workspaceFolderValue === 'string') { + await settings.update('pythonPath', undefined, ConfigurationTarget.WorkspaceFolder); + } + settings = workspace.getConfiguration('python', workspaceUri); + PythonSettings.dispose(); + const cfgSetting = PythonSettings.getInstance(workspaceUri); + assert.equal(cfgSetting.pythonPath, pythonPath, 'Python Path not inherited from workspace'); + }); + + test('Workspace folder should not inherit Python Path from workspace root', async () => { + const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); + const settings = workspace.getConfiguration('python', workspaceUri); + const pythonPath = `x${new Date().getTime()}`; + await settings.update('pythonPath', pythonPath, ConfigurationTarget.Workspace); + const privatePythonPath = `x${new Date().getTime()}`; + await settings.update('pythonPath', privatePythonPath, ConfigurationTarget.WorkspaceFolder); + + const cfgSetting = PythonSettings.getInstance(workspaceUri); + assert.equal(cfgSetting.pythonPath, privatePythonPath, 'Python Path for workspace folder is incorrect'); + }); + + test('Workspace folder should inherit Python Path from workspace root when opening a document', async () => { + const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); + const fileToOpen = path.join(multirootPath, 'workspace1', 'file.py'); + + const settings = workspace.getConfiguration('python', workspaceUri); + const pythonPath = `x${new Date().getTime()}`; + await settings.update('pythonPath', pythonPath, ConfigurationTarget.Workspace); + // Update workspace folder to something else so it gets refreshed. + await settings.update('pythonPath', `x${new Date().getTime()}`, ConfigurationTarget.WorkspaceFolder); + await settings.update('pythonPath', undefined, ConfigurationTarget.WorkspaceFolder); + + const document = await workspace.openTextDocument(fileToOpen); + const cfg = PythonSettings.getInstance(document.uri); + assert.equal(cfg.pythonPath, pythonPath, 'Python Path not inherited from workspace'); + }); + + test('Workspace folder should not inherit Python Path from workspace root when opening a document', async () => { + const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); + const fileToOpen = path.join(multirootPath, 'workspace1', 'file.py'); + + const settings = workspace.getConfiguration('python', workspaceUri); + const pythonPath = `x${new Date().getTime()}`; + await settings.update('pythonPath', pythonPath, ConfigurationTarget.Workspace); + const privatePythonPath = `x${new Date().getTime()}`; + await settings.update('pythonPath', privatePythonPath, ConfigurationTarget.WorkspaceFolder); + + const document = await workspace.openTextDocument(fileToOpen); + const cfg = PythonSettings.getInstance(document.uri); + assert.equal(cfg.pythonPath, privatePythonPath, 'Python Path for workspace folder is incorrect'); + }); + + test('Enabling/Disabling Pylint in root should be reflected in config settings', async () => { + const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', undefined); + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', true); + let settings = PythonSettings.getInstance(workspaceUri); + assert.equal(settings.linting.pylintEnabled, true, 'Pylint not enabled when it should be'); + + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', false); + settings = PythonSettings.getInstance(workspaceUri); + assert.equal(settings.linting.pylintEnabled, false, 'Pylint enabled when it should not be'); + }); + + test('Enabling/Disabling Pylint in root and workspace should be reflected in config settings', async () => { + const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); + + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', false); + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', true); + + let cfgSetting = PythonSettings.getInstance(workspaceUri); + assert.equal(cfgSetting.linting.pylintEnabled, false, 'Workspace folder pylint setting is true when it should not be'); + PythonSettings.dispose(); + + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', false); + + cfgSetting = PythonSettings.getInstance(workspaceUri); + assert.equal(cfgSetting.linting.pylintEnabled, true, 'Workspace folder pylint setting is false when it should not be'); + }); + + test('Enabling/Disabling Pylint in root should be reflected in config settings when opening a document', async () => { + const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); + const fileToOpen = path.join(multirootPath, 'workspace1', 'file.py'); + + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', false); + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); + let document = await workspace.openTextDocument(fileToOpen); + let cfg = PythonSettings.getInstance(document.uri); + assert.equal(cfg.linting.pylintEnabled, true, 'Pylint should be enabled in workspace'); + PythonSettings.dispose(); + + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', true); + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', false); + document = await workspace.openTextDocument(fileToOpen); + cfg = PythonSettings.getInstance(document.uri); + assert.equal(cfg.linting.pylintEnabled, false, 'Pylint should not be enabled in workspace'); + }); + + test('Enabling/Disabling Pylint in root should be reflected in config settings when opening a document', async () => { + const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); + const fileToOpen = path.join(multirootPath, 'workspace1', 'file.py'); + + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', false); + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); + let document = await workspace.openTextDocument(fileToOpen); + let cfg = PythonSettings.getInstance(document.uri); + assert.equal(cfg.linting.pylintEnabled, true, 'Pylint should be enabled in workspace'); + PythonSettings.dispose(); + + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', true); + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', false); + document = await workspace.openTextDocument(fileToOpen); + cfg = PythonSettings.getInstance(document.uri); + assert.equal(cfg.linting.pylintEnabled, false, 'Pylint should not be enabled in workspace'); + }); + + // tslint:disable-next-line:no-invalid-template-strings + test('${workspaceRoot} variable in settings should be replaced with the right value', async () => { + const workspace2Uri = Uri.file(path.join(multirootPath, 'workspace2')); + let fileToOpen = path.join(workspace2Uri.fsPath, 'file.py'); + + let document = await workspace.openTextDocument(fileToOpen); + let cfg = PythonSettings.getInstance(document.uri); + assert.equal(path.dirname(cfg.workspaceSymbols.tagFilePath), workspace2Uri.fsPath, 'ctags file path for workspace2 is incorrect'); + assert.equal(path.basename(cfg.workspaceSymbols.tagFilePath), 'workspace2.tags.file', 'ctags file name for workspace2 is incorrect'); + PythonSettings.dispose(); + + const workspace3Uri = Uri.file(path.join(multirootPath, 'workspace3')); + fileToOpen = path.join(workspace3Uri.fsPath, 'file.py'); + + document = await workspace.openTextDocument(fileToOpen); + cfg = PythonSettings.getInstance(document.uri); + assert.equal(path.dirname(cfg.workspaceSymbols.tagFilePath), workspace3Uri.fsPath, 'ctags file path for workspace3 is incorrect'); + assert.equal(path.basename(cfg.workspaceSymbols.tagFilePath), 'workspace3.tags.file', 'ctags file name for workspace3 is incorrect'); + PythonSettings.dispose(); + }); +}); diff --git a/src/test/common/configSettings.test.ts b/src/test/common/configSettings.test.ts index 8382b8f4e1c9..616c29d5afab 100644 --- a/src/test/common/configSettings.test.ts +++ b/src/test/common/configSettings.test.ts @@ -9,20 +9,23 @@ import * as assert from 'assert'; // You can import and use all API from the 'vscode' module // as well as import your extension to test it import * as vscode from 'vscode'; -import { initialize, IS_TRAVIS } from './../initialize'; +import * as path from 'path'; +import { initialize, IS_MULTI_ROOT_TEST, IS_TRAVIS } from './../initialize'; import { PythonSettings } from '../../client/common/configSettings'; import { SystemVariables } from '../../client/common/systemVariables'; +import { rootWorkspaceUri } from '../common'; -const pythonSettings = PythonSettings.getInstance(); +const workspaceRoot = path.join(__dirname, '..', '..', '..', 'src', 'test'); // Defines a Mocha test suite to group tests of similar kind together suite('Configuration Settings', () => { setup(() => initialize()); - - if (!IS_TRAVIS) { + + if (!IS_MULTI_ROOT_TEST) { test('Check Values', done => { - const systemVariables: SystemVariables = new SystemVariables(); + const systemVariables: SystemVariables = new SystemVariables(workspaceRoot); const pythonConfig = vscode.workspace.getConfiguration('python'); + const pythonSettings = PythonSettings.getInstance(vscode.Uri.file(workspaceRoot)); Object.keys(pythonSettings).forEach(key => { let settingValue = pythonConfig.get(key, 'Not a config'); if (settingValue === 'Not a config') { diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index 4f30f16e05bc..786ae7bb278e 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -1,8 +1,10 @@ import * as assert from 'assert'; -import { closeActiveWindows, IS_TRAVIS } from './../initialize'; -import { MockOutputChannel } from './../mockClasses'; -import { Installer, Product } from '../../client/common/installer'; +import * as path from 'path'; +import { Uri } from 'vscode'; import { EnumEx } from '../../client/common/enumUtils'; +import { Installer, Product } from '../../client/common/installer'; +import { closeActiveWindows, initializeTest, IS_MULTI_ROOT_TEST, IS_TRAVIS } from './../initialize'; +import { MockOutputChannel } from './../mockClasses'; // TODO: Need to mock the command runner, to check what commands are being sent. // Instead of altering the environment. @@ -10,22 +12,25 @@ import { EnumEx } from '../../client/common/enumUtils'; suite('Installer', () => { let outputChannel: MockOutputChannel; let installer: Installer; - - suiteSetup(() => { + const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); + const resource = IS_MULTI_ROOT_TEST ? workspaceUri : undefined; + suiteSetup(async () => { outputChannel = new MockOutputChannel('Installer'); installer = new Installer(outputChannel); + await initializeTest(); }); - suiteTeardown(() => closeActiveWindows()); - teardown(() => closeActiveWindows()); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); async function testUninstallingProduct(product: Product) { - const isInstalled = await installer.isInstalled(product); + let isInstalled = await installer.isInstalled(product, resource); if (isInstalled) { - await installer.uninstall(product); - const isInstalled = await installer.isInstalled(product); + await installer.uninstall(product, resource); + isInstalled = await installer.isInstalled(product, resource); // Someimtes installation doesn't work on Travis if (!IS_TRAVIS) { - assert.equal(isInstalled, false, `Product uninstall failed`); + assert.equal(isInstalled, false, 'Product uninstall failed'); } } } @@ -40,14 +45,14 @@ suite('Installer', () => { }); async function testInstallingProduct(product: Product) { - const isInstalled = await installer.isInstalled(product); + const isInstalled = await installer.isInstalled(product, resource); if (!isInstalled) { - await installer.install(product); + await installer.install(product, resource); } - const checkIsInstalledAgain = await installer.isInstalled(product); + const checkIsInstalledAgain = await installer.isInstalled(product, resource); // Someimtes installation doesn't work on Travis if (!IS_TRAVIS) { - assert.notEqual(checkIsInstalledAgain, false, `Product installation failed`); + assert.notEqual(checkIsInstalledAgain, false, 'Product installation failed'); } } EnumEx.getNamesAndValues(Product).forEach(prod => { diff --git a/src/test/definitions/hover.test.ts b/src/test/definitions/hover.test.ts index 194d325edab6..07f06b8843ca 100644 --- a/src/test/definitions/hover.test.ts +++ b/src/test/definitions/hover.test.ts @@ -9,7 +9,7 @@ import { EOL } from 'os'; // as well as import your extension to test it import * as vscode from 'vscode'; import * as path from 'path'; -import { initialize, closeActiveWindows } from '../initialize'; +import { initialize, closeActiveWindows, initializeTest } from '../initialize'; import { normalizeMarkedString } from '../textUtils'; const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); @@ -23,6 +23,7 @@ const fileStringFormat = path.join(hoverPath, 'stringFormat.py'); suite('Hover Definition', () => { suiteSetup(() => initialize()); + setup(() => initializeTest()); suiteTeardown(() => closeActiveWindows()); teardown(() => closeActiveWindows()); diff --git a/src/test/definitions/parallel.test.ts b/src/test/definitions/parallel.test.ts new file mode 100644 index 000000000000..f70edb4a9910 --- /dev/null +++ b/src/test/definitions/parallel.test.ts @@ -0,0 +1,40 @@ +import * as assert from 'assert'; +import { EOL } from 'os'; +import * as vscode from 'vscode'; +import * as path from 'path'; +import { initialize, closeActiveWindows } from '../initialize'; +import { normalizeMarkedString } from '../textUtils'; + +const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); +const fileOne = path.join(autoCompPath, 'one.py'); + +suite('Code, Hover Definition and Intellisense', () => { + suiteSetup(() => initialize()); + suiteTeardown(() => closeActiveWindows()); + teardown(() => closeActiveWindows()); + + test('All three together', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileOne); + const editor = await vscode.window.showTextDocument(textDocument); + + let position = new vscode.Position(30, 5); + const hoverDef = await vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + const codeDef = await vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); + position = new vscode.Position(3, 10); + const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + + assert.equal(list.items.filter(item => item.label === 'api_version').length, 1, 'api_version not found'); + + assert.equal(codeDef.length, 1, 'Definition length is incorrect'); + assert.equal(codeDef[0].uri.fsPath, fileOne, 'Incorrect file'); + assert.equal(`${codeDef[0].range.start.line},${codeDef[0].range.start.character}`, '17,4', 'Start position is incorrect'); + assert.equal(`${codeDef[0].range.end.line},${codeDef[0].range.end.character}`, '21,11', 'End position is incorrect'); + + assert.equal(hoverDef.length, 1, 'Definition length is incorrect'); + assert.equal(`${hoverDef[0].range.start.line},${hoverDef[0].range.start.character}`, '30,4', 'Start position is incorrect'); + assert.equal(`${hoverDef[0].range.end.line},${hoverDef[0].range.end.character}`, '30,11', 'End position is incorrect'); + assert.equal(hoverDef[0].contents.length, 1, 'Invalid content items'); + const expectedContent = '```python' + EOL + 'def method1()' + EOL + '```' + EOL + 'This is method1'; + assert.equal(normalizeMarkedString(hoverDef[0].contents[0]), expectedContent, 'function signature incorrect'); + }); +}); diff --git a/src/test/format/extension.format.test.ts b/src/test/format/extension.format.test.ts index 986d64e976a9..ce41edead475 100644 --- a/src/test/format/extension.format.test.ts +++ b/src/test/format/extension.format.test.ts @@ -1,3 +1,4 @@ +import { updateSetting } from '../common'; // Note: This example test is leveraging the Mocha test framework. // Please refer to their documentation on https://mochajs.org/ for help. @@ -10,16 +11,14 @@ import * as assert from 'assert'; // as well as import your extension to test it import * as vscode from 'vscode'; import * as path from 'path'; -import * as settings from '../../client/common/configSettings'; import * as fs from 'fs-extra'; import { EOL } from 'os'; +import { PythonSettings } from '../../client/common/configSettings'; import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter'; -import { initialize, IS_TRAVIS, closeActiveWindows } from '../initialize'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST, IS_TRAVIS } from '../initialize'; import { YapfFormatter } from '../../client/formatters/yapfFormatter'; import { execPythonFile } from '../../client/common/utils'; -const pythonSettings = settings.PythonSettings.getInstance(); - const ch = vscode.window.createOutputChannel('Tests'); const pythoFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); const workspaceRootPath = path.join(__dirname, '..', '..', '..', 'src', 'test'); @@ -30,6 +29,8 @@ const autoPep8FileToAutoFormat = path.join(pythoFilesPath, 'autoPep8FileToAutoFo const yapfFileToFormat = path.join(pythoFilesPath, 'yapfFileToFormat.py'); const yapfFileToAutoFormat = path.join(pythoFilesPath, 'yapfFileToAutoFormat.py'); +const configUpdateTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; + let formattedYapf = ''; let formattedAutoPep8 = ''; @@ -40,20 +41,22 @@ suite('Formatting', () => { fs.copySync(originalUnformattedFile, file, { overwrite: true }); }); fs.ensureDirSync(path.dirname(autoPep8FileToFormat)); - const yapf = execPythonFile('yapf', [originalUnformattedFile], workspaceRootPath, false); - const autoPep8 = execPythonFile('autopep8', [originalUnformattedFile], workspaceRootPath, false); + const yapf = execPythonFile(workspaceRootPath, 'yapf', [originalUnformattedFile], workspaceRootPath, false); + const autoPep8 = execPythonFile(workspaceRootPath, 'autopep8', [originalUnformattedFile], workspaceRootPath, false); await Promise.all([yapf, autoPep8]).then(formattedResults => { formattedYapf = formattedResults[0]; formattedAutoPep8 = formattedResults[1]; }).then(() => { }); }); - suiteTeardown(() => { + setup(() => initializeTest()); + suiteTeardown(async () => { [autoPep8FileToFormat, autoPep8FileToAutoFormat, yapfFileToFormat, yapfFileToAutoFormat].forEach(file => { if (fs.existsSync(file)) { fs.unlinkSync(file); } }); - return closeActiveWindows(); + await updateSetting('formatting.formatOnSave', false, vscode.Uri.file(pythoFilesPath), configUpdateTarget) + await closeActiveWindows(); }); teardown(() => closeActiveWindows()); @@ -85,30 +88,23 @@ suite('Formatting', () => { testFormatting(new YapfFormatter(ch), formattedYapf, yapfFileToFormat).then(done, done); }); - function testAutoFormatting(formatter: string, formattedContents: string, fileToFormat: string): PromiseLike { - let textDocument: vscode.TextDocument; - pythonSettings.formatting.formatOnSave = true; - pythonSettings.formatting.provider = formatter; - return vscode.workspace.openTextDocument(fileToFormat).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - return editor.edit(editBuilder => { - editBuilder.insert(new vscode.Position(0, 0), '#\n'); - }); - }).then(edited => { - return textDocument.save(); - }).then(saved => { - return new Promise((resolve, reject) => { - setTimeout(() => { - resolve(); - }, 5000); - }); - }).then(() => { - const text = textDocument.getText(); - assert.equal(text === formattedContents, true, 'Formatted contents are not the same'); + async function testAutoFormatting(formatter: string, formattedContents: string, fileToFormat: string): Promise { + await updateSetting('formatting.formatOnSave', true, vscode.Uri.file(fileToFormat), configUpdateTarget); + await updateSetting('formatting.provider', formatter, vscode.Uri.file(fileToFormat), configUpdateTarget); + const textDocument = await vscode.workspace.openTextDocument(fileToFormat); + const editor = await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + const edited = await editor.edit(editBuilder => { + editBuilder.insert(new vscode.Position(0, 0), '#\n'); + }); + const saved = await textDocument.save(); + await new Promise((resolve, reject) => { + setTimeout(() => { + resolve(); + }, 5000); }); + const text = textDocument.getText(); + assert.equal(text === formattedContents, true, 'Formatted contents are not the same'); } test('AutoPep8 autoformat on save', done => { testAutoFormatting('autopep8', `#${EOL}` + formattedAutoPep8, autoPep8FileToAutoFormat).then(done, done); diff --git a/src/test/format/extension.onTypeFormat.test.ts b/src/test/format/extension.onTypeFormat.test.ts index 4ede98a39bac..097922e9010e 100644 --- a/src/test/format/extension.onTypeFormat.test.ts +++ b/src/test/format/extension.onTypeFormat.test.ts @@ -11,7 +11,7 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs-extra'; -import { initialize, closeActiveWindows } from '../initialize'; +import { initialize, closeActiveWindows, initializeTest } from '../initialize'; import { BlockFormatProviders } from '../../client/typeFormatters/blockFormatProvider'; const srcPythoFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'typeFormatFiles'); @@ -676,6 +676,7 @@ suite('Else blocks with indentation of Tab', () => { fs.copySync(path.join(srcPythoFilesPath, file), targetFile); }); }); + setup(() => initializeTest()); suiteTeardown(() => closeActiveWindows()); teardown(() => closeActiveWindows()); diff --git a/src/test/format/extension.sort.test.ts b/src/test/format/extension.sort.test.ts index 15b1f0898e57..c8c2f1c6ffc3 100644 --- a/src/test/format/extension.sort.test.ts +++ b/src/test/format/extension.sort.test.ts @@ -1,22 +1,11 @@ - -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. - - -// The module 'assert' provides assertion methods from node import * as assert from 'assert'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from 'vscode'; -import * as path from 'path'; -import * as settings from '../../client/common/configSettings'; import * as fs from 'fs'; import { EOL } from 'os'; +import * as path from 'path'; +import { commands, ConfigurationTarget, Position, Range, Uri, window, workspace } from 'vscode'; import { PythonImportSortProvider } from '../../client/providers/importSortProvider'; -import { initialize, IS_TRAVIS, closeActiveWindows } from '../initialize'; - -const pythonSettings = settings.PythonSettings.getInstance(); +import { updateSetting } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; const sortingPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'sorting'); const fileToFormatWithoutConfig = path.join(sortingPath, 'noconfig', 'before.py'); @@ -27,138 +16,82 @@ const fileToFormatWithConfig1 = path.join(sortingPath, 'withconfig', 'before.1.p const originalFileToFormatWithConfig1 = path.join(sortingPath, 'withconfig', 'original.1.py'); const extensionDir = path.join(__dirname, '..', '..', '..'); +// tslint:disable-next-line:max-func-body-length suite('Sorting', () => { - suiteSetup(() => initialize()); - suiteTeardown(() => { + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + suiteSetup(initialize); + setup(initializeTest); + suiteTeardown(async () => { fs.writeFileSync(fileToFormatWithConfig, fs.readFileSync(originalFileToFormatWithConfig)); fs.writeFileSync(fileToFormatWithConfig1, fs.readFileSync(originalFileToFormatWithConfig1)); fs.writeFileSync(fileToFormatWithoutConfig, fs.readFileSync(originalFileToFormatWithoutConfig)); - return closeActiveWindows(); + await updateSetting('sortImports.args', [], Uri.file(sortingPath), configTarget); + await closeActiveWindows(); }); - setup(() => { - pythonSettings.sortImports.args = []; + setup(async () => { fs.writeFileSync(fileToFormatWithConfig, fs.readFileSync(originalFileToFormatWithConfig)); fs.writeFileSync(fileToFormatWithoutConfig, fs.readFileSync(originalFileToFormatWithoutConfig)); fs.writeFileSync(fileToFormatWithConfig1, fs.readFileSync(originalFileToFormatWithConfig1)); - return closeActiveWindows(); + await updateSetting('sortImports.args', [], Uri.file(sortingPath), configTarget); + await closeActiveWindows(); }); - test('Without Config', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileToFormatWithoutConfig).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - textEditor = editor; - assert(vscode.window.activeTextEditor, 'No active editor'); - const sorter = new PythonImportSortProvider(); - return sorter.sortImports(extensionDir, textDocument); - }).then(edits => { - assert.equal(edits.filter(value => value.newText === EOL && value.range.isEqual(new vscode.Range(2, 0, 2, 0))).length, 1, 'EOL not found'); - assert.equal(edits.filter(value => value.newText === '' && value.range.isEqual(new vscode.Range(3, 0, 4, 0))).length, 1, '"" not found'); - assert.equal(edits.filter(value => value.newText === `from rope.base import libutils${EOL}from rope.refactor.extract import ExtractMethod, ExtractVariable${EOL}from rope.refactor.rename import Rename${EOL}` && value.range.isEqual(new vscode.Range(6, 0, 6, 0))).length, 1, 'Text not found'); - assert.equal(edits.filter(value => value.newText === '' && value.range.isEqual(new vscode.Range(13, 0, 18, 0))).length, 1, '"" not found'); - }).then(done, done); + test('Without Config', async () => { + const textDocument = await workspace.openTextDocument(fileToFormatWithoutConfig); + await window.showTextDocument(textDocument); + const sorter = new PythonImportSortProvider(); + const edits = await sorter.sortImports(extensionDir, textDocument); + assert.equal(edits.filter(value => value.newText === EOL && value.range.isEqual(new Range(2, 0, 2, 0))).length, 1, 'EOL not found'); + assert.equal(edits.filter(value => value.newText === '' && value.range.isEqual(new Range(3, 0, 4, 0))).length, 1, '"" not found'); + assert.equal(edits.filter(value => value.newText === `from rope.base import libutils${EOL}from rope.refactor.extract import ExtractMethod, ExtractVariable${EOL}from rope.refactor.rename import Rename${EOL}` && value.range.isEqual(new Range(6, 0, 6, 0))).length, 1, 'Text not found'); + assert.equal(edits.filter(value => value.newText === '' && value.range.isEqual(new Range(13, 0, 18, 0))).length, 1, '"" not found'); }); - test('Without Config (via Command)', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - let originalContent = ''; - vscode.workspace.openTextDocument(fileToFormatWithoutConfig).then(document => { - textDocument = document; - originalContent = textDocument.getText(); - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - return vscode.commands.executeCommand('python.sortImports'); - }).then(() => { - assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); - }).then(done, done); + test('Without Config (via Command)', async () => { + const textDocument = await workspace.openTextDocument(fileToFormatWithoutConfig); + const originalContent = textDocument.getText(); + await window.showTextDocument(textDocument); + await commands.executeCommand('python.sortImports'); + assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); }); - test('With Config', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileToFormatWithConfig).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const sorter = new PythonImportSortProvider(); - return sorter.sortImports(extensionDir, textDocument); - }).then(edits => { - const newValue = `from third_party import lib2${EOL}from third_party import lib3${EOL}from third_party import lib4${EOL}from third_party import lib5${EOL}from third_party import lib6${EOL}from third_party import lib7${EOL}from third_party import lib8${EOL}from third_party import lib9${EOL}`; - assert.equal(edits.filter(value => value.newText === newValue && value.range.isEqual(new vscode.Range(0, 0, 3, 0))).length, 1, 'New Text not found'); - }).then(done, done); + test('With Config', async () => { + const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); + await window.showTextDocument(textDocument); + const sorter = new PythonImportSortProvider(); + const edits = await sorter.sortImports(extensionDir, textDocument); + const newValue = `from third_party import lib2${EOL}from third_party import lib3${EOL}from third_party import lib4${EOL}from third_party import lib5${EOL}from third_party import lib6${EOL}from third_party import lib7${EOL}from third_party import lib8${EOL}from third_party import lib9${EOL}`; + assert.equal(edits.filter(value => value.newText === newValue && value.range.isEqual(new Range(0, 0, 3, 0))).length, 1, 'New Text not found'); }); - test('With Config (via Command)', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - let originalContent = ''; - vscode.workspace.openTextDocument(fileToFormatWithConfig).then(document => { - textDocument = document; - originalContent = document.getText(); - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - return vscode.commands.executeCommand('python.sortImports'); - }).then(() => { - assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); - }).then(done, done); + test('With Config (via Command)', async () => { + const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); + const originalContent = textDocument.getText(); + await window.showTextDocument(textDocument); + await commands.executeCommand('python.sortImports'); + assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); }); - // Doesn't always work on Travis !?! - if (!IS_TRAVIS) { - test('With Changes and Config in Args', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - pythonSettings.sortImports.args = ['-sp', path.join(sortingPath, 'withconfig')]; - vscode.workspace.openTextDocument(fileToFormatWithConfig).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - return editor.edit(editor => { - editor.insert(new vscode.Position(0, 0), 'from third_party import lib0' + EOL); - }); - }).then(() => { - const sorter = new PythonImportSortProvider(); - return sorter.sortImports(extensionDir, textDocument); - }).then(edits => { - const newValue = `from third_party import lib2${EOL}from third_party import lib3${EOL}from third_party import lib4${EOL}from third_party import lib5${EOL}from third_party import lib6${EOL}from third_party import lib7${EOL}from third_party import lib8${EOL}from third_party import lib9${EOL}`; - assert.equal(edits.length, 1, 'Incorrect number of edits'); - assert.equal(edits[0].newText, newValue, 'New Value is not the same'); - assert.equal(`${edits[0].range.start.line},${edits[0].range.start.character}`, '1,0', 'Start position is not the same'); - assert.equal(`${edits[0].range.end.line},${edits[0].range.end.character}`, '4,0', 'End position is not the same'); - }).then(done, done); + test('With Changes and Config in Args', async () => { + await updateSetting('sortImports.args', ['-sp', path.join(sortingPath, 'withconfig')], Uri.file(sortingPath), ConfigurationTarget.Workspace); + const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); + const editor = await window.showTextDocument(textDocument); + await editor.edit(builder => { + builder.insert(new Position(0, 0), `from third_party import lib0${EOL}`); + }); + const sorter = new PythonImportSortProvider(); + const edits = await sorter.sortImports(extensionDir, textDocument); + assert.notEqual(edits.length, 0, 'No edits'); + }); + test('With Changes and Config in Args (via Command)', async () => { + await updateSetting('sortImports.args', ['-sp', path.join(sortingPath, 'withconfig')], Uri.file(sortingPath), configTarget); + const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); + const editor = await window.showTextDocument(textDocument); + await editor.edit(builder => { + builder.insert(new Position(0, 0), `from third_party import lib0${EOL}`); }); - } - test('With Changes and Config in Args (via Command)', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - let originalContent = ''; - pythonSettings.sortImports.args = ['-sp', path.join(sortingPath, 'withconfig')]; - vscode.workspace.openTextDocument(fileToFormatWithConfig).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - return editor.edit(editor => { - editor.insert(new vscode.Position(0, 0), 'from third_party import lib0' + EOL); - }); - }).then(() => { - originalContent = textDocument.getText(); - return vscode.commands.executeCommand('python.sortImports'); - }).then(edits => { - assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); - }).then(done, done); + const originalContent = textDocument.getText(); + await commands.executeCommand('python.sortImports'); + assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); }); }); diff --git a/src/test/index.ts b/src/test/index.ts index af916a981db9..f14ba247bb92 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1,26 +1,13 @@ -import { initializePython } from './initialize'; -// -// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING -// -// This file is providing the test runner to use when running extension tests. -// By default the test runner in use is Mocha based. -// -// You can provide your own test runner if you want to override it by exporting -// a function run(testRoot: string, clb: (error:Error) => void) that the extension -// host can call to run the tests. The test runner is expected to use console.log -// to report the results back to the caller. When the tests are finished, return -// a possible error to the callback or null if none. +import * as testRunner from 'vscode/lib/testrunner'; +import { initializePython, IS_MULTI_ROOT_TEST } from './initialize'; +process.env.VSC_PYTHON_CI_TEST = '1'; -let testRunner = require('vscode/lib/testrunner'); - -// You can directly control Mocha options by uncommenting the following lines -// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info +// You can directly control Mocha options by uncommenting the following lines. +// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info. testRunner.configure({ - ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) - useColors: true, // colored output from test results - timeout: 25000 + ui: 'tdd', + useColors: true, + timeout: 25000, + retries: 3 }); - -initializePython(); - module.exports = testRunner; diff --git a/src/test/initialize.ts b/src/test/initialize.ts index 1dcd6e247fd4..67193c2da0bd 100644 --- a/src/test/initialize.ts +++ b/src/test/initialize.ts @@ -1,34 +1,58 @@ -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { PythonSettings } from '../client/common/configSettings'; +import { activated } from '../client/extension'; +import { clearPythonPathInWorkspaceFolder, resetGlobalPythonPathSetting, setPythonPathInWorkspaceRoot } from './common'; -//First thing to be executed -process.env['PYTHON_DONJAYAMANNE_TEST'] = "1"; +const dummyPythonFile = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); +const multirootPath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc'); +const workspace3Uri = vscode.Uri.file(path.join(multirootPath, 'workspace3')); -// The module 'assert' provides assertion methods from node -import * as assert from "assert"; -import * as fs from 'fs'; +//First thing to be executed. +// tslint:disable-next-line:no-string-literal +process.env['VSC_PYTHON_CI_TEST'] = '1'; -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from "vscode"; -import * as path from "path"; -let dummyPythonFile = path.join(__dirname, "..", "..", "src", "test", "pythonFiles", "dummy.py"); +const PYTHON_PATH = getPythonPath(); +// tslint:disable-next-line:no-string-literal prefer-template +export const IS_TRAVIS = (process.env['TRAVIS'] + '') === 'true'; +export const TEST_TIMEOUT = 25000; +export const IS_MULTI_ROOT_TEST = isMultitrootTest(); -export function initialize(): Promise { - // Opening a python file activates the extension - return new Promise((resolve, reject) => { - vscode.workspace.openTextDocument(dummyPythonFile).then(() => resolve(), reject); - }); +// Ability to use custom python environments for testing +export async function initializePython() { + await resetGlobalPythonPathSetting(); + await clearPythonPathInWorkspaceFolder(dummyPythonFile); + await clearPythonPathInWorkspaceFolder(workspace3Uri); + await setPythonPathInWorkspaceRoot(PYTHON_PATH); +} + +// tslint:disable-next-line:no-any +export async function initialize(): Promise { + await initializePython(); + // Opening a python file activates the extension. + await vscode.workspace.openTextDocument(dummyPythonFile); + await activated; + // Dispose any cached python settings (used only in test env). + PythonSettings.dispose(); +} +// tslint:disable-next-line:no-any +export async function initializeTest(): Promise { + await initializePython(); + await closeActiveWindows(); + // Dispose any cached python settings (used only in test env). + PythonSettings.dispose(); } export async function wait(timeoutMilliseconds: number) { return new Promise(resolve => { + // tslint:disable-next-line:no-string-based-set-timeout setTimeout(resolve, timeoutMilliseconds); }); } +// tslint:disable-next-line:no-any export async function closeActiveWindows(): Promise { // https://github.com/Microsoft/vscode/blob/master/extensions/vscode-api-tests/src/utils.ts return new Promise(resolve => { @@ -53,6 +77,7 @@ export async function closeActiveWindows(): Promise { return resolve(); } vscode.commands.executeCommand('workbench.action.closeAllEditors') + // tslint:disable-next-line:no-any .then(() => null, (err: any) => { clearInterval(interval); resolve(); @@ -64,28 +89,15 @@ export async function closeActiveWindows(): Promise { }); } -export const IS_TRAVIS = (process.env['TRAVIS'] + '') === 'true'; -export const TEST_TIMEOUT = 25000; - function getPythonPath(): string { - const pythonPaths = ['/home/travis/virtualenv/python3.5.2/bin/python', - '/xUsers/travis/.pyenv/versions/3.5.1/envs/MYVERSION/bin/python', - '/xUsers/donjayamanne/Projects/PythonEnvs/p361/bin/python', - 'cC:/Users/dojayama/nine/python.exe', - 'C:/Development/PythonEnvs/p27/scripts/python.exe', - '/Users/donjayamanne/Projects/PythonEnvs/p27/bin/python']; - for (let counter = 0; counter < pythonPaths.length; counter++) { - if (fs.existsSync(pythonPaths[counter])) { - return pythonPaths[counter]; - } + // tslint:disable-next-line:no-unsafe-any + if (process.env.TRAVIS_PYTHON_PATH && fs.existsSync(process.env.TRAVIS_PYTHON_PATH)) { + // tslint:disable-next-line:no-unsafe-any + return process.env.TRAVIS_PYTHON_PATH; } return 'python'; } -const PYTHON_PATH = getPythonPath(); - -// Ability to use custom python environments for testing -export function initializePython() { - const pythonConfig = vscode.workspace.getConfiguration('python'); - pythonConfig.update('pythonPath', PYTHON_PATH); +function isMultitrootTest() { + return Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 1; } diff --git a/src/test/interpreters/condaEnvFileService.test.ts b/src/test/interpreters/condaEnvFileService.test.ts index 9778179149c8..26bd45c9928d 100644 --- a/src/test/interpreters/condaEnvFileService.test.ts +++ b/src/test/interpreters/condaEnvFileService.test.ts @@ -2,7 +2,7 @@ import * as assert from 'assert'; import * as path from 'path'; import * as fs from 'fs-extra'; import { EOL } from 'os'; -import { initialize } from '../initialize'; +import { initialize, initializeTest } from '../initialize'; import { IS_WINDOWS } from '../../client/common/utils'; import { MockInterpreterVersionProvider } from './mocks'; import { CondaEnvFileService } from '../../client/interpreter/locators/services/condaEnvFileService'; @@ -18,6 +18,7 @@ const environmentsFilePath = path.join(environmentsPath, 'environments.txt'); suite('Interpreters from Conda Environments Text File', () => { suiteSetup(() => initialize()); + setup(() => initializeTest()); suiteTeardown(async () => { // Clear the file so we don't get unwanted changes prompting for a checkin of this file await updateEnvWithInterpreters([]); diff --git a/src/test/interpreters/condaEnvService.test.ts b/src/test/interpreters/condaEnvService.test.ts index 0441b93cd7f4..59f9d44c78c6 100644 --- a/src/test/interpreters/condaEnvService.test.ts +++ b/src/test/interpreters/condaEnvService.test.ts @@ -1,29 +1,25 @@ import * as assert from 'assert'; import * as path from 'path'; -import * as settings from '../../client/common/configSettings'; -import { initialize } from '../initialize'; +import { Uri } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; import { IS_WINDOWS } from '../../client/common/utils'; -import { CondaEnvService } from '../../client/interpreter/locators/services/condaEnvService'; +import { PythonInterpreter } from '../../client/interpreter/contracts'; import { AnacondaCompanyName } from '../../client/interpreter/locators/services/conda'; +import { CondaEnvService } from '../../client/interpreter/locators/services/condaEnvService'; +import { initialize, initializeTest } from '../initialize'; import { MockProvider } from './mocks'; -import { PythonInterpreter } from '../../client/interpreter/contracts'; -const pythonSettings = settings.PythonSettings.getInstance(); const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); -let originalPythonPath; +const fileInNonRootWorkspace = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); +// tslint:disable-next-line:max-func-body-length suite('Interpreters from Conda Environments', () => { - suiteSetup(() => { - originalPythonPath = pythonSettings.pythonPath; - return initialize(); - }); - teardown(() => { - pythonSettings.pythonPath = originalPythonPath; - }); - + suiteSetup(initialize); + setup(initializeTest); test('Must return an empty list for empty json', async () => { const condaProvider = new CondaEnvService(); - const interpreters = await condaProvider.parseCondaInfo({} as any) + // tslint:disable-next-line:no-any prefer-type-cast + const interpreters = await condaProvider.parseCondaInfo({} as any); assert.equal(interpreters.length, 0, 'Incorrect number of entries'); }); test('Must extract display name from version info', async () => { @@ -113,12 +109,12 @@ suite('Interpreters from Conda Environments', () => { test('Must detect conda environments from a list', async () => { const registryInterpreters: PythonInterpreter[] = [ { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1' }, - { displayName: 'Two', path: pythonSettings.pythonPath, companyDisplayName: 'Two 2' }, + { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2' }, { displayName: 'Three', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'Three 3' }, { displayName: 'Anaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3' }, { displayName: 'xAnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3' }, { displayName: 'xnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'xContinuum Analytics, Inc.' }, - { displayName: 'xnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Continuum Analytics, Inc.' }, + { displayName: 'xnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Continuum Analytics, Inc.' } ]; const mockRegistryProvider = new MockProvider(registryInterpreters); const condaProvider = new CondaEnvService(mockRegistryProvider); @@ -126,39 +122,45 @@ suite('Interpreters from Conda Environments', () => { assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[0]), false, '1. Identified environment incorrectly'); assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[1]), false, '2. Identified environment incorrectly'); assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[2]), false, '3. Identified environment incorrectly'); - assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[3]), true, `4. Failed to identify conda environment when displayName starts with 'Anaconda'`); - assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[4]), true, `5. Failed to identify conda environment when displayName contains text 'Anaconda'`); - assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[5]), true, `6. Failed to identify conda environment when comanyDisplayName contains 'Continuum'`); - assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[6]), true, `7. Failed to identify conda environment when companyDisplayName starts with 'Continuum'`); + assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[3]), true, '4. Failed to identify conda environment when displayName starts with \'Anaconda\''); + assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[4]), true, '5. Failed to identify conda environment when displayName contains text \'Anaconda\''); + assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[5]), true, '6. Failed to identify conda environment when comanyDisplayName contains \'Continuum\''); + assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[6]), true, '7. Failed to identify conda environment when companyDisplayName starts with \'Continuum\''); }); test('Correctly identifies latest version when major version is different', async () => { const registryInterpreters: PythonInterpreter[] = [ { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1' }, - { displayName: 'Two', path: pythonSettings.pythonPath, companyDisplayName: 'Two 2', version: '3.1.3' }, + { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2', version: '3.1.3' }, { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1' }, + // tslint:disable-next-line:no-any { displayName: 'Four', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'Three 3', version: null }, + // tslint:disable-next-line:no-any { displayName: 'Five', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Three 3', version: undefined }, { displayName: 'Six', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'xContinuum Analytics, Inc.', version: '2' }, - { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' }, + { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' } ]; const mockRegistryProvider = new MockProvider(registryInterpreters); const condaProvider = new CondaEnvService(mockRegistryProvider); + // tslint:disable-next-line:no-non-null-assertion assert.equal(condaProvider.getLatestVersion(registryInterpreters)!.displayName, 'Two', 'Failed to identify latest version'); }); test('Correctly identifies latest version when major version is same', async () => { const registryInterpreters: PythonInterpreter[] = [ { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1' }, - { displayName: 'Two', path: pythonSettings.pythonPath, companyDisplayName: 'Two 2', version: '2.11.3' }, + { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2', version: '2.11.3' }, { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1' }, + // tslint:disable-next-line:no-any { displayName: 'Four', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'Three 3', version: null }, + // tslint:disable-next-line:no-any { displayName: 'Five', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Three 3', version: undefined }, { displayName: 'Six', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'xContinuum Analytics, Inc.', version: '2' }, - { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' }, + { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' } ]; const mockRegistryProvider = new MockProvider(registryInterpreters); const condaProvider = new CondaEnvService(mockRegistryProvider); + // tslint:disable-next-line:no-non-null-assertion assert.equal(condaProvider.getLatestVersion(registryInterpreters)!.displayName, 'Two', 'Failed to identify latest version'); }); test('Must use Conda env from Registry to locate conda.exe', async () => { diff --git a/src/test/interpreters/display.multiroot.test.ts b/src/test/interpreters/display.multiroot.test.ts new file mode 100644 index 000000000000..c2710638b23e --- /dev/null +++ b/src/test/interpreters/display.multiroot.test.ts @@ -0,0 +1,52 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { ConfigurationTarget, Uri, window, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; +import { InterpreterDisplay } from '../../client/interpreter/display'; +import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; +import { clearPythonPathInWorkspaceFolder } from '../common'; +import { closeActiveWindows, initialize, initializePython, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { MockStatusBarItem } from '../mockClasses'; +import { MockInterpreterVersionProvider } from './mocks'; +import { MockProvider } from './mocks'; + +const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); +const workspace3Uri = Uri.file(path.join(multirootPath, 'workspace3')); +const fileToOpen = path.join(workspace3Uri.fsPath, 'file.py'); + +// tslint:disable-next-line:max-func-body-length +suite('Multiroot Interpreters Display', () => { + suiteSetup(async function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); + setup(initializeTest); + suiteTeardown(initializePython); + teardown(async () => { + await clearPythonPathInWorkspaceFolder(fileToOpen); + await initialize(); + await closeActiveWindows(); + }); + + test('Must get display name from workspace folder interpreter and not from interpreter in workspace', async () => { + const settings = workspace.getConfiguration('python', Uri.file(fileToOpen)); + const pythonPath = fileToOpen; + await settings.update('pythonPath', pythonPath, ConfigurationTarget.WorkspaceFolder); + PythonSettings.dispose(); + + const document = await workspace.openTextDocument(fileToOpen); + await window.showTextDocument(document); + + const statusBar = new MockStatusBarItem(); + const provider = new MockProvider([]); + const displayName = `${path.basename(pythonPath)} [Environment]`; + const displayNameProvider = new MockInterpreterVersionProvider(displayName); + const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); + await display.refresh(); + + assert.equal(statusBar.text, displayName, 'Incorrect display name'); + }); +}); diff --git a/src/test/interpreters/display.test.ts b/src/test/interpreters/display.test.ts index 2f38d272125a..0cf3271124a3 100644 --- a/src/test/interpreters/display.test.ts +++ b/src/test/interpreters/display.test.ts @@ -1,32 +1,39 @@ import * as assert from 'assert'; import * as child_process from 'child_process'; -import * as settings from '../../client/common/configSettings'; +import { EOL } from 'os'; import * as path from 'path'; -import * as utils from '../../client/common/utils'; -import { initialize } from '../initialize'; +import { ConfigurationTarget, Uri, window, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; +import { InterpreterDisplay } from '../../client/interpreter/display'; +import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; +import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; +import { clearPythonPathInWorkspaceFolder, rootWorkspaceUri, updateSetting } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; import { MockStatusBarItem } from '../mockClasses'; import { MockInterpreterVersionProvider } from './mocks'; -import { InterpreterDisplay } from '../../client/interpreter/display'; import { MockProvider, MockVirtualEnv } from './mocks'; -import { EOL } from 'os'; -import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; -import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; -let pythonSettings = settings.PythonSettings.getInstance(); -let originalPythonPath; +const fileInNonRootWorkspace = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); +// tslint:disable-next-line:max-func-body-length suite('Interpreters Display', () => { - suiteSetup(() => { - originalPythonPath = pythonSettings.pythonPath; - return initialize(); + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + suiteSetup(initialize); + setup(async () => { + await initializeTest(); + if (IS_MULTI_ROOT_TEST) { + await initializeMultiRoot(); + } }); - teardown(() => { - pythonSettings.pythonPath = originalPythonPath; + teardown(async () => { + await clearPythonPathInWorkspaceFolder(fileInNonRootWorkspace); + await initialize(); + await closeActiveWindows(); }); - test('Must have command name', () => { const statusBar = new MockStatusBarItem(); const displayNameProvider = new MockInterpreterVersionProvider(''); + // tslint:disable-next-line:no-unused-expression new InterpreterDisplay(statusBar, new MockProvider([]), new VirtualEnvironmentManager([]), displayNameProvider); assert.equal(statusBar.command, 'python.setInterpreter', 'Incorrect command name'); }); @@ -52,14 +59,15 @@ suite('Interpreters Display', () => { await display.refresh(); assert.equal(statusBar.text, `${displayName} (${env2.name})`, 'Incorrect display name'); }); - test(`Must display default 'Display name' for unknown interpreter`, async () => { + test('Must display default \'Display name\' for unknown interpreter', async () => { const statusBar = new MockStatusBarItem(); const provider = new MockProvider([]); const displayName = 'Mock Display Name'; const displayNameProvider = new MockInterpreterVersionProvider(displayName, true); const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); // Change interpreter to an invalid value - const pythonPath = pythonSettings.pythonPath = 'c:/some/unknonw/Python Interpreter.exe'; + const pythonPath = 'UnknownInterpreter'; + await updateSetting('pythonPath', pythonPath, rootWorkspaceUri, configTarget); await display.refresh(); const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; @@ -67,15 +75,15 @@ suite('Interpreters Display', () => { }); test('Must get display name from a list of interpreters', async () => { const pythonPath = await new Promise(resolve => { - child_process.execFile(pythonSettings.pythonPath, ["-c", "import sys;print(sys.executable)"], (_, stdout) => { + child_process.execFile(PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { resolve(getFirstNonEmptyLineFromMultilineString(stdout)); }); - }).then(value => value.length === 0 ? pythonSettings.pythonPath : value); + }).then(value => value.length === 0 ? PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath : value); const statusBar = new MockStatusBarItem(); const interpreters = [ { displayName: 'One', path: 'c:/path1/one.exe', type: 'One 1' }, { displayName: 'Two', path: pythonPath, type: 'Two 2' }, - { displayName: 'Three', path: 'c:/path3/three.exe', type: 'Three 3' }, + { displayName: 'Three', path: 'c:/path3/three.exe', type: 'Three 3' } ]; const provider = new MockProvider(interpreters); const displayName = 'Mock Display Name'; @@ -87,16 +95,16 @@ suite('Interpreters Display', () => { }); test('Must suffix tooltip with the companyDisplayName of interpreter', async () => { const pythonPath = await new Promise(resolve => { - child_process.execFile(pythonSettings.pythonPath, ["-c", "import sys;print(sys.executable)"], (_, stdout) => { + child_process.execFile(PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { resolve(getFirstNonEmptyLineFromMultilineString(stdout)); }); - }).then(value => value.length === 0 ? pythonSettings.pythonPath : value); + }).then(value => value.length === 0 ? PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath : value); const statusBar = new MockStatusBarItem(); const interpreters = [ { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1' }, { displayName: 'Two', path: pythonPath, companyDisplayName: 'Two 2' }, - { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3' }, + { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3' } ]; const provider = new MockProvider(interpreters); const displayNameProvider = new MockInterpreterVersionProvider(''); @@ -111,25 +119,24 @@ suite('Interpreters Display', () => { const interpreters = [ { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1' }, { displayName: 'Two', path: 'c:/asdf', companyDisplayName: 'Two 2' }, - { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3' }, + { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3' } ]; const provider = new MockProvider(interpreters); const displayNameProvider = new MockInterpreterVersionProvider('', true); const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); // Change interpreter to an invalid value - pythonSettings.pythonPath = 'c:/some/unknonw/Python Interpreter.exe'; + const pythonPath = 'UnknownInterpreter'; + await updateSetting('pythonPath', pythonPath, rootWorkspaceUri, configTarget); await display.refresh(); assert.equal(statusBar.text, '$(alert) Select Python Environment', 'Incorrect display name'); }); + async function initializeMultiRoot() { + // For multiroot environments, we need a file open to determine the best interpreter that needs to be displayed + await openDummyFile(); + } + async function openDummyFile() { + const document = await workspace.openTextDocument(fileInNonRootWorkspace); + await window.showTextDocument(document); + } }); - -async function getInterpreterDisplayName(pythonPath: string, defaultValue: string) { - return utils.execPythonFile(pythonPath, ['--version'], __dirname, true) - .then(version => { - version = version.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0).join(''); - return version.length > 0 ? version : defaultValue; - }) - .catch(() => defaultValue); -} - diff --git a/src/test/interpreters/mocks.ts b/src/test/interpreters/mocks.ts index c4dde62edf6c..34c41bb41c75 100644 --- a/src/test/interpreters/mocks.ts +++ b/src/test/interpreters/mocks.ts @@ -1,21 +1,23 @@ +import { Architecture, Hive, IRegistry } from '../../client/common/registry'; +import { IInterpreterLocatorService, PythonInterpreter } from '../../client/interpreter/contracts'; import { IInterpreterVersionService } from '../../client/interpreter/interpreterVersion'; import { IVirtualEnvironment } from '../../client/interpreter/virtualEnvs/contracts'; -import { IInterpreterLocatorService, PythonInterpreter } from "../../client/interpreter/contracts"; -import { IRegistry, Hive, Architecture } from "../../client/common/registry"; export class MockProvider implements IInterpreterLocatorService { constructor(private suggestions: PythonInterpreter[]) { } - getInterpreters(): Promise { + public getInterpreters(): Promise { return Promise.resolve(this.suggestions); } + // tslint:disable-next-line:no-empty + public dispose() { } } export class MockRegistry implements IRegistry { constructor(private keys: { key: string, hive: Hive, arch?: Architecture, values: string[] }[], private values: { key: string, hive: Hive, arch?: Architecture, value: string, name?: string }[]) { } - getKeys(key: string, hive: Hive, arch?: Architecture): Promise { + public getKeys(key: string, hive: Hive, arch?: Architecture): Promise { const items = this.keys.find(item => { if (item.arch) { return item.key === key && item.hive === hive && item.arch === arch; @@ -25,7 +27,7 @@ export class MockRegistry implements IRegistry { return items ? Promise.resolve(items.values) : Promise.resolve([]); } - getValue(key: string, hive: Hive, arch?: Architecture, name?: string): Promise { + public getValue(key: string, hive: Hive, arch?: Architecture, name?: string): Promise { const items = this.values.find(item => { if (item.key !== key || item.hive !== hive) { return false; @@ -46,14 +48,17 @@ export class MockRegistry implements IRegistry { export class MockVirtualEnv implements IVirtualEnvironment { constructor(private isDetected: boolean, public name: string) { } - detect(pythonPath: string): Promise { + public detect(pythonPath: string): Promise { return Promise.resolve(this.isDetected); } } +// tslint:disable-next-line:max-classes-per-file export class MockInterpreterVersionProvider implements IInterpreterVersionService { constructor(private displayName: string, private useDefaultDisplayName: boolean = false) { } - getVersion(pythonPath: string, defaultDisplayName: string): Promise { + public getVersion(pythonPath: string, defaultDisplayName: string): Promise { return this.useDefaultDisplayName ? Promise.resolve(defaultDisplayName) : Promise.resolve(this.displayName); } + // tslint:disable-next-line:no-empty + public dispose() { } } diff --git a/src/test/interpreters/pythonPathUpdater.multiroot.test.ts b/src/test/interpreters/pythonPathUpdater.multiroot.test.ts new file mode 100644 index 000000000000..7f7c1040c68b --- /dev/null +++ b/src/test/interpreters/pythonPathUpdater.multiroot.test.ts @@ -0,0 +1,75 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; +import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; +import { GlobalPythonPathUpdaterService } from '../../client/interpreter/configuration/services/globalUpdaterService'; +import { WorkspaceFolderPythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceFolderUpdaterService'; +import { WorkspacePythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceUpdaterService'; +import { WorkspacePythonPath } from '../../client/interpreter/contracts'; +import { clearPythonPathInWorkspaceFolder } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; + +const workspaceRoot = path.join(__dirname, '..', '..', '..', 'src', 'test'); +const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); +const workspace3Uri = Uri.file(path.join(multirootPath, 'workspace3')); + +// tslint:disable-next-line:max-func-body-length +suite('Multiroot Python Path Settings Updater', () => { + suiteSetup(async function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); + setup(initializeTest); + suiteTeardown(async () => { + await closeActiveWindows(); + await initializeTest(); + }); + teardown(async () => { + await closeActiveWindows(); + await initializeTest(); + }); + + test('Updating Workspace Folder Python Path should work', async () => { + const workspaceUri = workspace3Uri; + const workspaceUpdater = new WorkspaceFolderPythonPathUpdaterService(workspace.getWorkspaceFolder(workspaceUri).uri); + const pythonPath = `xWorkspacePythonPath${new Date().getMilliseconds()}`; + await workspaceUpdater.updatePythonPath(pythonPath); + const folderValue = workspace.getConfiguration('python', workspace3Uri).inspect('pythonPath').workspaceFolderValue; + assert.equal(folderValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Updating Workspace Folder Python Path using the factor service should work', async () => { + const workspaceUri = workspace3Uri; + const factory = new PythonPathUpdaterServiceFactory(); + const workspaceUpdater = factory.getWorkspaceFolderPythonPathConfigurationService(workspace.getWorkspaceFolder(workspaceUri).uri); + const pythonPath = `xWorkspacePythonPathFromFactory${new Date().getMilliseconds()}`; + await workspaceUpdater.updatePythonPath(pythonPath); + const folderValue = workspace.getConfiguration('python', workspace3Uri).inspect('pythonPath').workspaceFolderValue; + assert.equal(folderValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Updating Workspace Python Path using the PythonPathUpdaterService should work', async () => { + const workspaceUri = workspace3Uri; + const updaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory()); + const pythonPath = `xWorkspacePythonPathFromUpdater${new Date().getMilliseconds()}`; + await updaterService.updatePythonPath(pythonPath, ConfigurationTarget.WorkspaceFolder, workspace.getWorkspaceFolder(workspaceUri).uri); + const folderValue = workspace.getConfiguration('python', workspace3Uri).inspect('pythonPath').workspaceFolderValue; + assert.equal(folderValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Python Path should be relative to workspace', async () => { + const workspaceUri = workspace.getWorkspaceFolder(workspace3Uri).uri; + const pythonInterpreter = `xWorkspacePythonPath${new Date().getMilliseconds()}`; + const pythonPath = path.join(workspaceUri.fsPath, 'x', 'y', 'z', pythonInterpreter); + const workspaceUpdater = new WorkspacePythonPathUpdaterService(workspaceUri); + await workspaceUpdater.updatePythonPath(pythonPath); + const workspaceValue = workspace.getConfiguration('python').inspect('pythonPath').workspaceValue; + // tslint:disable-next-line:no-invalid-template-strings + assert.equal(workspaceValue, path.join('${workspaceRoot}', 'x', 'y', 'z', pythonInterpreter), 'Workspace Python Path not updated'); + }); +}); diff --git a/src/test/interpreters/pythonPathUpdater.test.ts b/src/test/interpreters/pythonPathUpdater.test.ts new file mode 100644 index 000000000000..4b11c96ebd60 --- /dev/null +++ b/src/test/interpreters/pythonPathUpdater.test.ts @@ -0,0 +1,92 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; +import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; +import { GlobalPythonPathUpdaterService } from '../../client/interpreter/configuration/services/globalUpdaterService'; +import { WorkspacePythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceUpdaterService'; +import { WorkspacePythonPath } from '../../client/interpreter/contracts'; +import { clearPythonPathInWorkspaceFolder } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; + +const workspaceRoot = path.join(__dirname, '..', '..', '..', 'src', 'test'); + +// tslint:disable-next-line:max-func-body-length +suite('Python Path Settings Updater', () => { + suiteSetup(initialize); + setup(initializeTest); + suiteTeardown(async () => { + await closeActiveWindows(); + await initializeTest(); + }); + teardown(async () => { + await closeActiveWindows(); + await initializeTest(); + }); + + // Create Github issue VS Code bug (global changes not reflected immediately) + + // test('Updating Global Python Path should work', async () => { + // const globalUpdater = new GlobalPythonPathUpdaterService(); + // const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; + // await globalUpdater.updatePythonPath(pythonPath); + // const globalPythonValue = workspace.getConfiguration('python').inspect('pythonPath').globalValue; + // assert.equal(globalPythonValue, pythonPath, 'Global Python Path not updated'); + // }); + + // test('Updating Global Python Path using the factory service should work', async () => { + // const globalUpdater = new PythonPathUpdaterServiceFactory().getGlobalPythonPathConfigurationService(); + // const pythonPath = `xGlobalPythonPathFromFactory${new Date().getMilliseconds()}`; + // await globalUpdater.updatePythonPath(pythonPath); + // const globalPythonValue = workspace.getConfiguration('python').inspect('pythonPath').globalValue; + // assert.equal(globalPythonValue, pythonPath, 'Global Python Path not updated'); + // }); + + // test('Updating Global Python Path using the PythonPathUpdaterService should work', async () => { + // const updaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory()); + // const pythonPath = `xGlobalPythonPathFromUpdater${new Date().getMilliseconds()}`; + // await updaterService.updatePythonPath(pythonPath, ConfigurationTarget.Global); + // const globalPythonValue = workspace.getConfiguration('python').inspect('pythonPath').globalValue; + // assert.equal(globalPythonValue, pythonPath, 'Global Python Path not updated'); + // }); + + test('Updating Workspace Python Path should work', async () => { + const workspaceUri = Uri.file(workspaceRoot); + const workspaceUpdater = new WorkspacePythonPathUpdaterService(workspace.getWorkspaceFolder(workspaceUri).uri); + const pythonPath = `xWorkspacePythonPath${new Date().getMilliseconds()}`; + await workspaceUpdater.updatePythonPath(pythonPath); + const workspaceValue = workspace.getConfiguration('python').inspect('pythonPath').workspaceValue; + assert.equal(workspaceValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Updating Workspace Python Path using the factor service should work', async () => { + const workspaceUri = Uri.file(workspaceRoot); + const factory = new PythonPathUpdaterServiceFactory(); + const workspaceUpdater = factory.getWorkspacePythonPathConfigurationService(workspace.getWorkspaceFolder(workspaceUri).uri); + const pythonPath = `xWorkspacePythonPathFromFactory${new Date().getMilliseconds()}`; + await workspaceUpdater.updatePythonPath(pythonPath); + const workspaceValue = workspace.getConfiguration('python').inspect('pythonPath').workspaceValue; + assert.equal(workspaceValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Updating Workspace Python Path using the PythonPathUpdaterService should work', async () => { + const workspaceUri = Uri.file(workspaceRoot); + const updaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory()); + const pythonPath = `xWorkspacePythonPathFromUpdater${new Date().getMilliseconds()}`; + await updaterService.updatePythonPath(pythonPath, ConfigurationTarget.Workspace, workspace.getWorkspaceFolder(workspaceUri).uri); + const workspaceValue = workspace.getConfiguration('python').inspect('pythonPath').workspaceValue; + assert.equal(workspaceValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Python Path should be relative to workspace', async () => { + const workspaceUri = workspace.getWorkspaceFolder(Uri.file(workspaceRoot)).uri; + const pythonInterpreter = `xWorkspacePythonPath${new Date().getMilliseconds()}`; + const pythonPath = path.join(workspaceUri.fsPath, 'x', 'y', 'z', pythonInterpreter); + const workspaceUpdater = new WorkspacePythonPathUpdaterService(workspaceUri); + await workspaceUpdater.updatePythonPath(pythonPath); + const workspaceValue = workspace.getConfiguration('python').inspect('pythonPath').workspaceValue; + // tslint:disable-next-line:no-invalid-template-strings + assert.equal(workspaceValue, path.join('${workspaceRoot}', 'x', 'y', 'z', pythonInterpreter), 'Workspace Python Path not updated'); + }); +}); diff --git a/src/test/interpreters/windowsRegistryService.test.ts b/src/test/interpreters/windowsRegistryService.test.ts index 71a68c62f4bc..aedad057a4d0 100644 --- a/src/test/interpreters/windowsRegistryService.test.ts +++ b/src/test/interpreters/windowsRegistryService.test.ts @@ -1,25 +1,16 @@ import * as assert from 'assert'; import * as path from 'path'; -import * as settings from '../../client/common/configSettings'; -import { initialize } from '../initialize'; +import { initialize, initializeTest } from '../initialize'; import { IS_WINDOWS } from '../../client/debugger/Common/Utils'; import { WindowsRegistryService } from '../../client/interpreter/locators/services/windowsRegistryService'; import { MockRegistry } from './mocks'; import { Architecture, Hive } from '../../client/common/registry'; -const pythonSettings = settings.PythonSettings.getInstance(); const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); -let originalPythonPath; suite('Interpreters from Windows Registry', () => { - suiteSetup(() => { - originalPythonPath = pythonSettings.pythonPath; - return initialize(); - }); - teardown(() => { - pythonSettings.pythonPath = originalPythonPath; - }); - + suiteSetup(() => initialize()); + setup(() => initializeTest()); if (IS_WINDOWS) { test('Must return an empty list (x86)', async () => { const registry = new MockRegistry([], []); diff --git a/src/test/jupyter/jupyter.codeHelper.test.ts b/src/test/jupyter/jupyter.codeHelper.test.ts index b0a25bb906c3..27b529b72530 100644 --- a/src/test/jupyter/jupyter.codeHelper.test.ts +++ b/src/test/jupyter/jupyter.codeHelper.test.ts @@ -1,16 +1,21 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; import * as path from 'path'; -import { initialize, closeActiveWindows } from './../initialize'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; import { CodeHelper } from '../../client/jupyter/common/codeHelper'; import { JupyterCodeLensProvider } from '../../client/jupyter/editorIntegration/codeLensProvider'; const FILE_WITH_CELLS = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'jupyter', 'cells.py'); suite('Jupyter Code Helper', () => { - suiteSetup(() => initialize()); - - setup(() => closeActiveWindows()); + suiteSetup(async function () { + if (IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); + setup(() => initializeTest()); teardown(() => closeActiveWindows()); const codeLensProvider = new JupyterCodeLensProvider(); const codeHelper = new CodeHelper(codeLensProvider); diff --git a/src/test/jupyter/jupyterClient.test.ts b/src/test/jupyter/jupyterClient.test.ts index 5250443df1be..60cac7bb9d66 100644 --- a/src/test/jupyter/jupyterClient.test.ts +++ b/src/test/jupyter/jupyterClient.test.ts @@ -1,21 +1,28 @@ import * as assert from 'assert'; import { MockOutputChannel } from './mocks'; -import { initialize } from './../initialize'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; import { JupyterClientAdapter } from '../../client/jupyter/jupyter_client/main'; import { KernelRestartedError, KernelShutdownError } from '../../client/jupyter/common/errors'; import { createDeferred } from '../../client/common/helpers'; import { KernelspecMetadata } from '../../client/jupyter/contracts'; suite('JupyterClient', () => { - suiteSetup(() => initialize()); + suiteSetup(async function () { + if (IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); setup(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '0'; + process.env['VSC_PYTHON_CI_TEST'] = '0'; process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '1'; output = new MockOutputChannel('Jupyter'); jupyter = new JupyterClientAdapter(output, __dirname); + return initializeTest(); }); teardown(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '1'; + process.env['VSC_PYTHON_CI_TEST'] = '1'; process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '0'; output.dispose(); jupyter.dispose(); @@ -25,7 +32,7 @@ suite('JupyterClient', () => { let jupyter: JupyterClientAdapter; test('Ping (Process and Socket)', done => { - jupyter.start({ 'PYTHON_DONJAYAMANNE_TEST': '1', 'DEBUG_DJAYAMANNE_IPYTHON': '1' }).then(() => { + jupyter.start({ 'VSC_PYTHON_CI_TEST': '1', 'DEBUG_DJAYAMANNE_IPYTHON': '1' }).then(() => { done(); }).catch(reason => { assert.fail(reason, undefined, 'Starting Jupyter failed', ''); @@ -81,7 +88,7 @@ suite('JupyterClient', () => { }); test('Start Kernel (without start)', done => { jupyter.getAllKernelSpecs().then(kernelSpecs => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '0'; + process.env['VSC_PYTHON_CI_TEST'] = '0'; // Ok we got the kernelspecs, now create another new jupyter client // and tell it to start a specific kernel @@ -97,7 +104,7 @@ suite('JupyterClient', () => { done(); }); - process.env['PYTHON_DONJAYAMANNE_TEST'] = '1'; + process.env['VSC_PYTHON_CI_TEST'] = '1'; }).catch(reason => { assert.fail(reason, undefined, 'Failed to retrieve kernelspecs', ''); @@ -407,7 +414,7 @@ suite('JupyterClient', () => { assert.fail(reason, undefined, 'Failed to retrieve kernelspecs', ''); done(); }); - process.env['PYTHON_DONJAYAMANNE_TEST'] = '1'; + process.env['VSC_PYTHON_CI_TEST'] = '1'; }); test('Execute multiple blocks of Code', done => { jupyter.start().then(() => { diff --git a/src/test/jupyter/jupyterKernel.test.ts b/src/test/jupyter/jupyterKernel.test.ts index 3893178d74ec..89377d2c897c 100644 --- a/src/test/jupyter/jupyterKernel.test.ts +++ b/src/test/jupyter/jupyterKernel.test.ts @@ -1,6 +1,6 @@ import * as assert from 'assert'; import { MockOutputChannel } from './mocks'; -import { initialize } from './../initialize'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; import { JupyterClientAdapter } from '../../client/jupyter/jupyter_client/main'; import { KernelShutdownError } from '../../client/jupyter/common/errors'; import { createDeferred } from '../../client/common/helpers'; @@ -8,18 +8,25 @@ import { JupyterClientKernel } from '../../client/jupyter/jupyter_client-Kernel' import { KernelspecMetadata } from '../../client/jupyter/contracts'; suite('Jupyter Kernel', () => { - suiteSetup(() => initialize()); + suiteSetup(async function () { + if (IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); setup(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '0'; + process.env['VSC_PYTHON_CI_TEST'] = '0'; process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '1'; disposables = []; output = new MockOutputChannel('Jupyter'); disposables.push(output); jupyter = new JupyterClientAdapter(output, __dirname); disposables.push(jupyter); + return initializeTest(); }); teardown(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '1'; + process.env['VSC_PYTHON_CI_TEST'] = '1'; process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '0'; output.dispose(); jupyter.dispose(); diff --git a/src/test/jupyter/jupyterKernelManager.test.ts b/src/test/jupyter/jupyterKernelManager.test.ts index b4581e6889c1..27b99e77c5e6 100644 --- a/src/test/jupyter/jupyterKernelManager.test.ts +++ b/src/test/jupyter/jupyterKernelManager.test.ts @@ -1,14 +1,20 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; import { MockOutputChannel } from './mocks'; -import { initialize } from './../initialize'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; import { JupyterClientAdapter } from '../../client/jupyter/jupyter_client/main'; import { KernelManagerImpl } from '../../client/jupyter/kernel-manager'; suite('Kernel Manager', () => { - suiteSetup(() => initialize()); + suiteSetup(async function () { + if (IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); setup(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '0'; + process.env['VSC_PYTHON_CI_TEST'] = '0'; process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '1'; disposables = []; output = new MockOutputChannel('Jupyter'); @@ -17,9 +23,10 @@ suite('Kernel Manager', () => { disposables.push(jupyter); // Hack hack hack hack hack :) cmds.registerCommand = function () { }; + return initializeTest(); }); teardown(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '1'; + process.env['VSC_PYTHON_CI_TEST'] = '1'; process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '0'; output.dispose(); jupyter.dispose(); @@ -39,7 +46,7 @@ suite('Kernel Manager', () => { const oldRegisterCommand = vscode.commands.registerCommand; test('GetAllKernelSpecsFor python', done => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '0'; + process.env['VSC_PYTHON_CI_TEST'] = '0'; const mgr = new KernelManagerImpl(output, jupyter); disposables.push(mgr); mgr.getAllKernelSpecsFor('python').then(specMetadata => { diff --git a/src/test/linters/base.test.ts b/src/test/linters/base.test.ts deleted file mode 100644 index e6efa35b7add..000000000000 --- a/src/test/linters/base.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -// // -// // Note: This example test is leveraging the Mocha test framework. -// // Please refer to their documentation on https://mochajs.org/ for help. -// // Place this right on top -// import { initialize, IS_TRAVIS, PYTHON_PATH, closeActiveWindows, setPythonExecutable } from '../initialize'; -// // The module \'assert\' provides assertion methods from node -// import * as assert from 'assert'; - -// // You can import and use all API from the \'vscode\' module -// // as well as import your extension to test it -// import { EnumEx } from '../../client/common/enumUtils'; -// import { LinterFactor } from '../../client/linters/main'; -// import { SettingToDisableProduct, Product } from '../../client/common/installer'; -// import * as baseLinter from '../../client/linters/baseLinter'; -// import * as path from 'path'; -// import * as settings from '../../client/common/configSettings'; -// import { MockOutputChannel } from '../mockClasses'; -// import { Disposable } from 'vscode'; - -// const pythonSettings = settings.PythonSettings.getInstance(); -// const pythoFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'linting'); -// let disposable: Disposable; -// suite('Linting', () => { -// suiteSetup(() => { -// disposable = setPythonExecutable(pythonSettings); -// }); -// setup(() => { -// pythonSettings.linting.enabled = true; -// pythonSettings.linting.pylintEnabled = true; -// pythonSettings.linting.flake8Enabled = true; -// pythonSettings.linting.pep8Enabled = true; -// pythonSettings.linting.prospectorEnabled = true; -// pythonSettings.linting.pydocstyleEnabled = true; -// pythonSettings.linting.mypyEnabled = true; -// pythonSettings.linting.pylamaEnabled = true; -// }); -// suiteTeardown(done => { -// if (disposable) { disposable.dispose(); } -// closeActiveWindows().then(() => done(), () => done()); -// }); -// teardown(done => { -// closeActiveWindows().then(() => done(), () => done()); -// }); - -// function testEnablingDisablingOfLinter(linter: baseLinter.BaseLinter, propertyName: string) { -// pythonSettings.linting[propertyName] = true; -// assert.equal(true, linter.isEnabled()); - -// pythonSettings.linting[propertyName] = false; -// assert.equal(false, linter.isEnabled()); -// } -// EnumEx.getNamesAndValues(Product).forEach(product => { -// if (product.value === Product.autopep8 || -// product.value === Product.ctags || -// product.value === Product.pytest || -// product.value === Product.unittest || -// product.value === Product.yapf || -// product.value === Product.nosetest) { -// return; -// } -// test(`Enable and Disable ${product.name}`, () => { -// let ch = new MockOutputChannel('Lint'); -// const settingPath = SettingToDisableProduct.get(product.value); -// const settingName = path.extname(settingPath).substring(1); -// testEnablingDisablingOfLinter(LinterFactor.createLinter(product.value, ch, pythoFilesPath), settingName); -// }); -// }); -// }); \ No newline at end of file diff --git a/src/test/linters/lint.multiroot.test.ts b/src/test/linters/lint.multiroot.test.ts new file mode 100644 index 000000000000..5ad0f11b6cbb --- /dev/null +++ b/src/test/linters/lint.multiroot.test.ts @@ -0,0 +1,84 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { CancellationTokenSource, ConfigurationTarget, Uri, window, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; +import * as baseLinter from '../../client/linters/baseLinter'; +import * as flake8 from '../../client/linters/flake8'; +import * as pyLint from '../../client/linters/pylint'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { MockOutputChannel } from '../mockClasses'; + +const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); + +suite('Multiroot Linting', () => { + suiteSetup(function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + return initialize(); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + PythonSettings.dispose(); + }); + + async function testLinterInWorkspaceFolder(linter: baseLinter.BaseLinter, workspaceFolderRelativePath: string, mustHaveErrors: boolean) { + const fileToLint = path.join(multirootPath, workspaceFolderRelativePath, 'file.py'); + const cancelToken = new CancellationTokenSource(); + const document = await workspace.openTextDocument(fileToLint); + const editor = await window.showTextDocument(document); + const messages = await linter.lint(editor.document, cancelToken.token); + const errorMessage = mustHaveErrors ? 'No errors returned by linter' : 'Errors returned by linter'; + assert.equal(messages.length > 0, mustHaveErrors, errorMessage); + } + async function enableDisableSetting(workspaceFolder, configTarget: ConfigurationTarget, setting: string, value: boolean) { + const folderUri = Uri.file(workspaceFolder); + const settings = workspace.getConfiguration('python.linting', folderUri); + await settings.update(setting, value, configTarget); + } + + test('Enabling Pylint in root and also in Workspace, should return errors', async () => { + const ch = new MockOutputChannel('Lint'); + await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'pylintEnabled', true); + await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); + await testLinterInWorkspaceFolder(new pyLint.Linter(ch), 'workspace1', true); + }); + + test('Enabling Pylint in root and disabling in Workspace, should not return errors', async () => { + const ch = new MockOutputChannel('Lint'); + await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'pylintEnabled', true); + await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', false); + await testLinterInWorkspaceFolder(new pyLint.Linter(ch), 'workspace1', false); + }); + + test('Disabling Pylint in root and enabling in Workspace, should return errors', async () => { + const ch = new MockOutputChannel('Lint'); + await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'pylintEnabled', false); + await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); + await testLinterInWorkspaceFolder(new pyLint.Linter(ch), 'workspace1', true); + }); + + test('Enabling Flake8 in root and also in Workspace, should return errors', async () => { + const ch = new MockOutputChannel('Lint'); + await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'flake8Enabled', true); + await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'flake8Enabled', true); + await testLinterInWorkspaceFolder(new flake8.Linter(ch), 'workspace1', true); + }); + + test('Enabling Flake8 in root and disabling in Workspace, should not return errors', async () => { + const ch = new MockOutputChannel('Lint'); + await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'flake8Enabled', true); + await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'flake8Enabled', false); + await testLinterInWorkspaceFolder(new flake8.Linter(ch), 'workspace1', false); + }); + + test('Disabling Flake8 in root and enabling in Workspace, should return errors', async () => { + const ch = new MockOutputChannel('Lint'); + await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'flake8Enabled', false); + await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'flake8Enabled', true); + await testLinterInWorkspaceFolder(new flake8.Linter(ch), 'workspace1', true); + }); +}); diff --git a/src/test/linters/lint.test.ts b/src/test/linters/lint.test.ts index bf96de28cfc3..6eaa262f398e 100644 --- a/src/test/linters/lint.test.ts +++ b/src/test/linters/lint.test.ts @@ -1,29 +1,21 @@ -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// The module \'assert\' provides assertion methods from node import * as assert from 'assert'; - -// You can import and use all API from the \'vscode\' module -// as well as import your extension to test it +import * as fs from 'fs-extra'; +import * as path from 'path'; import * as vscode from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; +import { createDeferred } from '../../client/common/helpers'; +import { SettingToDisableProduct } from '../../client/common/installer'; +import { execPythonFile } from '../../client/common/utils'; import * as baseLinter from '../../client/linters/baseLinter'; -import * as pyLint from '../../client/linters/pylint'; -import * as pep8 from '../../client/linters/pep8Linter'; import * as flake8 from '../../client/linters/flake8'; +import * as pep8 from '../../client/linters/pep8Linter'; import * as prospector from '../../client/linters/prospector'; import * as pydocstyle from '../../client/linters/pydocstyle'; -import * as path from 'path'; -import * as settings from '../../client/common/configSettings'; -import * as fs from 'fs-extra'; -import { initialize, IS_TRAVIS, closeActiveWindows } from '../initialize'; -import { execPythonFile } from '../../client/common/utils'; -import { createDeferred } from '../../client/common/helpers'; -import { Product, SettingToDisableProduct, Linters } from '../../client/common/installer'; -import { EnumEx } from '../../client/common/enumUtils'; +import * as pyLint from '../../client/linters/pylint'; +import { PythonSettingKeys, rootWorkspaceUri, updateSetting } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; import { MockOutputChannel } from '../mockClasses'; -const pythonSettings = settings.PythonSettings.getInstance(); const pythoFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'linting'); const flake8ConfigPath = path.join(pythoFilesPath, 'flake8config'); const pep8ConfigPath = path.join(pythoFilesPath, 'pep8config'); @@ -32,7 +24,7 @@ const pylintConfigPath = path.join(pythoFilesPath, 'pylintconfig'); const fileToLint = path.join(pythoFilesPath, 'file.py'); let pylintFileToLintLines: string[] = []; -let pylintMessagesToBeReturned: baseLinter.ILintMessage[] = [ +const pylintMessagesToBeReturned: baseLinter.ILintMessage[] = [ { line: 24, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: '' }, { line: 30, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: '' }, { line: 34, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0012', message: 'Locally enabling no-member (E1101)', provider: '', type: '' }, @@ -54,10 +46,7 @@ let pylintMessagesToBeReturned: baseLinter.ILintMessage[] = [ { line: 77, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, { line: 83, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' } ]; -let pyLint3MessagesToBeReturned: baseLinter.ILintMessage[] = [ - { line: 13, column: 0, severity: baseLinter.LintMessageSeverity.Error, code: 'E0001', message: 'Missing parentheses in call to \'print\'', provider: '', type: '' } -]; -let flake8MessagesToBeReturned: baseLinter.ILintMessage[] = [ +const flake8MessagesToBeReturned: baseLinter.ILintMessage[] = [ { line: 5, column: 1, severity: baseLinter.LintMessageSeverity.Error, code: 'E302', message: 'expected 2 blank lines, found 1', provider: '', type: '' }, { line: 19, column: 15, severity: baseLinter.LintMessageSeverity.Error, code: 'E127', message: 'continuation line over-indented for visual indent', provider: '', type: '' }, { line: 24, column: 23, severity: baseLinter.LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: '' }, @@ -66,7 +55,7 @@ let flake8MessagesToBeReturned: baseLinter.ILintMessage[] = [ { line: 80, column: 5, severity: baseLinter.LintMessageSeverity.Error, code: 'E303', message: 'too many blank lines (2)', provider: '', type: '' }, { line: 87, column: 24, severity: baseLinter.LintMessageSeverity.Warning, code: 'W292', message: 'no newline at end of file', provider: '', type: '' } ]; -let pep8MessagesToBeReturned: baseLinter.ILintMessage[] = [ +const pep8MessagesToBeReturned: baseLinter.ILintMessage[] = [ { line: 5, column: 1, severity: baseLinter.LintMessageSeverity.Error, code: 'E302', message: 'expected 2 blank lines, found 1', provider: '', type: '' }, { line: 19, column: 15, severity: baseLinter.LintMessageSeverity.Error, code: 'E127', message: 'continuation line over-indented for visual indent', provider: '', type: '' }, { line: 24, column: 23, severity: baseLinter.LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: '' }, @@ -75,30 +64,30 @@ let pep8MessagesToBeReturned: baseLinter.ILintMessage[] = [ { line: 80, column: 5, severity: baseLinter.LintMessageSeverity.Error, code: 'E303', message: 'too many blank lines (2)', provider: '', type: '' }, { line: 87, column: 24, severity: baseLinter.LintMessageSeverity.Warning, code: 'W292', message: 'no newline at end of file', provider: '', type: '' } ]; -let pydocstyleMessagseToBeReturned: baseLinter.ILintMessage[] = [ - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'e\')', 'column': 0, 'line': 1, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'t\')', 'column': 0, 'line': 5, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D102', severity: baseLinter.LintMessageSeverity.Information, 'message': 'Missing docstring in public method', 'column': 4, 'line': 8, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D401', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should be in imperative mood (\'thi\', not \'this\')', 'column': 4, 'line': 11, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D403', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First word of the first line should be properly capitalized (\'This\', not \'this\')', 'column': 4, 'line': 11, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'e\')', 'column': 4, 'line': 11, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D403', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First word of the first line should be properly capitalized (\'And\', not \'and\')', 'column': 4, 'line': 15, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'t\')', 'column': 4, 'line': 15, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D403', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', 'column': 4, 'line': 21, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'g\')', 'column': 4, 'line': 21, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D403', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', 'column': 4, 'line': 28, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'g\')', 'column': 4, 'line': 28, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D403', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', 'column': 4, 'line': 38, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'g\')', 'column': 4, 'line': 38, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D403', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', 'column': 4, 'line': 53, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'g\')', 'column': 4, 'line': 53, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D403', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', 'column': 4, 'line': 68, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'g\')', 'column': 4, 'line': 68, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D403', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', 'column': 4, 'line': 80, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'g\')', 'column': 4, 'line': 80, 'type': '', 'provider': 'pydocstyle' } +const pydocstyleMessagseToBeReturned: baseLinter.ILintMessage[] = [ + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'e\')', column: 0, line: 1, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'t\')', column: 0, line: 5, type: '', provider: 'pydocstyle' }, + { code: 'D102', severity: baseLinter.LintMessageSeverity.Information, message: 'Missing docstring in public method', column: 4, line: 8, type: '', provider: 'pydocstyle' }, + { code: 'D401', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should be in imperative mood (\'thi\', not \'this\')', column: 4, line: 11, type: '', provider: 'pydocstyle' }, + { code: 'D403', severity: baseLinter.LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'This\', not \'this\')', column: 4, line: 11, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'e\')', column: 4, line: 11, type: '', provider: 'pydocstyle' }, + { code: 'D403', severity: baseLinter.LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'And\', not \'and\')', column: 4, line: 15, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'t\')', column: 4, line: 15, type: '', provider: 'pydocstyle' }, + { code: 'D403', severity: baseLinter.LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 21, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 21, type: '', provider: 'pydocstyle' }, + { code: 'D403', severity: baseLinter.LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 28, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 28, type: '', provider: 'pydocstyle' }, + { code: 'D403', severity: baseLinter.LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 38, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 38, type: '', provider: 'pydocstyle' }, + { code: 'D403', severity: baseLinter.LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 53, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 53, type: '', provider: 'pydocstyle' }, + { code: 'D403', severity: baseLinter.LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 68, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 68, type: '', provider: 'pydocstyle' }, + { code: 'D403', severity: baseLinter.LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 80, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 80, type: '', provider: 'pydocstyle' } ]; -let filteredPylintMessagesToBeReturned: baseLinter.ILintMessage[] = [ +const filteredPylintMessagesToBeReturned: baseLinter.ILintMessage[] = [ { line: 26, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blop\' member', provider: '', type: '' }, { line: 36, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, { line: 46, column: 18, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, @@ -108,178 +97,181 @@ let filteredPylintMessagesToBeReturned: baseLinter.ILintMessage[] = [ { line: 77, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, { line: 83, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' } ]; -let filteredPylint3MessagesToBeReturned: baseLinter.ILintMessage[] = [ +const filteredPylint3MessagesToBeReturned: baseLinter.ILintMessage[] = [ ]; -let filteredFlake8MessagesToBeReturned: baseLinter.ILintMessage[] = [ +const filteredFlake8MessagesToBeReturned: baseLinter.ILintMessage[] = [ { line: 87, column: 24, severity: baseLinter.LintMessageSeverity.Warning, code: 'W292', message: 'no newline at end of file', provider: '', type: '' } ]; -let filteredPep88MessagesToBeReturned: baseLinter.ILintMessage[] = [ +const filteredPep88MessagesToBeReturned: baseLinter.ILintMessage[] = [ { line: 87, column: 24, severity: baseLinter.LintMessageSeverity.Warning, code: 'W292', message: 'no newline at end of file', provider: '', type: '' } ]; -let fiteredPydocstyleMessagseToBeReturned: baseLinter.ILintMessage[] = [ - { 'code': 'D102', severity: baseLinter.LintMessageSeverity.Information, 'message': 'Missing docstring in public method', 'column': 4, 'line': 8, 'type': '', 'provider': 'pydocstyle' } +const fiteredPydocstyleMessagseToBeReturned: baseLinter.ILintMessage[] = [ + { code: 'D102', severity: baseLinter.LintMessageSeverity.Information, message: 'Missing docstring in public method', column: 4, line: 8, type: '', provider: 'pydocstyle' } ]; - +// tslint:disable-next-line:max-func-body-length suite('Linting', () => { const isPython3Deferred = createDeferred(); const isPython3 = isPython3Deferred.promise; suiteSetup(async () => { pylintFileToLintLines = fs.readFileSync(fileToLint).toString('utf-8').split(/\r?\n/g); await initialize(); - const version = await execPythonFile(pythonSettings.pythonPath, ['--version'], __dirname, true); + const version = await execPythonFile(fileToLint, PythonSettings.getInstance(vscode.Uri.file(fileToLint)).pythonPath, ['--version'], __dirname, true); isPython3Deferred.resolve(version.indexOf('3.') >= 0); }); - setup(() => { - pythonSettings.linting.lintOnSave = false; - pythonSettings.linting.lintOnTextChange = false; - pythonSettings.linting.enabled = true; - pythonSettings.linting.pylintEnabled = true; - pythonSettings.linting.flake8Enabled = true; - pythonSettings.linting.pep8Enabled = true; - pythonSettings.linting.prospectorEnabled = true; - pythonSettings.linting.pydocstyleEnabled = true; + setup(async () => { + await initializeTest(); + await resetSettings(); }); - suiteTeardown(() => closeActiveWindows()); - teardown(() => { - closeActiveWindows(); - pythonSettings.linting.lintOnSave = false; - pythonSettings.linting.lintOnTextChange = false; - pythonSettings.linting.enabled = true; - pythonSettings.linting.pylintEnabled = true; - pythonSettings.linting.flake8Enabled = true; - pythonSettings.linting.pep8Enabled = true; - pythonSettings.linting.prospectorEnabled = true; - pythonSettings.linting.pydocstyleEnabled = true; + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + await resetSettings(); }); + async function resetSettings() { + // Don't run these updates in parallel, as they are updating the same file. + await updateSetting('linting.enabled', true, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + if (IS_MULTI_ROOT_TEST) { + await updateSetting('linting.enabled', true, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + } + await updateSetting('linting.lintOnSave', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + await updateSetting('linting.lintOnTextChange', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + await updateSetting('linting.pylintEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + await updateSetting('linting.flake8Enabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + await updateSetting('linting.pep8Enabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + await updateSetting('linting.prospectorEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + await updateSetting('linting.mypyEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + await updateSetting('linting.pydocstyleEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + await updateSetting('linting.pylamaEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); - async function testEnablingDisablingOfLinter(linter: baseLinter.BaseLinter, propertyName: string) { - pythonSettings.linting[propertyName] = true; - - let cancelToken = new vscode.CancellationTokenSource(); - disableAllButThisLinter(linter.product); + if (IS_MULTI_ROOT_TEST) { + await updateSetting('linting.lintOnSave', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + await updateSetting('linting.lintOnTextChange', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + await updateSetting('linting.pylintEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + await updateSetting('linting.flake8Enabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + await updateSetting('linting.pep8Enabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + await updateSetting('linting.prospectorEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + await updateSetting('linting.mypyEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + await updateSetting('linting.pydocstyleEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + await updateSetting('linting.pylamaEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + } + } + async function testEnablingDisablingOfLinter(linter: baseLinter.BaseLinter, setting: PythonSettingKeys, enabled: boolean, output: MockOutputChannel) { + await updateSetting(setting, enabled, rootWorkspaceUri, IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace); const document = await vscode.workspace.openTextDocument(fileToLint); const editor = await vscode.window.showTextDocument(document); - try { - const messages = await linter.lint(editor.document, cancelToken.token); - assert.equal(messages.length, 0, 'Errors returned even when linter is disabled'); - } catch (error) { - assert.fail(error, null, 'Linter error'); + const cancelToken = new vscode.CancellationTokenSource(); + const messages = await linter.lint(editor.document, cancelToken.token); + if (enabled) { + assert.notEqual(messages.length, 0, `No linter errors when linter is enabled, Output - ${output.output}`); + } + else { + assert.equal(messages.length, 0, `Errors returned when linter is disabled, Output - ${output.output}`); } } - test('Enable and Disable Pylint', () => { - let ch = new MockOutputChannel('Lint'); - testEnablingDisablingOfLinter(new pyLint.Linter(ch), 'pylintEnabled'); + test('Disable Pylint and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new pyLint.Linter(ch), 'linting.pylintEnabled', false, ch); + }); + test('Enable Pylint and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new pyLint.Linter(ch), 'linting.pylintEnabled', true, ch); + }); + test('Disable Pep8 and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new pep8.Linter(ch), 'linting.pep8Enabled', false, ch); + }); + test('Enable Pep8 and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new pep8.Linter(ch), 'linting.pep8Enabled', true, ch); }); - test('Enable and Disable Pep8', () => { - let ch = new MockOutputChannel('Lint'); - testEnablingDisablingOfLinter(new pep8.Linter(ch), 'pep8Enabled'); + test('Disable Flake8 and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new flake8.Linter(ch), 'linting.flake8Enabled', false, ch); }); - test('Enable and Disable Flake8', () => { - let ch = new MockOutputChannel('Lint'); - testEnablingDisablingOfLinter(new flake8.Linter(ch), 'flake8Enabled'); + test('Enable Flake8 and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new flake8.Linter(ch), 'linting.flake8Enabled', true, ch); }); - test('Enable and Disable Prospector', () => { - let ch = new MockOutputChannel('Lint'); - testEnablingDisablingOfLinter(new prospector.Linter(ch), 'prospectorEnabled'); + test('Disable Prospector and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new prospector.Linter(ch), 'linting.prospectorEnabled', false, ch); }); - test('Enable and Disable Pydocstyle', () => { - let ch = new MockOutputChannel('Lint'); - testEnablingDisablingOfLinter(new pydocstyle.Linter(ch), 'pydocstyleEnabled'); + test('Disable Pydocstyle and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new pydocstyle.Linter(ch), 'linting.pydocstyleEnabled', false, ch); + }); + test('Enable Pydocstyle and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new pydocstyle.Linter(ch), 'linting.pydocstyleEnabled', true, ch); }); - function disableAllButThisLinter(linterToEnable: Product) { - EnumEx.getNamesAndValues(Product).map(linter => { - if (Linters.indexOf(linter.value) === -1) { - return; + // tslint:disable-next-line:no-any + async function testLinterMessages(linter: baseLinter.BaseLinter, outputChannel: MockOutputChannel, pythonFile: string, messagesToBeReceived: baseLinter.ILintMessage[]): Promise { + const cancelToken = new vscode.CancellationTokenSource(); + const settingToEnable = SettingToDisableProduct.get(linter.product); + // tslint:disable-next-line:no-any prefer-type-cast + await updateSetting(settingToEnable as any, true, rootWorkspaceUri, IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace); + const document = await vscode.workspace.openTextDocument(pythonFile); + const editor = await vscode.window.showTextDocument(document); + const messages = await linter.lint(editor.document, cancelToken.token); + if (messagesToBeReceived.length === 0) { + assert.equal(messages.length, 0, `No errors in linter, Output - ${outputChannel.output}`); + } + else { + if (outputChannel.output.indexOf('ENOENT') === -1) { + // Pylint for Python Version 2.7 could return 80 linter messages, where as in 3.5 it might only return 1. + // Looks like pylint stops linting as soon as it comes across any ERRORS. + assert.notEqual(messages.length, 0, `No errors in linter, Output - ${outputChannel.output}`); } - var setting = path.extname(SettingToDisableProduct.get(linter.value)).substring(1); - pythonSettings.linting[setting] = linterToEnable === linter.value; - }); - } - function testLinterMessages(linter: baseLinter.BaseLinter, outputChannel: MockOutputChannel, pythonFile: string, messagesToBeReceived: baseLinter.ILintMessage[]): Thenable { - - let cancelToken = new vscode.CancellationTokenSource(); - disableAllButThisLinter(linter.product); - return vscode.workspace.openTextDocument(pythonFile) - .then(document => vscode.window.showTextDocument(document)) - .then(editor => { - return linter.lint(editor.document, cancelToken.token); - }) - .then(messages => { - // Different versions of python return different errors, - if (messagesToBeReceived.length === 0) { - assert.equal(messages.length, 0, 'No errors in linter, Output - ' + outputChannel.output); - } - else { - if (outputChannel.output.indexOf('ENOENT') === -1) { - // Pylint for Python Version 2.7 could return 80 linter messages, where as in 3.5 it might only return 1 - // Looks like pylint stops linting as soon as it comes across any ERRORS - assert.notEqual(messages.length, 0, 'No errors in linter, Output - ' + outputChannel.output); - } - else { - assert.ok('Linter not installed', 'Linter not installed'); - } - } - // messagesToBeReceived.forEach(msg => { - // let similarMessages = messages.filter(m => m.code === msg.code && m.column === msg.column && - // m.line === msg.line && m.message === msg.message && m.severity === msg.severity); - // assert.equal(true, similarMessages.length > 0, 'Error not found, ' + JSON.stringify(msg) + '\n, Output - ' + outputChannel.output); - // }); - }, error => { - assert.fail(error, null, 'Linter error, Output - ' + outputChannel.output, ''); - }); + } } - test('PyLint', done => { - let ch = new MockOutputChannel('Lint'); - let linter = new pyLint.Linter(ch); - testLinterMessages(linter, ch, fileToLint, pylintMessagesToBeReturned).then(done, done); + test('PyLint', async () => { + const ch = new MockOutputChannel('Lint'); + const linter = new pyLint.Linter(ch); + await testLinterMessages(linter, ch, fileToLint, pylintMessagesToBeReturned); }); - test('Flake8', done => { - let ch = new MockOutputChannel('Lint'); - let linter = new flake8.Linter(ch); - testLinterMessages(linter, ch, fileToLint, flake8MessagesToBeReturned).then(done, done); + test('Flake8', async () => { + const ch = new MockOutputChannel('Lint'); + const linter = new flake8.Linter(ch); + await testLinterMessages(linter, ch, fileToLint, flake8MessagesToBeReturned); }); - test('Pep8', done => { - let ch = new MockOutputChannel('Lint'); - let linter = new pep8.Linter(ch); - testLinterMessages(linter, ch, fileToLint, pep8MessagesToBeReturned).then(done, done); + test('Pep8', async () => { + const ch = new MockOutputChannel('Lint'); + const linter = new pep8.Linter(ch); + await testLinterMessages(linter, ch, fileToLint, pep8MessagesToBeReturned); }); - if (!isPython3) { - test('Pydocstyle', done => { - let ch = new MockOutputChannel('Lint'); - let linter = new pydocstyle.Linter(ch); - testLinterMessages(linter, ch, fileToLint, pydocstyleMessagseToBeReturned).then(done, done); - }); - } - // Version dependenant, will be enabled once we have fixed this - // TODO: Check version of python running and accordingly change the values - if (!IS_TRAVIS) { - isPython3.then(value => { - const messagesToBeReturned = value ? filteredPylint3MessagesToBeReturned : filteredPylintMessagesToBeReturned; - test('PyLint with config in root', done => { - let ch = new MockOutputChannel('Lint'); - let linter = new pyLint.Linter(ch); - testLinterMessages(linter, ch, path.join(pylintConfigPath, 'file.py'), messagesToBeReturned).then(done, done); - }); + test('Pydocstyle', async () => { + const ch = new MockOutputChannel('Lint'); + const linter = new pydocstyle.Linter(ch); + await testLinterMessages(linter, ch, fileToLint, pydocstyleMessagseToBeReturned); + }); + // tslint:disable-next-line:no-floating-promises + isPython3.then(value => { + const messagesToBeReturned = value ? filteredPylint3MessagesToBeReturned : filteredPylintMessagesToBeReturned; + test('PyLint with config in root', async () => { + const ch = new MockOutputChannel('Lint'); + const linter = new pyLint.Linter(ch); + await testLinterMessages(linter, ch, path.join(pylintConfigPath, 'file.py'), messagesToBeReturned); }); - } - test('Flake8 with config in root', done => { - let ch = new MockOutputChannel('Lint'); - let linter = new flake8.Linter(ch); - testLinterMessages(linter, ch, path.join(flake8ConfigPath, 'file.py'), filteredFlake8MessagesToBeReturned).then(done, done); }); - test('Pep8 with config in root', done => { - let ch = new MockOutputChannel('Lint'); - let linter = new pep8.Linter(ch); - testLinterMessages(linter, ch, path.join(pep8ConfigPath, 'file.py'), filteredPep88MessagesToBeReturned).then(done, done); + test('Flake8 with config in root', async () => { + const ch = new MockOutputChannel('Lint'); + const linter = new flake8.Linter(ch); + await testLinterMessages(linter, ch, path.join(flake8ConfigPath, 'file.py'), filteredFlake8MessagesToBeReturned); + }); + test('Pep8 with config in root', async () => { + const ch = new MockOutputChannel('Lint'); + const linter = new pep8.Linter(ch); + await testLinterMessages(linter, ch, path.join(pep8ConfigPath, 'file.py'), filteredPep88MessagesToBeReturned); }); + // tslint:disable-next-line:no-floating-promises isPython3.then(value => { const messagesToBeReturned = value ? [] : fiteredPydocstyleMessagseToBeReturned; - test('Pydocstyle with config in root', done => { - let ch = new MockOutputChannel('Lint'); - let linter = new pydocstyle.Linter(ch); - testLinterMessages(linter, ch, path.join(pydocstyleConfigPath27, 'file.py'), messagesToBeReturned).then(done, done); + test('Pydocstyle with config in root', async () => { + const ch = new MockOutputChannel('Lint'); + const linter = new pydocstyle.Linter(ch); + await testLinterMessages(linter, ch, path.join(pydocstyleConfigPath27, 'file.py'), messagesToBeReturned); }); }); }); diff --git a/src/test/multiRootTest.ts b/src/test/multiRootTest.ts new file mode 100644 index 000000000000..57bf5368ccb7 --- /dev/null +++ b/src/test/multiRootTest.ts @@ -0,0 +1,9 @@ +import * as path from 'path'; + +process.env.CODE_TESTS_WORKSPACE = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); + +function start() { + console.log('start Multiroot tests'); + require('../../node_modules/vscode/bin/test'); +} +start(); diff --git a/src/test/multiRootWkspc/disableLinters/.vscode/tags b/src/test/multiRootWkspc/disableLinters/.vscode/tags new file mode 100644 index 000000000000..4739b4629cfb --- /dev/null +++ b/src/test/multiRootWkspc/disableLinters/.vscode/tags @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/test/multiRootWkspc/disableLinters/file.py b/src/test/multiRootWkspc/disableLinters/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/multiRootWkspc/disableLinters/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/multiRootWkspc/multi.code-workspace b/src/test/multiRootWkspc/multi.code-workspace new file mode 100644 index 000000000000..3c2ef8e8ca44 --- /dev/null +++ b/src/test/multiRootWkspc/multi.code-workspace @@ -0,0 +1,29 @@ +{ + "folders": [ + { + "path": "workspace1" + }, + { + "path": "workspace2" + }, + { + "path": "workspace3" + }, + { + "path": "parent\\child" + }, + { + "path": "disableLinters" + } + ], + "settings": { + "python.linting.flake8Enabled": false, + "python.linting.mypyEnabled": true, + "python.linting.pydocstyleEnabled": true, + "python.linting.pylamaEnabled": true, + "python.linting.pylintEnabled": false, + "python.linting.pep8Enabled": true, + "python.linting.prospectorEnabled": true, + "python.workspaceSymbols.enabled": true + } +} diff --git a/src/test/multiRootWkspc/parent/child/.vscode/settings.json b/src/test/multiRootWkspc/parent/child/.vscode/settings.json new file mode 100644 index 000000000000..b78380782cd9 --- /dev/null +++ b/src/test/multiRootWkspc/parent/child/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.workspaceSymbols.enabled": true +} \ No newline at end of file diff --git a/src/test/multiRootWkspc/parent/child/.vscode/tags b/src/test/multiRootWkspc/parent/child/.vscode/tags new file mode 100644 index 000000000000..e6791c755b0f --- /dev/null +++ b/src/test/multiRootWkspc/parent/child/.vscode/tags @@ -0,0 +1,24 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Child2Class ..\\childFile.py /^class Child2Class(object):$/;" kind:class line:5 +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\childFile.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\childFile.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +childFile.py ..\\childFile.py 1;" kind:file line:1 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1OfChild ..\\childFile.py /^ def meth1OfChild(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/test/multiRootWkspc/parent/child/childFile.py b/src/test/multiRootWkspc/parent/child/childFile.py new file mode 100644 index 000000000000..31d6fc7b4a18 --- /dev/null +++ b/src/test/multiRootWkspc/parent/child/childFile.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Child2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfChild(self, arg): + """this issues a message""" + print (self) diff --git a/src/test/multiRootWkspc/parent/child/file.py b/src/test/multiRootWkspc/parent/child/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/multiRootWkspc/parent/child/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/multiRootWkspc/workspace1/.vscode/settings.json b/src/test/multiRootWkspc/workspace1/.vscode/settings.json new file mode 100644 index 000000000000..a783cfe01962 --- /dev/null +++ b/src/test/multiRootWkspc/workspace1/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.linting.pylintEnabled": true, + "python.linting.enabled": false, + "python.linting.flake8Enabled": true +} diff --git a/src/test/multiRootWkspc/workspace1/.vscode/tags b/src/test/multiRootWkspc/workspace1/.vscode/tags new file mode 100644 index 000000000000..4739b4629cfb --- /dev/null +++ b/src/test/multiRootWkspc/workspace1/.vscode/tags @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/test/multiRootWkspc/workspace1/file.py b/src/test/multiRootWkspc/workspace1/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/multiRootWkspc/workspace1/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/multiRootWkspc/workspace2/.vscode/settings.json b/src/test/multiRootWkspc/workspace2/.vscode/settings.json new file mode 100644 index 000000000000..385728982cfa --- /dev/null +++ b/src/test/multiRootWkspc/workspace2/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.workspaceSymbols.tagFilePath": "${workspaceRoot}/workspace2.tags.file", + "python.workspaceSymbols.enabled": true +} diff --git a/src/test/multiRootWkspc/workspace2/file.py b/src/test/multiRootWkspc/workspace2/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/multiRootWkspc/workspace2/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/multiRootWkspc/workspace2/workspace2.tags.file b/src/test/multiRootWkspc/workspace2/workspace2.tags.file new file mode 100644 index 000000000000..2d54e7ed7c7b --- /dev/null +++ b/src/test/multiRootWkspc/workspace2/workspace2.tags.file @@ -0,0 +1,24 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^class Foo(object):$/;" kind:class line:5 +Workspace2Class C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\workspace2File.py /^class Workspace2Class(object):$/;" kind:class line:5 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\workspace2File.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\workspace2File.py /^__revision__ = None$/;" kind:variable line:3 +file.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py 1;" kind:file line:1 +meth1 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1OfWorkspace2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\workspace2File.py /^ def meth1OfWorkspace2(self, arg):$/;" kind:member line:11 +meth2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth8(self):$/;" kind:member line:80 +workspace2File.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\workspace2File.py 1;" kind:file line:1 diff --git a/src/test/multiRootWkspc/workspace2/workspace2File.py b/src/test/multiRootWkspc/workspace2/workspace2File.py new file mode 100644 index 000000000000..61aa87c55fed --- /dev/null +++ b/src/test/multiRootWkspc/workspace2/workspace2File.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Workspace2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfWorkspace2(self, arg): + """this issues a message""" + print (self) diff --git a/src/test/multiRootWkspc/workspace3/.vscode/settings.json b/src/test/multiRootWkspc/workspace3/.vscode/settings.json new file mode 100644 index 000000000000..8779a0c08efe --- /dev/null +++ b/src/test/multiRootWkspc/workspace3/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.workspaceSymbols.tagFilePath": "${workspaceRoot}/workspace3.tags.file" +} diff --git a/src/test/multiRootWkspc/workspace3/file.py b/src/test/multiRootWkspc/workspace3/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/multiRootWkspc/workspace3/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/multiRootWkspc/workspace3/workspace3.tags.file b/src/test/multiRootWkspc/workspace3/workspace3.tags.file new file mode 100644 index 000000000000..9a141392d6ae --- /dev/null +++ b/src/test/multiRootWkspc/workspace3/workspace3.tags.file @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py 1;" kind:file line:1 +meth1 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/test/providers/shebangCodeLenseProvider.test.ts b/src/test/providers/shebangCodeLenseProvider.test.ts index a3af897f4cd6..23616ebb3e70 100644 --- a/src/test/providers/shebangCodeLenseProvider.test.ts +++ b/src/test/providers/shebangCodeLenseProvider.test.ts @@ -1,56 +1,82 @@ import * as assert from 'assert'; +import * as child_process from 'child_process'; import * as path from 'path'; import * as vscode from 'vscode'; -import { ShebangCodeLensProvider } from '../../client/providers/shebangCodeLensProvider' - -import { initialize, IS_TRAVIS, closeActiveWindows } from '../initialize'; +import { ConfigurationTarget } from 'vscode'; +import { IS_WINDOWS, PythonSettings } from '../../client/common/configSettings'; +import { ShebangCodeLensProvider } from '../../client/interpreter/display/shebangCodeLensProvider'; +import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; +import { rootWorkspaceUri, updateSetting } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'shebang'); const fileShebang = path.join(autoCompPath, 'shebang.py'); +const fileShebangEnv = path.join(autoCompPath, 'shebangEnv.py'); +const fileShebangInvalid = path.join(autoCompPath, 'shebangInvalid.py'); const filePlain = path.join(autoCompPath, 'plain.py'); -var settings = vscode.workspace.getConfiguration("python"); -const origPythonPath = settings.get("pythonPath"); - -suite("Shebang detection", () => { - suiteSetup(async () => { - await initialize(); - }); - +suite('Shebang detection', () => { + suiteSetup(initialize); suiteTeardown(async () => { - await vscode.workspace.getConfiguration("python").update("pythonPath", origPythonPath); - }); - - teardown(() => closeActiveWindows()); - setup(() => { - settings = vscode.workspace.getConfiguration("python"); + await initialize(); + await closeActiveWindows(); }); + setup(initializeTest); - test("Shebang available, CodeLens showing", async () => { - await settings.update("pythonPath", "python"); + test('A code lens will appear when sheban python and python in settings are different', async () => { + const pythonPath = 'someUnknownInterpreter'; const editor = await openFile(fileShebang); + PythonSettings.getInstance(editor.document.uri).pythonPath = pythonPath; const codeLenses = await setupCodeLens(editor); - assert.equal(codeLenses.length, 1, "No CodeLens available"); - let codeLens = codeLenses[0]; + assert.equal(codeLenses.length, 1, 'No CodeLens available'); + const codeLens = codeLenses[0]; assert(codeLens.range.isSingleLine, 'Invalid CodeLens Range'); assert.equal(codeLens.command.command, 'python.setShebangInterpreter'); - }); - test("Shebang available, CodeLens hiding", async () => { - await settings.update("pythonPath", "/usr/bin/test"); + test('Code lens will not appear when sheban python and python in settings are the same', async () => { + PythonSettings.dispose(); + const pythonPath = await getFullyQualifiedPathToInterpreter('python'); const editor = await openFile(fileShebang); + PythonSettings.getInstance(editor.document.uri).pythonPath = pythonPath; const codeLenses = await setupCodeLens(editor); - assert(!codeLenses, "CodeLens available although interpreters are equal"); + assert.equal(codeLenses.length, 0, 'CodeLens available although interpreters are equal'); }); - test("Shebang missing, CodeLens hiding", async () => { - const editor = await openFile(filePlain); + test('Code lens will not appear when sheban python is invalid', async () => { + const editor = await openFile(fileShebangInvalid); const codeLenses = await setupCodeLens(editor); - assert(!codeLenses, "CodeLens available although no shebang"); + assert.equal(codeLenses.length, 0, 'CodeLens available even when shebang is invalid'); + }); + + if (!IS_WINDOWS) { + test('A code lens will appear when shebang python uses env and python settings are different', async () => { + const editor = await openFile(fileShebangEnv); + PythonSettings.getInstance(editor.document.uri).pythonPath = 'p1'; + const codeLenses = await setupCodeLens(editor); + + assert.equal(codeLenses.length, 1, 'No CodeLens available'); + const codeLens = codeLenses[0]; + assert(codeLens.range.isSingleLine, 'Invalid CodeLens Range'); + assert.equal(codeLens.command.command, 'python.setShebangInterpreter'); + }); + + test('Code lens will not appear even when shebang python uses env and python settings are the same', async () => { + const pythonPath = await getFullyQualifiedPathToInterpreter('python'); + const editor = await openFile(fileShebangEnv); + PythonSettings.getInstance(editor.document.uri).pythonPath = pythonPath; + const codeLenses = await setupCodeLens(editor); + assert.equal(codeLenses.length, 0, 'CodeLens available although interpreters are equal'); + }); + } + + test('Code lens will not appear as there is no shebang', async () => { + const editor = await openFile(filePlain); + const codeLenses = await setupCodeLens(editor); + assert.equal(codeLenses.length, 0, 'CodeLens available although no shebang'); }); async function openFile(fileName: string) { @@ -59,11 +85,17 @@ suite("Shebang detection", () => { assert(vscode.window.activeTextEditor, 'No active editor'); return editor; } + async function getFullyQualifiedPathToInterpreter(pythonPath: string) { + return new Promise(resolve => { + child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { + resolve(getFirstNonEmptyLineFromMultilineString(stdout)); + }); + }).catch(() => undefined); + } async function setupCodeLens(editor: vscode.TextEditor) { const document = editor.document; const codeLensProvider = new ShebangCodeLensProvider(); - const codeLenses = await codeLensProvider.provideCodeLenses(document, null); - return codeLenses; + return await codeLensProvider.provideCodeLenses(document, null); } -}); \ No newline at end of file +}); diff --git a/src/test/pythonFiles/linting/file.py b/src/test/pythonFiles/linting/file.py index 047ba0dc679e..7b625a769243 100644 --- a/src/test/pythonFiles/linting/file.py +++ b/src/test/pythonFiles/linting/file.py @@ -10,78 +10,78 @@ def __init__(self): def meth1(self, arg): """this issues a message""" - print self + print (self) def meth2(self, arg): """and this one not""" # pylint: disable=unused-argument - print self\ - + "foo" + print (self\ + + "foo") def meth3(self): """test one line disabling""" # no error - print self.bla # pylint: disable=no-member + print (self.bla) # pylint: disable=no-member # error - print self.blop + print (self.blop) def meth4(self): """test re-enabling""" # pylint: disable=no-member # no error - print self.bla - print self.blop + print (self.bla) + print (self.blop) # pylint: enable=no-member # error - print self.blip + print (self.blip) def meth5(self): """test IF sub-block re-enabling""" # pylint: disable=no-member # no error - print self.bla + print (self.bla) if self.blop: # pylint: enable=no-member # error - print self.blip + print (self.blip) else: # no error - print self.blip + print (self.blip) # no error - print self.blip + print (self.blip) def meth6(self): """test TRY/EXCEPT sub-block re-enabling""" # pylint: disable=no-member # no error - print self.bla + print (self.bla) try: # pylint: enable=no-member # error - print self.blip + print (self.blip) except UndefinedName: # pylint: disable=undefined-variable # no error - print self.blip + print (self.blip) # no error - print self.blip + print (self.blip) def meth7(self): """test one line block opening disabling""" if self.blop: # pylint: disable=no-member # error - print self.blip + print (self.blip) else: # error - print self.blip + print (self.blip) # error - print self.blip + print (self.blip) def meth8(self): """test late disabling""" # error - print self.blip + print (self.blip) # pylint: disable=no-member # no error - print self.bla - print self.blop \ No newline at end of file + print (self.bla) + print (self.blop) diff --git a/src/test/pythonFiles/shebang/shebang.py b/src/test/pythonFiles/shebang/shebang.py index 26611cb041df..20d13ba825fb 100644 --- a/src/test/pythonFiles/shebang/shebang.py +++ b/src/test/pythonFiles/shebang/shebang.py @@ -1,3 +1,3 @@ -#!/usr/bin/test +#!python print("dummy") diff --git a/src/test/pythonFiles/shebang/shebangEnv.py b/src/test/pythonFiles/shebang/shebangEnv.py new file mode 100644 index 000000000000..c08ab31b509f --- /dev/null +++ b/src/test/pythonFiles/shebang/shebangEnv.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python + +print("dummy") diff --git a/src/test/pythonFiles/shebang/shebangInvalid.py b/src/test/pythonFiles/shebang/shebangInvalid.py new file mode 100644 index 000000000000..8a66a42523fb --- /dev/null +++ b/src/test/pythonFiles/shebang/shebangInvalid.py @@ -0,0 +1,3 @@ +#!/usr/bin/env1234 python + +print("dummy") diff --git a/src/test/pythonFiles/symbolFiles/childFile.py b/src/test/pythonFiles/symbolFiles/childFile.py new file mode 100644 index 000000000000..31d6fc7b4a18 --- /dev/null +++ b/src/test/pythonFiles/symbolFiles/childFile.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Child2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfChild(self, arg): + """this issues a message""" + print (self) diff --git a/src/test/pythonFiles/symbolFiles/file.py b/src/test/pythonFiles/symbolFiles/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/pythonFiles/symbolFiles/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/pythonFiles/symbolFiles/workspace2File.py b/src/test/pythonFiles/symbolFiles/workspace2File.py new file mode 100644 index 000000000000..61aa87c55fed --- /dev/null +++ b/src/test/pythonFiles/symbolFiles/workspace2File.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Workspace2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfWorkspace2(self, arg): + """this issues a message""" + print (self) diff --git a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_one.py b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_one.py new file mode 100644 index 000000000000..db18d3885488 --- /dev/null +++ b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_one.py @@ -0,0 +1,8 @@ +import unittest + +class Test_test_one_1(unittest.TestCase): + def test_1_1_1(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.py b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.py new file mode 100644 index 000000000000..4e1a6151deb1 --- /dev/null +++ b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.py @@ -0,0 +1,8 @@ +import unittest + +class Test_test_two_2(unittest.TestCase): + def test_2_1_1(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.txt b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.txt new file mode 100644 index 000000000000..4e1a6151deb1 --- /dev/null +++ b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.txt @@ -0,0 +1,8 @@ +import unittest + +class Test_test_two_2(unittest.TestCase): + def test_2_1_1(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.updated.txt b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.updated.txt new file mode 100644 index 000000000000..b70c80df1619 --- /dev/null +++ b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.updated.txt @@ -0,0 +1,14 @@ +import unittest + +class Test_test_two_2(unittest.TestCase): + def test_2_1_1(self): + self.assertEqual(1,1,'Not equal') + + def test_2_1_2(self): + self.assertEqual(1,1,'Not equal') + + def test_2_1_3(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py new file mode 100644 index 000000000000..4825f3a4db3b --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py @@ -0,0 +1,15 @@ +import sys +import os + +import unittest + +class Test_test1(unittest.TestCase): + def tst_A(self): + self.fail("Not implemented") + + def tst_B(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py new file mode 100644 index 000000000000..c9a76c07f933 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py @@ -0,0 +1,18 @@ +import unittest + +class Tst_test2(unittest.TestCase): + def tst_A2(self): + self.fail("Not implemented") + + def tst_B2(self): + self.assertEqual(1,1,'Not equal') + + def tst_C2(self): + self.assertEqual(1,2,'Not equal') + + def tst_D2(self): + raise ArithmeticError() + pass + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/test_root.py b/src/test/pythonFiles/testFiles/noseFiles/test_root.py new file mode 100644 index 000000000000..452813e9a079 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/test_root.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_Root_test1(unittest.TestCase): + def test_Root_A(self): + self.fail("Not implemented") + + def test_Root_B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_Root_c(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py b/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py new file mode 100644 index 000000000000..734b84cd342e --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py @@ -0,0 +1,13 @@ +import unittest + + +class Test_test3(unittest.TestCase): + def test4A(self): + self.fail("Not implemented") + + def test4B(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py new file mode 100644 index 000000000000..e869986b6ead --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_test1(unittest.TestCase): + def test_A(self): + self.fail("Not implemented") + + def test_B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_c(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py new file mode 100644 index 000000000000..ad89d873e879 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py @@ -0,0 +1,32 @@ +import unittest + +class Test_test2(unittest.TestCase): + def test_A2(self): + self.fail("Not implemented") + + def test_B2(self): + self.assertEqual(1,1,'Not equal') + + def test_C2(self): + self.assertEqual(1,2,'Not equal') + + def test_D2(self): + raise ArithmeticError() + pass + +class Test_test2a(unittest.TestCase): + def test_222A2(self): + self.fail("Not implemented") + + def test_222B2(self): + self.assertEqual(1,1,'Not equal') + + class Test_test2a1(unittest.TestCase): + def test_222A2wow(self): + self.fail("Not implemented") + + def test_222B2wow(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py b/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py new file mode 100644 index 000000000000..507e6af02063 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py @@ -0,0 +1,13 @@ +import unittest + + +class Test_test3(unittest.TestCase): + def test_A(self): + self.fail("Not implemented") + + def test_B(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/standard/.cache/v/cache/lastfailed b/src/test/pythonFiles/testFiles/standard/.cache/v/cache/lastfailed deleted file mode 100644 index 5ca32881aadf..000000000000 --- a/src/test/pythonFiles/testFiles/standard/.cache/v/cache/lastfailed +++ /dev/null @@ -1,3 +0,0 @@ -{ - "test_root.py::Test_Root_test1::test_Root_A": true -} \ No newline at end of file diff --git a/src/test/refactor/extension.refactor.extract.method.test.ts b/src/test/refactor/extension.refactor.extract.method.test.ts index f3cd520f608c..436c1f8f519f 100644 --- a/src/test/refactor/extension.refactor.extract.method.test.ts +++ b/src/test/refactor/extension.refactor.extract.method.test.ts @@ -4,9 +4,9 @@ import * as assert from 'assert'; // as well as import your extension to test it import * as vscode from 'vscode'; import * as path from 'path'; -import * as settings from '../../client/common/configSettings'; import * as fs from 'fs-extra'; -import { initialize, closeActiveWindows, IS_TRAVIS, wait } from './../initialize'; +import { PythonSettings } from '../../client/common/configSettings'; +import { initialize, closeActiveWindows, IS_TRAVIS, wait, initializeTest } from './../initialize'; import { Position } from 'vscode'; import { extractMethod } from '../../client/providers/simpleRefactorProvider'; import { RefactorProxy } from '../../client/refactor/proxy'; @@ -14,7 +14,6 @@ import { getTextEditsFromPatch } from '../../client/common/editor'; import { MockOutputChannel } from './../mockClasses'; const EXTENSION_DIR = path.join(__dirname, '..', '..', '..'); -const pythonSettings = settings.PythonSettings.getInstance(); const refactorSourceFile = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'refactoring', 'standAlone', 'refactor.py'); const refactorTargetFile = path.join(__dirname, '..', '..', '..', 'out', 'test', 'pythonFiles', 'refactoring', 'standAlone', 'refactor.py'); @@ -41,7 +40,7 @@ suite('Method Extraction', () => { fs.unlinkSync(refactorTargetFile); } fs.copySync(refactorSourceFile, refactorTargetFile, { overwrite: true }); - await closeActiveWindows(); + await initializeTest(); (vscode).commands.executeCommand = (cmd) => Promise.resolve(); }); teardown(() => { @@ -49,7 +48,8 @@ suite('Method Extraction', () => { return closeActiveWindows(); }); - function testingMethodExtraction(shouldError: boolean, pythonSettings: settings.IPythonSettings, startPos: Position, endPos: Position) { + function testingMethodExtraction(shouldError: boolean, startPos: Position, endPos: Position) { + const pythonSettings = PythonSettings.getInstance(vscode.Uri.file(refactorTargetFile)); let rangeOfTextToExtract = new vscode.Range(startPos, endPos); let proxy = new RefactorProxy(EXTENSION_DIR, pythonSettings, path.dirname(refactorTargetFile)); let expectedTextEdits: vscode.TextEdit[]; @@ -61,7 +61,7 @@ suite('Method Extraction', () => { mockTextDoc = textDocument; expectedTextEdits = getTextEditsFromPatch(textDocument.getText(), DIFF); resolve(); - }, error => reject(error)) + }, error => reject(error)); }) .then(() => proxy.extractMethod(mockTextDoc, 'myNewMethod', refactorTargetFile, rangeOfTextToExtract, options)) .then(response => { @@ -93,16 +93,16 @@ suite('Method Extraction', () => { test('Extract Method', done => { let startPos = new vscode.Position(239, 0); let endPos = new vscode.Position(241, 35); - testingMethodExtraction(false, pythonSettings, startPos, endPos).then(() => done(), done); + testingMethodExtraction(false, startPos, endPos).then(() => done(), done); }); test('Extract Method will fail if complete statements are not selected', done => { let startPos = new vscode.Position(239, 30); let endPos = new vscode.Position(241, 35); - testingMethodExtraction(true, pythonSettings, startPos, endPos).then(() => done(), done); + testingMethodExtraction(true, startPos, endPos).then(() => done(), done); }); - function testingMethodExtractionEndToEnd(shouldError: boolean, pythonSettings: settings.IPythonSettings, startPos: Position, endPos: Position) { + function testingMethodExtractionEndToEnd(shouldError: boolean, startPos: Position, endPos: Position) { let ch = new MockOutputChannel('Python'); let textDocument: vscode.TextDocument; let textEditor: vscode.TextEditor; @@ -119,7 +119,7 @@ suite('Method Extraction', () => { textEditor = editor; return; }).then(() => { - return extractMethod(EXTENSION_DIR, textEditor, rangeOfTextToExtract, ch, path.dirname(refactorTargetFile), pythonSettings).then(() => { + return extractMethod(EXTENSION_DIR, textEditor, rangeOfTextToExtract, ch).then(() => { if (shouldError) { ignoreErrorHandling = true; assert.fail('No error', 'Error', 'Extraction should fail with an error', ''); @@ -161,13 +161,13 @@ suite('Method Extraction', () => { test('Extract Method (end to end)', done => { let startPos = new vscode.Position(239, 0); let endPos = new vscode.Position(241, 35); - testingMethodExtractionEndToEnd(false, pythonSettings, startPos, endPos).then(() => done(), done); + testingMethodExtractionEndToEnd(false, startPos, endPos).then(() => done(), done); }); } test('Extract Method will fail if complete statements are not selected', done => { let startPos = new vscode.Position(239, 30); let endPos = new vscode.Position(241, 35); - testingMethodExtractionEndToEnd(true, pythonSettings, startPos, endPos).then(() => done(), done); + testingMethodExtractionEndToEnd(true, startPos, endPos).then(() => done(), done); }); }); diff --git a/src/test/refactor/extension.refactor.extract.var.test.ts b/src/test/refactor/extension.refactor.extract.var.test.ts index 7428a6b1d43d..06144c333e18 100644 --- a/src/test/refactor/extension.refactor.extract.var.test.ts +++ b/src/test/refactor/extension.refactor.extract.var.test.ts @@ -4,9 +4,9 @@ import * as assert from 'assert'; // as well as import your extension to test it import * as vscode from 'vscode'; import * as path from 'path'; -import * as settings from '../../client/common/configSettings'; import * as fs from 'fs-extra'; -import { initialize, closeActiveWindows, IS_TRAVIS, wait } from './../initialize'; +import { PythonSettings } from '../../client/common/configSettings'; +import { closeActiveWindows, initialize, initializeTest, IS_TRAVIS, wait } from './../initialize'; import { Position } from 'vscode'; import { extractVariable } from '../../client/providers/simpleRefactorProvider'; import { RefactorProxy } from '../../client/refactor/proxy'; @@ -14,7 +14,6 @@ import { getTextEditsFromPatch } from '../../client/common/editor'; import { MockOutputChannel } from './../mockClasses'; const EXTENSION_DIR = path.join(__dirname, '..', '..', '..'); -const pythonSettings = settings.PythonSettings.getInstance(); const refactorSourceFile = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'refactoring', 'standAlone', 'refactor.py'); const refactorTargetFile = path.join(__dirname, '..', '..', '..', 'out', 'test', 'pythonFiles', 'refactoring', 'standAlone', 'refactor.py'); @@ -26,9 +25,9 @@ suite('Variable Extraction', () => { // Hack hac hack const oldExecuteCommand = vscode.commands.executeCommand; const options: vscode.TextEditorOptions = { cursorStyle: vscode.TextEditorCursorStyle.Line, insertSpaces: true, lineNumbers: vscode.TextEditorLineNumbersStyle.Off, tabSize: 4 }; - suiteSetup(done => { + suiteSetup(async () => { fs.copySync(refactorSourceFile, refactorTargetFile, { overwrite: true }); - initialize().then(() => done(), () => done()); + await initialize(); }); suiteTeardown(() => { vscode.commands.executeCommand = oldExecuteCommand; @@ -40,7 +39,7 @@ suite('Variable Extraction', () => { fs.unlinkSync(refactorTargetFile); } fs.copySync(refactorSourceFile, refactorTargetFile, { overwrite: true }); - await closeActiveWindows(); + await initializeTest(); (vscode).commands.executeCommand = (cmd) => Promise.resolve(); }); teardown(() => { @@ -48,7 +47,8 @@ suite('Variable Extraction', () => { return closeActiveWindows(); }); - function testingVariableExtraction(shouldError: boolean, pythonSettings: settings.IPythonSettings, startPos: Position, endPos: Position) { + function testingVariableExtraction(shouldError: boolean, startPos: Position, endPos: Position) { + const pythonSettings = PythonSettings.getInstance(vscode.Uri.file(refactorTargetFile)); let rangeOfTextToExtract = new vscode.Range(startPos, endPos); let proxy = new RefactorProxy(EXTENSION_DIR, pythonSettings, path.dirname(refactorTargetFile)); let expectedTextEdits: vscode.TextEdit[]; @@ -92,16 +92,16 @@ suite('Variable Extraction', () => { test('Extract Variable', done => { let startPos = new vscode.Position(234, 29); let endPos = new vscode.Position(234, 38); - testingVariableExtraction(false, pythonSettings, startPos, endPos).then(() => done(), done); + testingVariableExtraction(false, startPos, endPos).then(() => done(), done); }); test('Extract Variable fails if whole string not selected', done => { let startPos = new vscode.Position(234, 20); let endPos = new vscode.Position(234, 38); - testingVariableExtraction(true, pythonSettings, startPos, endPos).then(() => done(), done); + testingVariableExtraction(true, startPos, endPos).then(() => done(), done); }); - function testingVariableExtractionEndToEnd(shouldError: boolean, pythonSettings: settings.IPythonSettings, startPos: Position, endPos: Position) { + function testingVariableExtractionEndToEnd(shouldError: boolean, startPos: Position, endPos: Position) { let ch = new MockOutputChannel('Python'); let textDocument: vscode.TextDocument; let textEditor: vscode.TextEditor; @@ -117,7 +117,7 @@ suite('Variable Extraction', () => { textEditor = editor; return; }).then(() => { - return extractVariable(EXTENSION_DIR, textEditor, rangeOfTextToExtract, ch, path.dirname(refactorTargetFile), pythonSettings).then(() => { + return extractVariable(EXTENSION_DIR, textEditor, rangeOfTextToExtract, ch).then(() => { if (shouldError) { ignoreErrorHandling = true; assert.fail('No error', 'Error', 'Extraction should fail with an error', ''); @@ -160,13 +160,13 @@ suite('Variable Extraction', () => { test('Extract Variable (end to end)', done => { let startPos = new vscode.Position(234, 29); let endPos = new vscode.Position(234, 38); - testingVariableExtractionEndToEnd(false, pythonSettings, startPos, endPos).then(() => done(), done); + testingVariableExtractionEndToEnd(false, startPos, endPos).then(() => done(), done); }); } test('Extract Variable fails if whole string not selected (end to end)', done => { let startPos = new vscode.Position(234, 20); let endPos = new vscode.Position(234, 38); - testingVariableExtractionEndToEnd(true, pythonSettings, startPos, endPos).then(() => done(), done); + testingVariableExtractionEndToEnd(true, startPos, endPos).then(() => done(), done); }); }); diff --git a/src/test/standardTest.ts b/src/test/standardTest.ts new file mode 100644 index 000000000000..ee31d40bb142 --- /dev/null +++ b/src/test/standardTest.ts @@ -0,0 +1,8 @@ +import * as path from 'path'; + +process.env.CODE_TESTS_WORKSPACE = path.join(__dirname, '..', '..', 'src', 'test'); + +function start() { + require('../../node_modules/vscode/bin/test'); +} +start(); diff --git a/src/test/unittests/debugger.test.ts b/src/test/unittests/debugger.test.ts new file mode 100644 index 000000000000..c908a7f7080e --- /dev/null +++ b/src/test/unittests/debugger.test.ts @@ -0,0 +1,197 @@ +import { assert, expect, should, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { ConfigurationTarget } from 'vscode'; +import { createDeferred } from '../../client/common/helpers'; +import { BaseTestManager } from '../../client/unittests/common/baseTestManager'; +import { CANCELLATION_REASON } from '../../client/unittests/common/constants'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, TestsToRun } from '../../client/unittests/common/types'; +import { TestResultDisplay } from '../../client/unittests/display/main'; +import { TestManager as NosetestManager } from '../../client/unittests/nosetest/main'; +import { TestManager as PytestManager } from '../../client/unittests/pytest/main'; +import { TestManager as UnitTestManager } from '../../client/unittests/unittest/main'; +import { deleteDirectory, rootWorkspaceUri, updateSetting } from '../common'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; +import { MockOutputChannel } from './../mockClasses'; +import { MockDebugLauncher } from './mocks'; + +use(chaiAsPromised); + +const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'debuggerTest'); +const defaultUnitTestArgs = [ + '-v', + '-s', + '.', + '-p', + '*test*.py' +]; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests Debugging', () => { + let testManager: BaseTestManager; + let testResultDisplay: TestResultDisplay; + let outChannel: MockOutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let mockDebugLauncher: MockDebugLauncher; + let testsHelper: ITestsHelper; + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + suiteSetup(async function () { + // Test disvovery is where the delay is, hence give 10 seconds (as we discover tests at least twice in each test). + // tslint:disable-next-line:no-invalid-this + this.timeout(10000); + await initialize(); + await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); + await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); + }); + setup(async () => { + outChannel = new MockOutputChannel('Python Test Log'); + testResultDisplay = new TestResultDisplay(outChannel); + await deleteDirectory(path.join(testFilesPath, '.cache')); + await initializeTest(); + }); + teardown(async () => { + await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); + await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); + outChannel.dispose(); + mockDebugLauncher.dispose(); + if (testManager) { + testManager.dispose(); + } + testResultDisplay.dispose(); + }); + + function createTestManagerDepedencies() { + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + mockDebugLauncher = new MockDebugLauncher(); + } + + async function testStartingDebugger() { + const tests = await testManager.discoverTests(true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + + const testFunction = [tests.testFunctions[0].testFunction]; + testManager.runTest({ testFunction }, false, true); + const launched = await mockDebugLauncher.launched; + assert.isTrue(launched, 'Debugger not launched'); + } + + test('Debugger should start (unittest)', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new UnitTestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStartingDebugger(); + }); + + test('Debugger should start (pytest)', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new PytestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStartingDebugger(); + }); + + test('Debugger should start (nosetest)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new NosetestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStartingDebugger(); + }); + + async function testStoppingDebugger() { + const tests = await testManager.discoverTests(true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + + const testFunction = [tests.testFunctions[0].testFunction]; + const runningPromise = testManager.runTest({ testFunction }, false, true); + const launched = await mockDebugLauncher.launched; + assert.isTrue(launched, 'Debugger not launched'); + + const discoveryPromise = testManager.discoverTests(true, true, true); + + expect(runningPromise).eventually.throws(CANCELLATION_REASON, 'Incorrect reason for ending the debugger'); + } + + test('Debugger should stop when user invokes a test discovery (unittest)', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new UnitTestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStoppingDebugger(); + }); + + test('Debugger should stop when user invokes a test discovery (pytest)', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new PytestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStoppingDebugger(); + }); + + test('Debugger should stop when user invokes a test discovery (nosetest)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new NosetestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStoppingDebugger(); + }); + + async function testDebuggerWhenRediscoveringTests() { + const tests = await testManager.discoverTests(true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + + const testFunction = [tests.testFunctions[0].testFunction]; + const runningPromise = testManager.runTest({ testFunction }, false, true); + const launched = await mockDebugLauncher.launched; + assert.isTrue(launched, 'Debugger not launched'); + + const discoveryPromise = testManager.discoverTests(false, true); + const deferred = createDeferred(); + + discoveryPromise + .then(() => deferred.resolve('')) + .catch(ex => deferred.reject(ex)); + + // This promise should never resolve nor reject. + runningPromise + .then(() => 'Debugger stopped when it shouldn\'t have') + .catch(() => 'Debugger crashed when it shouldn\'t have') + .then(error => { + deferred.reject(error); + }); + + // Should complete without any errors + await deferred.promise; + } + + test('Debugger should not stop when test discovery is invoked automatically by extension (unittest)', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new UnitTestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testDebuggerWhenRediscoveringTests(); + }); + + test('Debugger should not stop when test discovery is invoked automatically by extension (pytest)', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new PytestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testDebuggerWhenRediscoveringTests(); + }); + + test('Debugger should not stop when test discovery is invoked automatically by extension (nosetest)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new NosetestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testDebuggerWhenRediscoveringTests(); + }); +}); diff --git a/src/test/unittests/mocks.ts b/src/test/unittests/mocks.ts new file mode 100644 index 000000000000..036c0ffa14d1 --- /dev/null +++ b/src/test/unittests/mocks.ts @@ -0,0 +1,40 @@ +import { CancellationToken, Disposable, OutputChannel } from 'vscode'; +import { createDeferred, Deferred } from '../../client/common/helpers'; +import { ITestDebugLauncher, Tests } from '../../client/unittests/common/types'; + +export class MockDebugLauncher implements ITestDebugLauncher, Disposable { + public get launched(): Promise { + return this._launched.promise; + } + public get debuggerPromise(): Deferred { + return this._promise; + } + public get cancellationToken(): CancellationToken { + return this._token; + } + // tslint:disable-next-line:variable-name + private _launched: Deferred; + // tslint:disable-next-line:variable-name + private _promise?: Deferred; + // tslint:disable-next-line:variable-name + private _token: CancellationToken; + constructor() { + this._launched = createDeferred(); + } + public async launchDebugger(rootDirectory: string, testArgs: string[], token?: CancellationToken, outChannel?: OutputChannel): Promise { + this._launched.resolve(true); + // tslint:disable-next-line:no-non-null-assertion + this._token = token!; + this._promise = createDeferred(); + // tslint:disable-next-line:no-non-null-assertion + token!.onCancellationRequested(() => { + if (this._promise) { + this._promise.reject('Mock-User Cancelled'); + } + }); + return this._promise.promise; + } + public dispose() { + this._promise = undefined; + } +} diff --git a/src/test/unittests/nosetest.test.ts b/src/test/unittests/nosetest.test.ts index 26f4bd39a0ea..9105c483fec3 100644 --- a/src/test/unittests/nosetest.test.ts +++ b/src/test/unittests/nosetest.test.ts @@ -1,139 +1,162 @@ import * as assert from 'assert'; -import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; -import * as configSettings from '../../client/common/configSettings'; -import * as nose from '../../client/unittests/nosetest/main'; -import { initialize } from './../initialize'; -import { TestsToRun } from '../../client/unittests/common/contracts'; +import * as vscode from 'vscode'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../../client/unittests/common/types'; import { TestResultDisplay } from '../../client/unittests/display/main'; +import * as nose from '../../client/unittests/nosetest/main'; +import { rootWorkspaceUri, updateSetting } from '../common'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; import { MockOutputChannel } from './../mockClasses'; +import { MockDebugLauncher } from './mocks'; -const pythonSettings = configSettings.PythonSettings.getInstance(); -const UNITTEST_TEST_FILES_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); +const UNITTEST_TEST_FILES_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'noseFiles'); const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'single'); -const filesToDelete = [path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'standard', '.noseids'), -path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'cwd', 'src', '.noseids')]; -const unitTestTestFilesCwdPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'cwd', 'src'); -const originalArgs = pythonSettings.unitTest.nosetestArgs; +const filesToDelete = [ + path.join(UNITTEST_TEST_FILES_PATH, '.noseids'), + path.join(UNITTEST_SINGLE_TEST_FILE_PATH, '.noseids') +]; +// tslint:disable-next-line:max-func-body-length suite('Unit Tests (nosetest)', () => { + const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; + const rootDirectory = UNITTEST_TEST_FILES_PATH; + let testManager: nose.TestManager; + let testResultDisplay: TestResultDisplay; + let outChannel: vscode.OutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let testsHelper: ITestsHelper; + suiteSetup(async () => { filesToDelete.forEach(file => { if (fs.existsSync(file)) { fs.unlinkSync(file); } }); + await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); await initialize(); }); - suiteTeardown(() => { + suiteTeardown(async () => { + await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); filesToDelete.forEach(file => { if (fs.existsSync(file)) { fs.unlinkSync(file); } }); }); - setup(() => { - pythonSettings.unitTest.nosetestArgs = originalArgs; + setup(async () => { outChannel = new MockOutputChannel('Python Test Log'); testResultDisplay = new TestResultDisplay(outChannel); + await initializeTest(); }); - teardown(() => { + teardown(async () => { outChannel.dispose(); testManager.dispose(); testResultDisplay.dispose(); + await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); }); function createTestManager(rootDir: string = rootDirectory) { - testManager = new nose.TestManager(rootDir, outChannel); + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new nose.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper, new MockDebugLauncher()); } - const rootDirectory = UNITTEST_TEST_FILES_PATH; - let testManager: nose.TestManager; - let testResultDisplay: TestResultDisplay; - let outChannel: vscode.OutputChannel; test('Discover Tests (single test file)', async () => { - pythonSettings.unitTest.nosetestArgs = []; - testManager = new nose.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel); - const tests = await testManager.discoverTests(true, true) + createTestManager(UNITTEST_SINGLE_TEST_FILE_PATH); + const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_one.py') && t.nameToRun === t.name), true, 'Test File not found'); }); - test('Check that nameToRun in testSuits has class name after : (single test file)', async () => { - pythonSettings.unitTest.nosetestArgs = []; - testManager = new nose.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel); + test('Check that nameToRun in testSuites has class name after : (single test file)', async () => { + createTestManager(UNITTEST_SINGLE_TEST_FILE_PATH); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); - assert.equal(tests.testSuits.every(t => t.testSuite.name === t.testSuite.nameToRun.split(":")[1]), true, 'Suite name does not match class name'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.every(t => t.testSuite.name === t.testSuite.nameToRun.split(':')[1]), true, 'Suite name does not match class name'); }); - test('Discover Tests (pattern = test_)', async () => { - pythonSettings.unitTest.nosetestArgs = []; + function lookForTestFile(tests: Tests, testFile: string) { + const found = tests.testFiles.some(t => t.name === testFile && t.nameToRun === t.name); + assert.equal(found, true, `Test File not found '${testFile}'`); + } + test('Discover Tests (-m=test)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); - assert.equal(tests.testFiles.length, 6, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 22, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 6, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_unittest_one.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_unittest_two.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_pytest.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_another_pytest.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'unittest_three_test.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); + assert.equal(tests.testFiles.length, 5, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 16, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 6, 'Incorrect number of test suites'); + lookForTestFile(tests, path.join('tests', 'test_unittest_one.py')); + lookForTestFile(tests, path.join('tests', 'test_unittest_two.py')); + lookForTestFile(tests, path.join('tests', 'unittest_three_test.py')); + lookForTestFile(tests, path.join('tests', 'test4.py')); + lookForTestFile(tests, 'test_root.py'); }); - test('Discover Tests (pattern = _test_)', async () => { - pythonSettings.unitTest.nosetestArgs = [ - '-m=*test*' - ]; + test('Discover Tests (-w=specific -m=tst)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-w', 'specific', '-m', 'tst'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); - assert.equal(tests.testFiles.length, 6, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 18, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 5, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_unittest_one.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_unittest_two.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_pytest.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_another_pytest.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'unittest_three_test.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + lookForTestFile(tests, path.join('specific', 'tst_unittest_one.py')); + lookForTestFile(tests, path.join('specific', 'tst_unittest_two.py')); + }); + + test('Discover Tests (-m=test_)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test_'], rootWorkspaceUri, configTarget); + createTestManager(); + const tests = await testManager.discoverTests(true, true); + assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); + lookForTestFile(tests, 'test_root.py'); }); test('Run Tests', async () => { - pythonSettings.unitTest.nosetestArgs = []; + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); const results = await testManager.runTest(); - assert.equal(results.summary.errors, 5, 'Errors'); - assert.equal(results.summary.failures, 6, 'Failures'); - assert.equal(results.summary.passed, 8, 'Passed'); - assert.equal(results.summary.skipped, 3, 'skipped'); + assert.equal(results.summary.errors, 1, 'Errors'); + assert.equal(results.summary.failures, 7, 'Failures'); + assert.equal(results.summary.passed, 6, 'Passed'); + assert.equal(results.summary.skipped, 2, 'skipped'); }); test('Run Failed Tests', async () => { - pythonSettings.unitTest.nosetestArgs = []; + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); let results = await testManager.runTest(); - assert.equal(results.summary.errors, 5, 'Errors'); - assert.equal(results.summary.failures, 6, 'Failures'); - assert.equal(results.summary.passed, 8, 'Passed'); - assert.equal(results.summary.skipped, 3, 'skipped'); + assert.equal(results.summary.errors, 1, 'Errors'); + assert.equal(results.summary.failures, 7, 'Failures'); + assert.equal(results.summary.passed, 6, 'Passed'); + assert.equal(results.summary.skipped, 2, 'skipped'); - results = await testManager.runTest(true); - assert.equal(results.summary.errors, 5, 'Errors again'); - assert.equal(results.summary.failures, 6, 'Failures again'); + results = await testManager.runTest(undefined, true); + assert.equal(results.summary.errors, 1, 'Errors again'); + assert.equal(results.summary.failures, 7, 'Failures again'); assert.equal(results.summary.passed, 0, 'Passed again'); assert.equal(results.summary.skipped, 0, 'skipped again'); }); test('Run Specific Test File', async () => { - pythonSettings.unitTest.nosetestArgs = []; + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); - const testFile: TestsToRun = { testFile: [tests.testFiles[0]], testFolder: [], testFunction: [], testSuite: [] }; + const testFileToRun = tests.testFiles.find(t => t.fullPath.endsWith('test_root.py')); + assert.ok(testFileToRun, 'Test file not found'); + // tslint:disable-next-line:no-non-null-assertion + const testFile: TestsToRun = { testFile: [testFileToRun!], testFolder: [], testFunction: [], testSuite: [] }; const results = await testManager.runTest(testFile); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 1, 'Failures'); @@ -142,10 +165,13 @@ suite('Unit Tests (nosetest)', () => { }); test('Run Specific Test Suite', async () => { - pythonSettings.unitTest.nosetestArgs = []; + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); - const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [tests.testSuits[0].testSuite] }; + const testSuiteToRun = tests.testSuites.find(s => s.xmlClassName === 'test_root.Test_Root_test1'); + assert.ok(testSuiteToRun, 'Test suite not found'); + // tslint:disable-next-line:no-non-null-assertion + const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [testSuiteToRun!.testSuite] }; const results = await testManager.runTest(testSuite); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 1, 'Failures'); @@ -154,25 +180,17 @@ suite('Unit Tests (nosetest)', () => { }); test('Run Specific Test Function', async () => { - pythonSettings.unitTest.nosetestArgs = []; + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); - const testFn: TestsToRun = { testFile: [], testFolder: [], testFunction: [tests.testFunctions[0].testFunction], testSuite: [] }; + const testFnToRun = tests.testFunctions.find(f => f.xmlClassName === 'test_root.Test_Root_test1'); + assert.ok(testFnToRun, 'Test function not found'); + // tslint:disable-next-line:no-non-null-assertion + const testFn: TestsToRun = { testFile: [], testFolder: [], testFunction: [testFnToRun!.testFunction], testSuite: [] }; const results = await testManager.runTest(testFn); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 1, 'Failures'); assert.equal(results.summary.passed, 0, 'Passed'); assert.equal(results.summary.skipped, 0, 'skipped'); }); - - test('Setting cwd should return tests', async () => { - pythonSettings.unitTest.nosetestArgs = ['tests']; - createTestManager(unitTestTestFilesCwdPath); - - const tests = await testManager.discoverTests(true, true); - assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); - assert.equal(tests.testFolders.length, 1, 'Incorrect number of test folders'); - assert.equal(tests.testFunctions.length, 1, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 1, 'Incorrect number of test suites'); - }); }); diff --git a/src/test/unittests/pytest.test.ts b/src/test/unittests/pytest.test.ts index 4d7dcdcbc664..cf0ba4130ca8 100644 --- a/src/test/unittests/pytest.test.ts +++ b/src/test/unittests/pytest.test.ts @@ -1,60 +1,75 @@ import * as assert from 'assert'; -import * as vscode from 'vscode'; -import * as pytest from '../../client/unittests/pytest/main'; import * as path from 'path'; -import * as configSettings from '../../client/common/configSettings'; -import { initialize } from './../initialize'; -import { TestsToRun, TestFile } from '../../client/unittests/common/contracts'; +import * as vscode from 'vscode'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, TestFile, TestsToRun } from '../../client/unittests/common/types'; import { TestResultDisplay } from '../../client/unittests/display/main'; +import * as pytest from '../../client/unittests/pytest/main'; +import { rootWorkspaceUri, updateSetting } from '../common'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; import { MockOutputChannel } from './../mockClasses'; +import { MockDebugLauncher } from './mocks'; -const pythonSettings = configSettings.PythonSettings.getInstance(); const UNITTEST_TEST_FILES_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'single'); const UNITTEST_TEST_FILES_PATH_WITH_CONFIGS = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'unitestsWithConfigs'); const unitTestTestFilesCwdPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'cwd', 'src'); +// tslint:disable-next-line:max-func-body-length suite('Unit Tests (PyTest)', () => { - suiteSetup(() => initialize()); - setup(() => { + let rootDirectory = UNITTEST_TEST_FILES_PATH; + let testManager: pytest.TestManager; + let testResultDisplay: TestResultDisplay; + let outChannel: vscode.OutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let testsHelper: ITestsHelper; + const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; + suiteSetup(async () => { + await initialize(); + await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); + }); + setup(async () => { rootDirectory = UNITTEST_TEST_FILES_PATH; outChannel = new MockOutputChannel('Python Test Log'); testResultDisplay = new TestResultDisplay(outChannel); + await initializeTest(); }); - teardown(() => { + teardown(async () => { outChannel.dispose(); testManager.dispose(); testResultDisplay.dispose(); + await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); }); function createTestManager(rootDir: string = rootDirectory) { - testManager = new pytest.TestManager(rootDir, outChannel); + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new pytest.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper, new MockDebugLauncher()); } - let rootDirectory = UNITTEST_TEST_FILES_PATH; - let testManager: pytest.TestManager; - let testResultDisplay: TestResultDisplay; - let outChannel: vscode.OutputChannel; test('Discover Tests (single test file)', async () => { - pythonSettings.unitTest.nosetestArgs = [ - ]; - testManager = new pytest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel); + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new pytest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel, storageService, resultsService, testsHelper, new MockDebugLauncher()); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); assert.equal(tests.testFiles.some(t => t.name === 'tests/test_one.py' && t.nameToRun === t.name), true, 'Test File not found'); assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); }); test('Discover Tests (pattern = test_)', async () => { - pythonSettings.unitTest.pyTestArgs = [ - '-k=test_' - ]; + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 6, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 29, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 8, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 8, 'Incorrect number of test suites'); assert.equal(tests.testFiles.some(t => t.name === 'tests/test_unittest_one.py' && t.nameToRun === t.name), true, 'Test File not found'); assert.equal(tests.testFiles.some(t => t.name === 'tests/test_unittest_two.py' && t.nameToRun === t.name), true, 'Test File not found'); assert.equal(tests.testFiles.some(t => t.name === 'tests/unittest_three_test.py' && t.nameToRun === t.name), true, 'Test File not found'); @@ -64,34 +79,29 @@ suite('Unit Tests (PyTest)', () => { }); test('Discover Tests (pattern = _test)', async () => { - pythonSettings.unitTest.pyTestArgs = [ - '-k=_test.py' - ]; + await updateSetting('unitTest.pyTestArgs', ['-k=_test.py'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 1, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); assert.equal(tests.testFiles.some(t => t.name === 'tests/unittest_three_test.py' && t.nameToRun === t.name), true, 'Test File not found'); }); - test('Discover Tests (with config)', async () => { - pythonSettings.unitTest.pyTestArgs = []; + await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); rootDirectory = UNITTEST_TEST_FILES_PATH_WITH_CONFIGS; createTestManager(); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 14, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 4, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 4, 'Incorrect number of test suites'); assert.equal(tests.testFiles.some(t => t.name === 'other/test_unittest_one.py' && t.nameToRun === t.name), true, 'Test File not found'); assert.equal(tests.testFiles.some(t => t.name === 'other/test_pytest.py' && t.nameToRun === t.name), true, 'Test File not found'); }); test('Run Tests', async () => { - pythonSettings.unitTest.pyTestArgs = [ - '-k=test_' - ]; + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); const results = await testManager.runTest(); assert.equal(results.summary.errors, 0, 'Errors'); @@ -101,17 +111,15 @@ suite('Unit Tests (PyTest)', () => { }); test('Run Failed Tests', async () => { - pythonSettings.unitTest.pyTestArgs = [ - '-k=test_' - ]; + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); - let results = await testManager.runTest() + let results = await testManager.runTest(); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 9, 'Failures'); assert.equal(results.summary.passed, 17, 'Passed'); assert.equal(results.summary.skipped, 3, 'skipped'); - results = await testManager.runTest(true); + results = await testManager.runTest(undefined, true); assert.equal(results.summary.errors, 0, 'Failed Errors'); assert.equal(results.summary.failures, 9, 'Failed Failures'); assert.equal(results.summary.passed, 0, 'Failed Passed'); @@ -119,9 +127,7 @@ suite('Unit Tests (PyTest)', () => { }); test('Run Specific Test File', async () => { - pythonSettings.unitTest.pyTestArgs = [ - '-k=test_' - ]; + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); await testManager.discoverTests(true, true); const testFile: TestFile = { @@ -142,13 +148,11 @@ suite('Unit Tests (PyTest)', () => { }); test('Run Specific Test Suite', async () => { - pythonSettings.unitTest.pyTestArgs = [ - '-k=test_' - ]; + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); - const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [tests.testSuits[0].testSuite] }; - const results = await testManager.runTest(testSuite) + const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [tests.testSuites[0].testSuite] }; + const results = await testManager.runTest(testSuite); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 1, 'Failures'); assert.equal(results.summary.passed, 1, 'Passed'); @@ -156,9 +160,7 @@ suite('Unit Tests (PyTest)', () => { }); test('Run Specific Test Function', async () => { - pythonSettings.unitTest.pyTestArgs = [ - '-k=test_' - ]; + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); const testFn: TestsToRun = { testFile: [], testFolder: [], testFunction: [tests.testFunctions[0].testFunction], testSuite: [] }; @@ -169,18 +171,14 @@ suite('Unit Tests (PyTest)', () => { assert.equal(results.summary.skipped, 0, 'skipped'); }); - test('Setting cwd should return tests', async () => { - pythonSettings.unitTest.unittestArgs = [ - '-s=./tests', - '-p=test*.py' - ]; + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(unitTestTestFilesCwdPath); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFolders.length, 1, 'Incorrect number of test folders'); assert.equal(tests.testFunctions.length, 1, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 1, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); }); }); diff --git a/src/test/unittests/rediscover.test.ts b/src/test/unittests/rediscover.test.ts new file mode 100644 index 000000000000..8478b9b8e14b --- /dev/null +++ b/src/test/unittests/rediscover.test.ts @@ -0,0 +1,108 @@ +import { assert } from 'chai'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { ConfigurationTarget, Position, Range, Uri, window, workspace } from 'vscode'; +import { BaseTestManager } from '../../client/unittests/common/baseTestManager'; +import { CANCELLATION_REASON } from '../../client/unittests/common/constants'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, TestsToRun } from '../../client/unittests/common/types'; +import { TestResultDisplay } from '../../client/unittests/display/main'; +import { TestManager as NosetestManager } from '../../client/unittests/nosetest/main'; +import { TestManager as PytestManager } from '../../client/unittests/pytest/main'; +import { TestManager as UnitTestManager } from '../../client/unittests/unittest/main'; +import { deleteDirectory, deleteFile, rootWorkspaceUri, updateSetting } from '../common'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; +import { MockOutputChannel } from './../mockClasses'; +import { MockDebugLauncher } from './mocks'; + +const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'debuggerTest'); +const testFile = path.join(testFilesPath, 'tests', 'test_debugger_two.py'); +const testFileWithFewTests = path.join(testFilesPath, 'tests', 'test_debugger_two.txt'); +const testFileWithMoreTests = path.join(testFilesPath, 'tests', 'test_debugger_two.updated.txt'); +const defaultUnitTestArgs = [ + '-v', + '-s', + '.', + '-p', + '*test*.py' +]; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests Discovery', () => { + let testManager: BaseTestManager; + let testResultDisplay: TestResultDisplay; + let outChannel: MockOutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let mockDebugLauncher: MockDebugLauncher; + let testsHelper: ITestsHelper; + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + suiteSetup(async () => { + await initialize(); + }); + setup(async () => { + outChannel = new MockOutputChannel('Python Test Log'); + testResultDisplay = new TestResultDisplay(outChannel); + await fs.copy(testFileWithFewTests, testFile, { overwrite: true }); + await deleteDirectory(path.join(testFilesPath, '.cache')); + await resetSettings(); + await initializeTest(); + }); + teardown(async () => { + await resetSettings(); + await fs.copy(testFileWithFewTests, testFile, { overwrite: true }); + await deleteFile(path.join(path.dirname(testFile), `${path.basename(testFile, '.py')}.pyc`)); + outChannel.dispose(); + if (testManager) { + testManager.dispose(); + } + testResultDisplay.dispose(); + }); + + async function resetSettings() { + await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); + await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); + } + + function createTestManagerDepedencies() { + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + mockDebugLauncher = new MockDebugLauncher(); + } + + async function discoverUnitTests() { + let tests = await testManager.discoverTests(true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + await deleteFile(path.join(path.dirname(testFile), `${path.basename(testFile, '.py')}.pyc`)); + await fs.copy(testFileWithMoreTests, testFile, { overwrite: true }); + tests = await testManager.discoverTests(true, true); + assert.equal(tests.testFunctions.length, 4, 'Incorrect number of updated test functions'); + } + + test('Re-discover tests (unittest)', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new UnitTestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await discoverUnitTests(); + }); + + test('Re-discover tests (pytest)', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new PytestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await discoverUnitTests(); + }); + + test('Re-discover tests (nosetest)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new NosetestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await discoverUnitTests(); + }); +}); diff --git a/src/test/unittests/unittest.test.ts b/src/test/unittests/unittest.test.ts index 43d4fdab710f..bcf201bcc3dc 100644 --- a/src/test/unittests/unittest.test.ts +++ b/src/test/unittests/unittest.test.ts @@ -1,83 +1,103 @@ import * as assert from 'assert'; +import * as fs from 'fs-extra'; import * as path from 'path'; -import * as configSettings from '../../client/common/configSettings'; -import * as unittest from '../../client/unittests/unittest/main'; -import { initialize } from './../initialize'; -import { TestsToRun } from '../../client/unittests/common/contracts'; +import { ConfigurationTarget } from 'vscode'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, TestsToRun } from '../../client/unittests/common/types'; import { TestResultDisplay } from '../../client/unittests/display/main'; +import * as unittest from '../../client/unittests/unittest/main'; +import { rootWorkspaceUri, updateSetting } from '../common'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; import { MockOutputChannel } from './../mockClasses'; +import { MockDebugLauncher } from './mocks'; -const pythonSettings = configSettings.PythonSettings.getInstance(); const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles'); const UNITTEST_TEST_FILES_PATH = path.join(testFilesPath, 'standard'); const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(testFilesPath, 'single'); const unitTestTestFilesCwdPath = path.join(testFilesPath, 'cwd', 'src'); const unitTestSpecificTestFilesPath = path.join(testFilesPath, 'specificTest'); - +const defaultUnitTestArgs = [ + '-v', + '-s', + '.', + '-p', + '*test*.py' +]; + +// tslint:disable-next-line:max-func-body-length suite('Unit Tests (unittest)', () => { - suiteSetup(() => initialize()); - setup(() => { + let testManager: unittest.TestManager; + let testResultDisplay: TestResultDisplay; + let outChannel: MockOutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let testsHelper: ITestsHelper; + const rootDirectory = UNITTEST_TEST_FILES_PATH; + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + suiteSetup(async () => { + await initialize(); + await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + }); + setup(async () => { outChannel = new MockOutputChannel('Python Test Log'); testResultDisplay = new TestResultDisplay(outChannel); + const cachePath = path.join(UNITTEST_TEST_FILES_PATH, '.cache'); + if (await fs.pathExists(cachePath)) { + await fs.remove(cachePath); + } + await initializeTest(); }); - teardown(() => { + teardown(async () => { outChannel.dispose(); testManager.dispose(); testResultDisplay.dispose(); + await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); }); function createTestManager(rootDir: string = rootDirectory) { - testManager = new unittest.TestManager(rootDir, outChannel); + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new unittest.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper, new MockDebugLauncher()); } - const rootDirectory = UNITTEST_TEST_FILES_PATH; - let testManager: unittest.TestManager; - let testResultDisplay: TestResultDisplay; - let outChannel: MockOutputChannel; test('Discover Tests (single test file)', async () => { - pythonSettings.unitTest.unittestArgs = [ - '-s=./tests', - '-p=test_*.py' - ]; - testManager = new unittest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel); + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new unittest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel, storageService, resultsService, testsHelper, new MockDebugLauncher()); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 1, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); assert.equal(tests.testFiles.some(t => t.name === 'test_one.py' && t.nameToRun === 'Test_test1.test_A'), true, 'Test File not found'); }); test('Discover Tests', async () => { - pythonSettings.unitTest.unittestArgs = [ - '-s=./tests', - '-p=test_*.py' - ]; + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 9, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 3, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 3, 'Incorrect number of test suites'); assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_one.py' && t.nameToRun === 'Test_test1.test_A'), true, 'Test File not found'); assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_two.py' && t.nameToRun === 'Test_test2.test_A2'), true, 'Test File not found'); }); test('Discover Tests (pattern = *_test_*.py)', async () => { - pythonSettings.unitTest.unittestArgs = [ - '-s=./tests', - '-p=*_test*.py' - ]; + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=*_test*.py'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 1, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); assert.equal(tests.testFiles.some(t => t.name === 'unittest_three_test.py' && t.nameToRun === 'Test_test3.test_A'), true, 'Test File not found'); }); test('Run Tests', async () => { - pythonSettings.unitTest.unittestArgs = [ - '-v', '-s', './tests', - '-p', 'test_unittest*.py' - ]; + await updateSetting('unitTest.unittestArgs', ['-v', '-s', './tests', '-p', 'test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(); const results = await testManager.runTest(); assert.equal(results.summary.errors, 1, 'Errors'); @@ -86,27 +106,8 @@ suite('Unit Tests (unittest)', () => { assert.equal(results.summary.skipped, 1, 'skipped'); }); - // test('Fail Fast', done => { - // pythonSettings.unitTest.unittestArgs = [ - // '-s=./tests', - // '-p=*test*.py', - // '--failfast' - // ]; - // createTestManager(); - // testManager.runTest().then(results => { - // assert.equal(results.summary.errors, 1, 'Errors'); - // assert.equal(results.summary.failures, 5, 'Failures'); - // assert.equal(results.summary.passed, 4, 'Passed'); - // assert.equal(results.summary.skipped, 1, 'skipped'); - // done(); - // }).catch(done); - // }); - test('Run Failed Tests', async () => { - pythonSettings.unitTest.unittestArgs = [ - '-s=./tests', - '-p=test_unittest*.py' - ]; + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(); let results = await testManager.runTest(); assert.equal(results.summary.errors, 1, 'Errors'); @@ -114,7 +115,7 @@ suite('Unit Tests (unittest)', () => { assert.equal(results.summary.passed, 3, 'Passed'); assert.equal(results.summary.skipped, 1, 'skipped'); - results = await testManager.runTest(true); + results = await testManager.runTest(undefined, true); assert.equal(results.summary.errors, 1, 'Failed Errors'); assert.equal(results.summary.failures, 4, 'Failed Failures'); assert.equal(results.summary.passed, 0, 'Failed Passed'); @@ -122,14 +123,12 @@ suite('Unit Tests (unittest)', () => { }); test('Run Specific Test File', async () => { - pythonSettings.unitTest.unittestArgs = [ - '-s=./tests', - '-p=test_unittest*.py' - ]; + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(unitTestSpecificTestFilesPath); const tests = await testManager.discoverTests(true, true); - const testFileToTest = tests.testFiles.find(f => f.name === 'test_unittest_one.py'); + // tslint:disable-next-line:no-non-null-assertion + const testFileToTest = tests.testFiles.find(f => f.name === 'test_unittest_one.py')!; const testFile: TestsToRun = { testFile: [testFileToTest], testFolder: [], testFunction: [], testSuite: [] }; const results = await testManager.runTest(testFile); @@ -140,14 +139,12 @@ suite('Unit Tests (unittest)', () => { }); test('Run Specific Test Suite', async () => { - pythonSettings.unitTest.unittestArgs = [ - '-s=./tests', - '-p=test_unittest*.py' - ]; + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(unitTestSpecificTestFilesPath); const tests = await testManager.discoverTests(true, true); - const testSuiteToTest = tests.testSuits.find(s => s.testSuite.name === 'Test_test_one_1')!.testSuite; + // tslint:disable-next-line:no-non-null-assertion + const testSuiteToTest = tests.testSuites.find(s => s.testSuite.name === 'Test_test_one_1')!.testSuite; const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [testSuiteToTest] }; const results = await testManager.runTest(testSuite); @@ -158,10 +155,7 @@ suite('Unit Tests (unittest)', () => { }); test('Run Specific Test Function', async () => { - pythonSettings.unitTest.unittestArgs = [ - '-s=./tests', - '-p=test_unittest*.py' - ]; + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); const testFn: TestsToRun = { testFile: [], testFolder: [], testFunction: [tests.testFunctions[0].testFunction], testSuite: [] }; @@ -173,16 +167,13 @@ suite('Unit Tests (unittest)', () => { }); test('Setting cwd should return tests', async () => { - pythonSettings.unitTest.unittestArgs = [ - '-s=./tests', - '-p=test_*.py' - ]; + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); createTestManager(unitTestTestFilesCwdPath); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFolders.length, 1, 'Incorrect number of test folders'); assert.equal(tests.testFunctions.length, 1, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 1, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); }); -}); +}); diff --git a/src/test/workspaceSymbols/common.ts b/src/test/workspaceSymbols/common.ts new file mode 100644 index 000000000000..527b852ab6ad --- /dev/null +++ b/src/test/workspaceSymbols/common.ts @@ -0,0 +1,8 @@ +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; + +export async function enableDisableWorkspaceSymbols(resource: Uri, enabled: boolean, configTarget: ConfigurationTarget) { + const settings = workspace.getConfiguration('python', resource); + await settings.update('workspaceSymbols.enabled', enabled, configTarget); + PythonSettings.dispose(); +} diff --git a/src/test/workspaceSymbols/multiroot.test.ts b/src/test/workspaceSymbols/multiroot.test.ts new file mode 100644 index 000000000000..9d4de7940274 --- /dev/null +++ b/src/test/workspaceSymbols/multiroot.test.ts @@ -0,0 +1,65 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { CancellationTokenSource, ConfigurationTarget, Uri } from 'vscode'; +import { Generator } from '../../client/workspaceSymbols/generator'; +import { WorkspaceSymbolProvider } from '../../client/workspaceSymbols/provider'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { MockOutputChannel } from '../mockClasses'; +import { updateSetting } from './../common'; + +const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); + +suite('Multiroot Workspace Symbols', () => { + suiteSetup(function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + return initialize(); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + await updateSetting('workspaceSymbols.enabled', false, Uri.file(path.join(multirootPath, 'parent', 'child')), ConfigurationTarget.WorkspaceFolder); + await updateSetting('workspaceSymbols.enabled', false, Uri.file(path.join(multirootPath, 'workspace2')), ConfigurationTarget.WorkspaceFolder); + }); + + test('symbols should be returned when enabeld and vice versa', async () => { + const childWorkspaceUri = Uri.file(path.join(multirootPath, 'parent', 'child')); + const outputChannel = new MockOutputChannel('Output'); + + await updateSetting('workspaceSymbols.enabled', false, childWorkspaceUri, ConfigurationTarget.WorkspaceFolder); + + let generator = new Generator(childWorkspaceUri, outputChannel); + let provider = new WorkspaceSymbolProvider([generator], outputChannel); + let symbols = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); + assert.equal(symbols.length, 0, 'Symbols returned even when workspace symbols are turned off'); + generator.dispose(); + + await updateSetting('workspaceSymbols.enabled', true, childWorkspaceUri, ConfigurationTarget.WorkspaceFolder); + + generator = new Generator(childWorkspaceUri, outputChannel); + provider = new WorkspaceSymbolProvider([generator], outputChannel); + symbols = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); + assert.notEqual(symbols.length, 0, 'Symbols should be returned when workspace symbols are turned on'); + }); + test('symbols should be filtered correctly', async () => { + const childWorkspaceUri = Uri.file(path.join(multirootPath, 'parent', 'child')); + const workspace2Uri = Uri.file(path.join(multirootPath, 'workspace2')); + const outputChannel = new MockOutputChannel('Output'); + + await updateSetting('workspaceSymbols.enabled', true, childWorkspaceUri, ConfigurationTarget.WorkspaceFolder); + await updateSetting('workspaceSymbols.enabled', true, workspace2Uri, ConfigurationTarget.WorkspaceFolder); + + const generators = [ + new Generator(childWorkspaceUri, outputChannel), + new Generator(workspace2Uri, outputChannel)]; + const provider = new WorkspaceSymbolProvider(generators, outputChannel); + const symbols = await provider.provideWorkspaceSymbols('meth1Of', new CancellationTokenSource().token); + + assert.equal(symbols.length, 2, 'Incorrect number of symbols returned'); + assert.notEqual(symbols.findIndex(sym => sym.location.uri.fsPath.endsWith('childFile.py')), -1, 'File with symbol not found in child workspace folder'); + assert.notEqual(symbols.findIndex(sym => sym.location.uri.fsPath.endsWith('workspace2File.py')), -1, 'File with symbol not found in child workspace folder'); + }); +}); diff --git a/src/test/workspaceSymbols/standard.test.ts b/src/test/workspaceSymbols/standard.test.ts new file mode 100644 index 000000000000..b2859b0fc428 --- /dev/null +++ b/src/test/workspaceSymbols/standard.test.ts @@ -0,0 +1,74 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { CancellationTokenSource, ConfigurationTarget, Uri } from 'vscode'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { Generator } from '../../client/workspaceSymbols/generator'; +import { MockOutputChannel } from '../mockClasses'; +import { WorkspaceSymbolProvider } from '../../client/workspaceSymbols/provider'; +import { updateSetting } from './../common'; +import { PythonSettings } from '../../client/common/configSettings'; + +const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); +const configUpdateTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + +suite('Workspace Symbols', () => { + suiteSetup(() => initialize()); + suiteTeardown(() => closeActiveWindows()); + setup(() => initializeTest()); + teardown(async () => { + await closeActiveWindows(); + await updateSetting('workspaceSymbols.enabled', false, workspaceUri, configUpdateTarget); + }); + + test(`symbols should be returned when enabeld and vice versa`, async () => { + const outputChannel = new MockOutputChannel('Output'); + await updateSetting('workspaceSymbols.enabled', false, workspaceUri, configUpdateTarget); + + // The workspace will be in the output test folder + // So lets modify the settings so it sees the source test folder + let settings = PythonSettings.getInstance(workspaceUri); + settings.workspaceSymbols.tagFilePath = path.join(workspaceUri.fsPath, '.vscode', 'tags') + + let generator = new Generator(workspaceUri, outputChannel); + let provider = new WorkspaceSymbolProvider([generator], outputChannel); + let symbols = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); + assert.equal(symbols.length, 0, 'Symbols returned even when workspace symbols are turned off'); + generator.dispose(); + + await updateSetting('workspaceSymbols.enabled', true, workspaceUri, configUpdateTarget); + + // The workspace will be in the output test folder + // So lets modify the settings so it sees the source test folder + settings = PythonSettings.getInstance(workspaceUri); + settings.workspaceSymbols.tagFilePath = path.join(workspaceUri.fsPath, '.vscode', 'tags') + + generator = new Generator(workspaceUri, outputChannel); + provider = new WorkspaceSymbolProvider([generator], outputChannel); + symbols = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); + assert.notEqual(symbols.length, 0, 'Symbols should be returned when workspace symbols are turned on'); + }); + test(`symbols should be filtered correctly`, async () => { + const outputChannel = new MockOutputChannel('Output'); + + await updateSetting('workspaceSymbols.enabled', true, workspaceUri, configUpdateTarget); + + // The workspace will be in the output test folder + // So lets modify the settings so it sees the source test folder + const settings = PythonSettings.getInstance(workspaceUri); + settings.workspaceSymbols.tagFilePath = path.join(workspaceUri.fsPath, '.vscode', 'tags') + + const generators = [new Generator(workspaceUri, outputChannel)]; + const provider = new WorkspaceSymbolProvider(generators, outputChannel); + const symbols = await provider.provideWorkspaceSymbols('meth1Of', new CancellationTokenSource().token); + + assert.equal(symbols.length >= 2, true, 'Incorrect number of symbols returned'); + assert.notEqual(symbols.findIndex(sym => sym.location.uri.fsPath.endsWith('childFile.py')), -1, 'File with symbol not found in child workspace folder'); + assert.notEqual(symbols.findIndex(sym => sym.location.uri.fsPath.endsWith('workspace2File.py')), -1, 'File with symbol not found in child workspace folder'); + + const symbolsForMeth = await provider.provideWorkspaceSymbols('meth', new CancellationTokenSource().token); + assert.equal(symbolsForMeth.length >= 10, true, 'Incorrect number of symbols returned'); + assert.notEqual(symbolsForMeth.findIndex(sym => sym.location.uri.fsPath.endsWith('childFile.py')), -1, 'Symbols not returned for childFile.py'); + assert.notEqual(symbolsForMeth.findIndex(sym => sym.location.uri.fsPath.endsWith('workspace2File.py')), -1, 'Symbols not returned for workspace2File.py'); + assert.notEqual(symbolsForMeth.findIndex(sym => sym.location.uri.fsPath.endsWith('file.py')), -1, 'Symbols not returned for file.py'); + }); +}); diff --git a/src/testMultiRootWkspc/disableLinters/.vscode/tags b/src/testMultiRootWkspc/disableLinters/.vscode/tags new file mode 100644 index 000000000000..4739b4629cfb --- /dev/null +++ b/src/testMultiRootWkspc/disableLinters/.vscode/tags @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/testMultiRootWkspc/disableLinters/file.py b/src/testMultiRootWkspc/disableLinters/file.py new file mode 100644 index 000000000000..439f899e9e22 --- /dev/null +++ b/src/testMultiRootWkspc/disableLinters/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop diff --git a/src/testMultiRootWkspc/multi.code-workspace b/src/testMultiRootWkspc/multi.code-workspace new file mode 100644 index 000000000000..6aca26f07b90 --- /dev/null +++ b/src/testMultiRootWkspc/multi.code-workspace @@ -0,0 +1,42 @@ +{ + "folders": [ + { + "path": "workspace1" + }, + { + "path": "workspace2" + }, + { + "path": "workspace3" + }, + { + "path": "parent\\child" + }, + { + "path": "disableLinters" + }, + { + "path": "../test" + } + ], + "settings": { + "python.linting.flake8Enabled": false, + "python.linting.mypyEnabled": false, + "python.linting.pydocstyleEnabled": false, + "python.linting.pylamaEnabled": false, + "python.linting.pylintEnabled": true, + "python.linting.pep8Enabled": false, + "python.linting.prospectorEnabled": false, + "python.workspaceSymbols.enabled": false, + "python.formatting.formatOnSave": false, + "python.formatting.provider": "yapf", + "python.sortImports.args": [ + "-sp", + "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/sorting/withconfig" + ], + "python.linting.lintOnSave": false, + "python.linting.lintOnTextChange": false, + "python.linting.enabled": true, + "python.pythonPath": "python" + } +} diff --git a/src/testMultiRootWkspc/parent/child/.vscode/settings.json b/src/testMultiRootWkspc/parent/child/.vscode/settings.json new file mode 100644 index 000000000000..c404e94945a9 --- /dev/null +++ b/src/testMultiRootWkspc/parent/child/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.workspaceSymbols.enabled": false +} \ No newline at end of file diff --git a/src/testMultiRootWkspc/parent/child/.vscode/tags b/src/testMultiRootWkspc/parent/child/.vscode/tags new file mode 100644 index 000000000000..e6791c755b0f --- /dev/null +++ b/src/testMultiRootWkspc/parent/child/.vscode/tags @@ -0,0 +1,24 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Child2Class ..\\childFile.py /^class Child2Class(object):$/;" kind:class line:5 +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\childFile.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\childFile.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +childFile.py ..\\childFile.py 1;" kind:file line:1 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1OfChild ..\\childFile.py /^ def meth1OfChild(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/testMultiRootWkspc/parent/child/childFile.py b/src/testMultiRootWkspc/parent/child/childFile.py new file mode 100644 index 000000000000..31d6fc7b4a18 --- /dev/null +++ b/src/testMultiRootWkspc/parent/child/childFile.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Child2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfChild(self, arg): + """this issues a message""" + print (self) diff --git a/src/testMultiRootWkspc/parent/child/file.py b/src/testMultiRootWkspc/parent/child/file.py new file mode 100644 index 000000000000..439f899e9e22 --- /dev/null +++ b/src/testMultiRootWkspc/parent/child/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop diff --git a/src/testMultiRootWkspc/workspace1/.vscode/settings.json b/src/testMultiRootWkspc/workspace1/.vscode/settings.json new file mode 100644 index 000000000000..f4d89e3bc0e4 --- /dev/null +++ b/src/testMultiRootWkspc/workspace1/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.linting.enabled": false, + "python.linting.flake8Enabled": true, + "python.linting.pylintEnabled": false +} diff --git a/src/testMultiRootWkspc/workspace1/.vscode/tags b/src/testMultiRootWkspc/workspace1/.vscode/tags new file mode 100644 index 000000000000..4739b4629cfb --- /dev/null +++ b/src/testMultiRootWkspc/workspace1/.vscode/tags @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/testMultiRootWkspc/workspace1/file.py b/src/testMultiRootWkspc/workspace1/file.py new file mode 100644 index 000000000000..439f899e9e22 --- /dev/null +++ b/src/testMultiRootWkspc/workspace1/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop diff --git a/src/testMultiRootWkspc/workspace2/.vscode/settings.json b/src/testMultiRootWkspc/workspace2/.vscode/settings.json new file mode 100644 index 000000000000..750d19764931 --- /dev/null +++ b/src/testMultiRootWkspc/workspace2/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.workspaceSymbols.tagFilePath": "${workspaceRoot}/workspace2.tags.file", + "python.workspaceSymbols.enabled": false +} diff --git a/src/testMultiRootWkspc/workspace2/file.py b/src/testMultiRootWkspc/workspace2/file.py new file mode 100644 index 000000000000..439f899e9e22 --- /dev/null +++ b/src/testMultiRootWkspc/workspace2/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop diff --git a/src/testMultiRootWkspc/workspace2/workspace2.tags.file b/src/testMultiRootWkspc/workspace2/workspace2.tags.file new file mode 100644 index 000000000000..375785e2a94e --- /dev/null +++ b/src/testMultiRootWkspc/workspace2/workspace2.tags.file @@ -0,0 +1,24 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^class Foo(object):$/;" kind:class line:5 +Workspace2Class C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\workspace2File.py /^class Workspace2Class(object):$/;" kind:class line:5 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\workspace2File.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\workspace2File.py /^__revision__ = None$/;" kind:variable line:3 +file.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py 1;" kind:file line:1 +meth1 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1OfWorkspace2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\workspace2File.py /^ def meth1OfWorkspace2(self, arg):$/;" kind:member line:11 +meth2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth8(self):$/;" kind:member line:80 +workspace2File.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\workspace2File.py 1;" kind:file line:1 diff --git a/src/testMultiRootWkspc/workspace2/workspace2File.py b/src/testMultiRootWkspc/workspace2/workspace2File.py new file mode 100644 index 000000000000..61aa87c55fed --- /dev/null +++ b/src/testMultiRootWkspc/workspace2/workspace2File.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Workspace2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfWorkspace2(self, arg): + """this issues a message""" + print (self) diff --git a/src/testMultiRootWkspc/workspace3/.vscode/settings.json b/src/testMultiRootWkspc/workspace3/.vscode/settings.json new file mode 100644 index 000000000000..8779a0c08efe --- /dev/null +++ b/src/testMultiRootWkspc/workspace3/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.workspaceSymbols.tagFilePath": "${workspaceRoot}/workspace3.tags.file" +} diff --git a/src/testMultiRootWkspc/workspace3/file.py b/src/testMultiRootWkspc/workspace3/file.py new file mode 100644 index 000000000000..439f899e9e22 --- /dev/null +++ b/src/testMultiRootWkspc/workspace3/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop diff --git a/src/testMultiRootWkspc/workspace3/workspace3.tags.file b/src/testMultiRootWkspc/workspace3/workspace3.tags.file new file mode 100644 index 000000000000..3a65841e2aff --- /dev/null +++ b/src/testMultiRootWkspc/workspace3/workspace3.tags.file @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py 1;" kind:file line:1 +meth1 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/tsconfig.json b/tsconfig.json index d0276a05085f..0b10f6fac53d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,7 @@ ], "sourceMap": true, "rootDir": "src" - // TODO: better code + // TODO: enable to ensure all code complies with strict coding standards // , "strict": true }, "exclude": [ @@ -20,4 +20,4 @@ "src/client/src/typings", "docs" ] -} \ No newline at end of file +} diff --git a/tsfmt.json b/tsfmt.json new file mode 100644 index 000000000000..fffcf07c1998 --- /dev/null +++ b/tsfmt.json @@ -0,0 +1,17 @@ +{ + "tabSize": 4, + "indentSize": 4, + "newLineCharacter": "\n", + "convertTabsToSpaces": false, + "insertSpaceAfterCommaDelimiter": true, + "insertSpaceAfterSemicolonInForStatements": true, + "insertSpaceBeforeAndAfterBinaryOperators": true, + "insertSpaceAfterKeywordsInControlFlowStatements": true, + "insertSpaceAfterFunctionKeywordForAnonymousFunctions": true, + "insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, + "insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false, + "insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": false, + "insertSpaceBeforeFunctionParenthesis": false, + "placeOpenBraceOnNewLineForFunctions": false, + "placeOpenBraceOnNewLineForControlBlocks": false +} diff --git a/tslint.json b/tslint.json index aec10615173d..f55dca7c2b4c 100644 --- a/tslint.json +++ b/tslint.json @@ -1,13 +1,46 @@ { + "extends": [ + "tslint-eslint-rules", + "tslint-microsoft-contrib" + ], "rules": { "no-unused-expression": true, - "no-unreachable": true, "no-duplicate-variable": true, - "no-duplicate-key": true, "no-unused-variable": true, "curly": true, "class-name": true, - "semicolon": true, - "triple-equals": true + "semicolon": [ + true + ], + "triple-equals": true, + "no-relative-imports": false, + "max-line-length": false, + "typedef": false, + "no-string-throw": true, + "missing-jsdoc": false, + "one-line": [ + true, + "check-catch", + "check-finally", + "check-else" + ], + "no-parameter-properties": false, + "no-reserved-keywords": false, + "newline-before-return": false, + "export-name": false, + "align": false, + "linebreak-style": false, + "strict-boolean-expressions": [ + true, + "allow-null-union", + "allow-undefined-union", + "allow-string", + "allow-number" + ], + "await-promise": [ + true, + "Thenable" + ], + "completed-docs": false } -} \ No newline at end of file +} diff --git a/typings.json b/typings.json deleted file mode 100644 index 9f6c5ea9c4a9..000000000000 --- a/typings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "globalDependencies": { - "fs-extra": "registry:dt/fs-extra#0.0.0+20160319124112", - "xml2js": "registry:dt/xml2js#0.0.0+20160317120654" - } -} diff --git a/typings/globals/xml2js/index.d.ts b/typings/globals/xml2js/index.d.ts deleted file mode 100644 index a3feea4d5ab8..000000000000 --- a/typings/globals/xml2js/index.d.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Generated by typings -// Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/xml2js/xml2js.d.ts -declare module 'xml2js' { - - export = xml2js; - - namespace xml2js { - function parseString(xml: string, callback: (err: any, result: any) => void): void; - function parseString(xml: string, options: Options, callback: (err: any, result: any) => void): void; - - var defaults: { - '0.1': Options; - '0.2': OptionsV2; - } - - class Builder { - constructor(options?: BuilderOptions); - buildObject(rootObj: any): string; - } - - class Parser { - constructor(options?: Options); - processAsync(): any; - assignOrPush(obj: any, key: string, newValue: any): any; - reset(): any; - parseString(str: string , cb?: Function): void; - } - - interface RenderOptions { - indent?: string; - newline?: string; - pretty?: boolean; - } - - interface XMLDeclarationOptions { - encoding?: string; - standalone?: boolean; - version?: string; - } - - interface BuilderOptions { - doctype?: any; - headless?: boolean; - indent?: string; - newline?: string; - pretty?: boolean; - renderOpts?: RenderOptions; - rootName?: string; - xmldec?: XMLDeclarationOptions; - } - - interface Options { - async?: boolean; - attrkey?: string; - attrNameProcessors?: [(name: string) => string]; - attrValueProcessors?: [(name: string) => string]; - charkey?: string; - charsAsChildren?: boolean; - childkey?: string; - emptyTag?: any; - explicitArray?: boolean; - explicitCharkey?: boolean; - explicitChildren?: boolean; - explicitRoot?: boolean; - ignoreAttrs?: boolean; - mergeAttrs?: boolean; - normalize?: boolean; - normalizeTags?: boolean; - strict?: boolean; - tagNameProcessors?: [(name: string) => string]; - trim?: boolean; - validator?: Function; - valueProcessors?: [(name: string) => string]; - xmlns?: boolean; - } - - interface OptionsV2 extends Options { - preserveChildrenOrder?: boolean; - rootName?: string; - xmldec?: { - version: string; - encoding?: string; - standalone?: boolean; - }; - doctype?: any; - renderOpts?: { - pretty?: boolean; - indent?: string; - newline?: string; - }; - headless?: boolean; - chunkSize?: number; - cdata?: boolean; - } - } -} diff --git a/typings/globals/xml2js/typings.json b/typings/globals/xml2js/typings.json deleted file mode 100644 index 4f70efbc7f27..000000000000 --- a/typings/globals/xml2js/typings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "resolution": "main", - "tree": { - "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/xml2js/xml2js.d.ts", - "raw": "registry:dt/xml2js#0.0.0+20160317120654", - "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/xml2js/xml2js.d.ts" - } -} diff --git a/typings/index.d.ts b/typings/index.d.ts deleted file mode 100644 index 3e9f3c454ee6..000000000000 --- a/typings/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/typings/node.d.ts b/typings/node.d.ts deleted file mode 100644 index 457fd135e6f6..000000000000 --- a/typings/node.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/typings/vscode-typings.d.ts b/typings/vscode-typings.d.ts deleted file mode 100644 index e9d47fd5a066..000000000000 --- a/typings/vscode-typings.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file