diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index fadcc7ec1f20..1d6d6a97d1df 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -12,33 +12,33 @@ Otherwise **please** fill in the requested details below. "XXX" markers should n ## Environment data -- VS Code version: < XXX > -- Extension version (available under the Extensions sidebar): < XXX > -- OS and version: < XXX > -- Python version: < XXX; include the distribution details if applicable, e.g. Anaconda > -- Type of virtual environment used (if applicable): < XXX: N/A | venv | virtualenv | conda | ... > -- Relevant/affected Python packages and their versions: < XXX > +- VS Code version: XXX +- Extension version (available under the Extensions sidebar): XXX +- OS and version: XXX +- Python version (& distribution if applicable, e.g. Anaconda): XXX +- Type of virtual environment used (N/A | venv | virtualenv | conda | ...): XXX +- Relevant/affected Python packages and their versions: XXX ## Actual behavior -< XXX > +XXX ## Expected behavior -< XXX > +XXX ## Steps to reproduce: -1. < XXX > +1. XXX ## Logs Output for `Python` in the `Output` panel (`View`→`Output`, change the drop-down the upper-right of the `Output` panel to `Python`) ``` -< XXX > +XXX ``` Output from `Console` under the `Developer Tools` panel (toggle Developer Tools on under `Help`) ``` -< XXX > +XXX ``` diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000000..ecc2a4026baf --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ +Fixes # + +- [ ] Title summarizes what is changing +- [ ] Includes a [news entry](https://github.com/Microsoft/vscode-python/tree/master/news) file (remember to thank yourself!) +- [ ] Unit tests & [code coverage](https://codecov.io/gh/Microsoft/vscode-python) are not adversely affected (within reason) +- [ ] Works on all [actively maintained versions of Python](https://devguide.python.org/#status-of-python-branches) (e.g. Python 2.7 & the latest Python 3 release) +- [ ] Works on Windows 10, macOS, and Linux (e.g. considered file system case-sensitivity) +- [ ] Dependencies are pinned (e.g. `"1.2.3"`, not `"^1.2.3"`) +- [ ] `package-lock.json` has been regenerated if dependencies have changed diff --git a/.github/lock.yml b/.github/lock.yml new file mode 100644 index 000000000000..dde77291a4ef --- /dev/null +++ b/.github/lock.yml @@ -0,0 +1,3 @@ +daysUntilLock: 28 +lockComment: false +only: issues diff --git a/.github/release_plan.md b/.github/release_plan.md new file mode 100644 index 000000000000..e6ae17fd41bf --- /dev/null +++ b/.github/release_plan.md @@ -0,0 +1,92 @@ +# Week of Monday, XXX + +- [ ] Review the state of the current [milestone](https://github.com/Microsoft/vscode-python/milestones) +- [ ] Go through telemetry for GDPR +- [ ] Go through all [merged pull requests](https://github.com/Microsoft/vscode-python/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Amerged) and add the [`validate fix` label](https://github.com/Microsoft/vscode-python/labels/validate%20fix) as appropriate as well as checking for news entries +- [ ] Validate [fixed issues](https://github.com/Microsoft/vscode-python/issues?q=label%3A%22validate+fix%22+is%3Aclosed) +- [ ] Triage [unverified issues](https://github.com/Microsoft/vscode-python/labels/needs%20verification) +- [ ] Update pre-existing dependencies as appropriate (npm, Python, git submodule, or otherwise; requires updating CELA) + +## Planning +- [ ] Evaluate if TypeScript usage needs updating to sync with VS Code's usage +- [ ] Evaluate [projects](https://github.com/Microsoft/vscode-python/projects) & [`meta` issues](https://github.com/Microsoft/vscode-python/labels/meta) +- [ ] Go through [`needs PR` issues](https://github.com/Microsoft/vscode-python/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22needs+PR%22+-label%3A%22help+wanted%22+-label%3A%22good+first+issue%22+no%3Amilestone) to see if there's anything we want to add to this milestone +- [ ] Finalize the initial set of issues for the [milestone](https://github.com/Microsoft/vscode-python/milestones) +- [ ] Close issues that have [needed more info](https://github.com/Microsoft/vscode-python/issues?q=is%3Aopen+label%3A%22needs+more+info%22+sort%3Aupdated-asc) for over a month + +# Week of Monday, XXX + +- [ ] Review the state of the current [milestone](https://github.com/Microsoft/vscode-python/milestones) +- [ ] Go through telemetry for GDPR +- [ ] Go through all [merged pull requests](https://github.com/Microsoft/vscode-python/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Amerged) and add the [`validate fix` label](https://github.com/Microsoft/vscode-python/labels/validate%20fix) as appropriate as well as checking for news entries +- [ ] Validate [fixed issues](https://github.com/Microsoft/vscode-python/issues?q=label%3A%22validate+fix%22+is%3Aclosed) +- [ ] Triage [unverified issues](https://github.com/Microsoft/vscode-python/labels/needs%20verification) + +## Planning +- [ ] Check if there have been no performance regressions +- [ ] Read through [VS Code's iteration plan](https://github.com/Microsoft/vscode/labels/iteration-plan) (it may still be a [draft](https://github.com/Microsoft/vscode/labels/iteration-plan-draft)) + +# Week of Monday, XXX + +- [ ] Review the state of the current [milestone](https://github.com/Microsoft/vscode-python/milestones) +- [ ] Go through telemetry for GDPR +- [ ] Go through all [merged pull requests](https://github.com/Microsoft/vscode-python/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Amerged) and add the [`validate fix` label](https://github.com/Microsoft/vscode-python/labels/validate%20fix) as appropriate as well as checking for news entries +- [ ] Validate [fixed issues](https://github.com/Microsoft/vscode-python/issues?q=label%3A%22validate+fix%22+is%3Aclosed) +- [ ] Triage [unverified issues](https://github.com/Microsoft/vscode-python/labels/needs%20verification) + +## Legal +- [ ] Announce the lock-down of dependencies for this release +- [ ] Notify CELA of all changes to the [repository](https://github.com/Microsoft/vscode-python/tree/master/pythonFiles), [distribution dependencies](https://github.com/Microsoft/vscode-python/blob/master/package.json) (including [ptvsd](https://pypi.org/project/ptvsd/), and [git submodules](https://github.com/Microsoft/vscode-python) (e.g. Typeshed) + +## Release a beta version for testing +- [ ] Update the [version](https://github.com/Microsoft/vscode-python/blob/master/package.json) to be a `beta` & update the [changelog](https://github.com/Microsoft/vscode-python/blob/master/CHANGELOG.md) +- [ ] Announce the beta [development build](https://github.com/Microsoft/vscode-python/blob/master/CONTRIBUTING.md#development-build) (along with how to help [validate fixes](https://github.com/Microsoft/vscode-python/issues?q=label%3A%22validate+fix%22+is%3Aclosed)) +- [ ] Open appropriate [documentation issues](https://github.com/microsoft/vscode-docs/issues?q=is%3Aissue+is%3Aopen+label%3Apython) + +# Week of Monday, XXX + +- [ ] Review the state of the current [milestone](https://github.com/Microsoft/vscode-python/milestones) +- [ ] Go through telemetry for GDPR +- [ ] Merge any last-minute [pull requests](https://github.com/Microsoft/vscode-python/pulls) +- [ ] Go through all [merged pull requests](https://github.com/Microsoft/vscode-python/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Amerged) and add the [`validate fix` label](https://github.com/Microsoft/vscode-python/labels/validate%20fix) as appropriate as well as checking for news entries +- [ ] Validate [fixed issues](https://github.com/Microsoft/vscode-python/issues?q=label%3A%22validate+fix%22+is%3Aclosed) +- [ ] Triage [unverified issues](https://github.com/Microsoft/vscode-python/labels/needs%20verification) + +## Prep for the release candidate +- [ ] Announce feature freeze +- [ ] Make sure the [repo](https://github.com/Microsoft/vscode-python/blob/master/ThirdPartyNotices-Repository.txt) and [distribution TPNs](https://github.com/Microsoft/vscode-python/blob/master/ThirdPartyNotices-Distribution.txt) have been updated appropriately + +## Test the release candidate code +- [ ] Update the [version](https://github.com/Microsoft/vscode-python/blob/master/package.json) to be an `rc` & update the [changelog](https://github.com/Microsoft/vscode-python/blob/master/CHANGELOG.md) +- [ ] Announce the release candidate [development build](https://github.com/Microsoft/vscode-python/blob/master/CONTRIBUTING.md#development-build) +- [ ] Open appropriate [documentation issues](https://github.com/microsoft/vscode-docs/issues?q=is%3Aissue+is%3Aopen+label%3Apython) +- [ ] Begin drafting a [blog](http://aka.ms/pythonblog) post + +## Prep the release +- [ ] Ensure all new feature usages are tracked via telemetry +- [ ] Make sure no extraneous files are being included in the `.vsix` file (make sure to check for hidden files) +- [ ] Make sure the [appropriate pull requests](https://github.com/microsoft/vscode-docs/pulls) for the [documentation](https://code.visualstudio.com/docs/python/python-tutorial) -- including the [WOW](https://code.visualstudio.com/docs/languages/python) page -- are ready + +## Release +- [ ] Update the [changelog](https://github.com/Microsoft/vscode-python/blob/master/CHANGELOG.md) (including the names of external contributors & projects) +- [ ] Update the [version](https://github.com/Microsoft/vscode-python/blob/master/package.json) number to be final +- [ ] Make sure [CI](https://github.com/Microsoft/vscode-python/blob/master/CONTRIBUTING.md) is passing +- [ ] Generate the final `.vsix` file +- [ ] Upload the final `.vsix` file to the [marketplace](https://marketplace.visualstudio.com/items?itemName=ms-python.python) +- [ ] Publish [documentation](https://code.visualstudio.com/docs/python/python-tutorial) [changes](https://github.com/microsoft/vscode-docs/pulls) +- [ ] Publish the [blog](http://aka.ms/pythonblog) post +- [ ] Create a [release](https://github.com/Microsoft/vscode-python/releases) on GitHub (which creates an appropriate git tag) +- [ ] Determine if a hotfix is needed +- [ ] Create the `release-` [branch](https://github.com/Microsoft/vscode-python/) + +## Prep for the _next_ release +- [ ] Bump the [version](https://github.com/Microsoft/vscode-python/blob/master/package.json) number to the next `alpha` +- [ ] Make sure the next **two** [milestones](https://github.com/Microsoft/vscode-python/milestones) exist +- [ ] Lift the feature freeze +- [ ] Create a new [release plan](https://github.com/Microsoft/vscode-python/edit/master/.github/release_plan.md) + +## Clean up after _this_ release +- [ ] Clean up any straggling [fixed issues needing validation](https://github.com/Microsoft/vscode-python/issues?q=label%3A%22validate+fix%22+is%3Aclosed) +- [ ] Close the (now) old [milestone](https://github.com/Microsoft/vscode-python/milestones) +- [ ] Delete the previous releases' [branch](https://github.com/Microsoft/vscode-python/branches) +- [ ] Go through [`needs more info` issues](https://github.com/Microsoft/vscode-python/issues?q=is%3Aopen+label%3A%22needs+more+info%22+sort%3Aupdated-asc) and close any that have no activity for over a month diff --git a/.github/test_plan.md b/.github/test_plan.md new file mode 100644 index 000000000000..a98b21fea0b6 --- /dev/null +++ b/.github/test_plan.md @@ -0,0 +1,283 @@ +# Test plan + +## Environment + +- OS: XXX (Windows, macOS, latest Ubuntu LTS) + - Shell: XXX (Command Prompt, PowerShell, bash, fish) +- Python + - Distribution: XXX (CPython, miniconda) + - Version: XXX (2.7, latest 3.x) +- VS Code: XXX (Insiders) + +## Tests + +**ALWAYS**: +- Check the `Output` window under `Python` for logged errors +- Have `Developer Tools` open to detect any errors +- Consider running the tests in a multi-folder workspace +- Focus on in-development features (i.e. experimental debugger and language server) + +
+ Scenarios + +### [Environment](https://code.visualstudio.com/docs/python/environments) +#### Interpreters + +- [ ] Interpreter is [shown in the status bar](https://code.visualstudio.com/docs/python/environments#_choosing-an-environment) +- [ ] An interpreter can be manually specified using the [`Select Interpreter` command](https://code.visualstudio.com/docs/python/environments#_choosing-an-environment) +- [ ] Detected system-installed interpreters +- [ ] Detected an Anaconda installation +- [ ] (Linux/macOS) Detected all interpreters installed w/ [pyenv](https://github.com/pyenv/pyenv) detected +- [ ] [`"python.pythonPath"`](https://code.visualstudio.com/docs/python/environments#_manually-specifying-an-interpreter) triggers an update in the status bar +- [ ] `Run Python File in Terminal` +- [ ] `Run Selection/Line in Python Terminal` + - [ ] Right-click + - [ ] Command + - [ ] `Shift+Enter` + +#### Virtual environments + +**ALWAYS**: +- Use the latest version of Anaconda +- Realize that `conda` is slow +- Create an environment with a space in their path somewhere +- Make sure that you do not have `python.pythonPath` specified in your `settings.json` when testing automatic detection +- Do note that the `Select Interpreter` drop-down window scrolls + +- [ ] Detected a single virtual environment at the top-level of the workspace folder (if you created this _after_ opening VS Code, then run `Reload Window` to pick up the new environment) + - [ ] Appropriate suffix label specified in status bar (e.g. `(venv)`) + - [ ] [`Create Terminal`](https://code.visualstudio.com/docs/python/environments#_activating-an-environment-in-the-terminal) works + - [ ] Steals focus + - [ ] `"python.terminal.activateEnvironment": false` deactivates automatically running the activation script in the terminal + - [ ] After the language server downloads it is able to complete its analysis of the environment w/o requiring a restart +- [ ] Detect multiple virtual environments contained in the directory specified in `"python.venvPath"` +- [ ] Detected all [conda environments created with an interpreter](https://code.visualstudio.com/docs/python/environments#_conda-environments) + - [ ] Appropriate suffix label specified in status bar (e.g. `(condaenv)`) + - [ ] Prompted to install Pylint + - [ ] Asked whether to install using conda or pip + - [ ] Installs into environment + - [ ] [`Create Terminal`](https://code.visualstudio.com/docs/python/environments#_activating-an-environment-in-the-terminal) works + - [ ] `"python.terminal.activateEnvironment": false` deactivates automatically running the activation script in the terminal + - [ ] After the language server downloads it is able to complete its analysis of the environment w/o requiring a restart +- [ ] (Linux/macOS until [`-m` is supported](https://github.com/Microsoft/vscode-python/issues/978)) Detected the virtual environment created by [pipenv](https://docs.pipenv.org/) + - [ ] Appropriate suffix label specified in status bar (e.g. `(pipenv)`) + - [ ] Prompt to install Pylint uses `pipenv install --dev` + - [ ] [`Create Terminal`](https://code.visualstudio.com/docs/python/environments#_activating-an-environment-in-the-terminal) works + - [ ] `"python.terminal.activateEnvironment": false` deactivates automatically running the activation script in the terminal + - [ ] After the language server downloads it is able to complete its analysis of the environment w/o requiring a restart +- [ ] (Linux/macOS) Virtual environments created under `{workspaceFolder}/.direnv/python-{python_version}` are detected (for [direnv](https://direnv.net/) and its [`layout python3`](https://github.com/direnv/direnv/blob/master/stdlib.sh) support) + - [ ] Appropriate suffix label specified in status bar (e.g. `(venv)`) + +#### [Environment files](https://code.visualstudio.com/docs/python/environments#_environment-variable-definitions-file) +Sample files: +```python +# example.py +import os +print('Hello,', os.environ.get('WHO'), '!') +``` +``` +# .env +WHO=world +PYTHONPATH=some/path/somewhere +```` + +**ALWAYS**: +- Make sure to use `Reload Window` between tests to reset your environment +- Note that environment files only apply under the debugger and Jedi + +- [ ] Environment variables in a `.env` file are exposed when running under the debugger +- [ ] `"python.envFile"` allows for specifying an environment file manually (e.g. Jedi picks up `PYTHONPATH` changes) +- [ ] `envFile` in a `launch.json` configuration works + +#### [Debugging](https://code.visualstudio.com/docs/python/environments#_python-interpreter-for-debugging) + +- [ ] `pythonPath` setting in your `launch.json` overrides your `python.pythonPath` default setting + +### [Linting](https://code.visualstudio.com/docs/python/linting) + +**ALWAYS**: +- Check under the `Problems` tab to see e.g. if a linter is raising errors + +#### Pylint/default linting +[Prompting to install Pylint is covered under `Environments` above] + +For testing the disablement of the default linting rules for Pylint: +```ini +# pylintrc +[MESSAGES CONTROL] +enable=bad-names +``` +```python3 +# example.py +foo = 42 # Marked as a blacklisted name. +``` +- [ ] Installation via the prompt installs Pylint as appropriate + - [ ] Uses `--user` for system-install of Python + - [ ] Installs into a virtual environment environment directly +- [ ] Pylint works +- [ ] `"python.linting.pylintUseMinimalCheckers": false` turns off the default rules w/ no `pylintrc` file present +- [ ] The existence of a `pylintrc` file turns off the default rules + +#### Other linters + +**Note**: +- You can use the `Run Linting` command to run a newly installed linter +- When the extension installs a new linter, it turns off all other linters + +- [ ] flake8 works + - [ ] `Select linter` lists the linter and installs it if necessary +- [ ] mypy works + - [ ] `Select linter` lists the linter and installs it if necessary +- [ ] pep8 works + - [ ] `Select linter` lists the linter and installs it if necessary +- [ ] prospector works + - [ ] `Select linter` lists the linter and installs it if necessary +- [ ] pydocstyle works + - [ ] `Select linter` lists the linter and installs it if necessary +- [ ] pylama works + - [ ] `Select linter` lists the linter and installs it if necessary +- [ ] 3 or more linters work simultaneously (make sure you have turned on the linters in your `settings.json`) + - [ ] `Run Linting` runs all activated linters + - [ ] `"python.linting.enabled": false` disables all linters + - [ ] The `Enable Linting` command changes `"python.linting.enabled"` +- [ ] `"python.linting.lintOnSave` works + +### [Editing](https://code.visualstudio.com/docs/python/editing) + +#### [IntelliSense](https://code.visualstudio.com/docs/python/editing#_autocomplete-and-intellisense) + +Please also test for general accuracy on the most "interesting" code you can find. + +- [ ] `"python.autoComplete.extraPaths"` works +- [ ] `"python.autoComplete.preloadModules"` works (e.g. listing `numpy` should visibly speed up completing on the module's contents) +- [ ] `"python.autocomplete.addBrackets": true` causes auto-completion of functions to append `()` + +#### [Formatting](https://code.visualstudio.com/docs/python/editing#_formatting) +Sample file: +```python +# There should be _some_ change after running `Format Document`. +def foo():pass +``` + +- [ ] Prompted to install a formatter if none installed and `Format Document` is run + - [ ] Installing `autopep8` works + - [ ] Installing `black` works + - [ ] Installing `yapf` works +- [ ] autopep8 works +- [ ] black works +- [ ] yapf works +- [ ] `"editor.formatOnType": true` works and has expected results + +#### [Refactoring](https://code.visualstudio.com/docs/python/editing#_refactoring) + +- [ ] [`Extract Variable`](https://code.visualstudio.com/docs/python/editing#_extract-variable) works + - [ ] You are prompted to install `rope` if it is not already available +- [ ] [`Extract method`](https://code.visualstudio.com/docs/python/editing#_extract-method) works + - [ ] You are prompted to install `rope` if it is not already available +- [ ] [`Sort Imports`](https://code.visualstudio.com/docs/python/editing#_sort-imports) works + +### [Debugging](https://code.visualstudio.com/docs/python/debugging) + +**ALWAYS**: +- Test the current debugger +- Test the experimental debugger (and note whether it is _at least_ as fast as the old debugger) + +- [ ] [Configurations](https://code.visualstudio.com/docs/python/debugging#_debugging-specific-app-types) work + - [ ] `Current File` + - [ ] `Module` + - [ ] `Attach` + - [ ] `Terminal (integrated)` + - [ ] `Terminal (external)` + - [ ] `Django` + - [ ] `Flask` + - [ ] `Pyramid` + - [ ] `Watson` + - [ ] `Scrapy` + - [ ] `PySpark` + - [ ] `All debug Options` with [appropriate values](https://code.visualstudio.com/docs/python/debugging#_standard-configuration-and-options) edited to make values valid +- [ ] Running code from start to finish w/ no special debugging options (e.g. no breakpoints) +- [ ] Breakpoints + - [ ] Set + - [ ] Hit + - [ ] Conditional + - [ ] Expression + - [ ] Hit count + - [ ] Log points (experimental debugger only) +- [ ] Stepping + - [ ] Over + - [ ] Into + - [ ] Out +- [ ] Can inspect variables + - [ ] Through hovering over variable in code + - [ ] `Variables` section of debugger sidebar +- [ ] [Remote debugging](https://code.visualstudio.com/docs/python/debugging#_remote-debugging) works + - [ ] ... over SSH +- [ ] [App Engine](https://code.visualstudio.com/docs/python/debugging#_google-app-engine-debugging) + +### [Unit testing](https://code.visualstudio.com/docs/python/unit-testing) + +#### [`unittest`](https://code.visualstudio.com/docs/python/unit-testing#_unittest-configuration-settings) +```python +import unittest + +class PassingTests(unittest.TestCase): + + def test_passing(self): + self.assertEqual(42, 42) + + def test_passing_still(self): + self.assertEqual("silly walk", "silly walk") + + +class FailingTests(unittest.TestCase): + + def test_failure(self): + self.assertEqual(42, -13) + + def test_failure_still(self): + self.assertEqual("I'm right!", "no, I am!") +``` +- [ ] `Run All Unit Tests` triggers the prompt to configure the test runner +- [ ] Tests are discovered (as shown by code lenses on each test) + +#### [`pytest`](https://code.visualstudio.com/docs/python/unit-testing#_pytest-configuration-settings) +```python +def test_passing(): + assert 42 == 42 + +def test_failure(): + assert 42 == -13 +``` + +- [ ] `Run All Unit Tests` triggers the prompt to configure the test runner + - [ ] `pytest` gets installed +- [ ] Tests are discovered (as shown by code lenses on each test) + +#### [`nose`](https://code.visualstudio.com/docs/python/unit-testing#_nose-configuration-settings) +```python +def test_passing(): + assert 42 == 42 + +def test_failure(): + assert 42 == -13 +``` + +- [ ] `Run All Unit Tests` triggers the prompt to configure the test runner + - [ ] Nose gets installed +- [ ] Tests are discovered (as shown by code lenses on each test) + +#### General + +- [ ] Code lenses appears + - [ ] `Run Test` lens works (and status bar updates as appropriate) + - [ ] `Debug Test` lens works + - [ ] Appropriate ✔/❌ shown for each test +- [ ] Status bar is functioning + - [ ] Appropriate test results displayed + - [ ] `Run All Unit Tests` works + - [ ] `Discover Unit Tests` works (resets tests result display in status bar) + - [ ] `Run Unit Test Method ...` works + - [ ] `View Unit Test Output` works + - [ ] After having at least one failure, `Run Failed Tests` works + +
diff --git a/.gitignore b/.gitignore index 466b005af61d..c6cd087c91ce 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,10 @@ coverage/ .vscode-test/** .venv pythonFiles/experimental/ptvsd/** +debug_coverage*/** +languageServer/** +bin/** +obj/** +.pytest_cache +tmp/** +.python-version diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000000..a57bae098756 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "typeshed"] + path = typeshed + url = https://github.com/python/typeshed.git + branch = master \ No newline at end of file diff --git a/.mocha-reporter/mocha-vsts-reporter.js b/.mocha-reporter/mocha-vsts-reporter.js new file mode 100644 index 000000000000..7ac83371527c --- /dev/null +++ b/.mocha-reporter/mocha-vsts-reporter.js @@ -0,0 +1,63 @@ +'use-strict'; + +var mocha = require('mocha'); +var MochaJUnitReporter = require('mocha-junit-reporter'); +module.exports = MochaVstsReporter; + +function MochaVstsReporter(runner, options) { + MochaJUnitReporter.call(this, runner, options); + var INDENT_BASE = ' '; + var indenter = ''; + var indentLevel = 0; + var passes = 0; + var failures = 0; + var skipped = 0; + + runner.on('suite', function(suite){ + if (suite.root === true){ + console.log('Begin test run.............'); + indentLevel++; + indenter = INDENT_BASE.repeat(indentLevel); + } else { + console.log('%sStart "%s"', indenter, suite.title); + indentLevel++; + indenter = INDENT_BASE.repeat(indentLevel); + } + }); + + runner.on('suite end', function(suite){ + if (suite.root === true) { + indentLevel=0; + indenter = ''; + console.log('.............End test run.'); + } else { + console.log('%sEnd "%s"', indenter, suite.title); + indentLevel--; + indenter = INDENT_BASE.repeat(indentLevel); + // ##vso[task.setprogress]current operation + } + }); + + runner.on('pass', function(test){ + passes++; + console.log('%s✓ %s (%dms)', indenter, test.title, test.duration); + }); + + runner.on('pending', function(test){ + skipped++; + console.log('%s- %s', indenter, test.title); + console.log('##vso[task.logissue type=warning;sourcepath=%s;]SKIPPED TEST %s :: %s', test.file, test.parent.title, test.title); + }); + + runner.on('fail', function(test, err){ + failures++; + console.log('%s✖ %s -- error: %s', indenter, test.title, err.message); + console.log('##vso[task.logissue type=error;sourcepath=%s;]FAILED %s :: %s', test.file, test.parent.title, test.title); + }); + + runner.on('end', function(){ + console.log('SUMMARY: %d/%d passed, %d skipped', passes, passes + failures, skipped); + }); +} + +mocha.utils.inherits(MochaVstsReporter, MochaJUnitReporter); diff --git a/.travis.yml b/.travis.yml index f81ec0a06242..52cf8962505f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,57 +5,133 @@ matrix: include: - os: linux python: "2.7" + env: DEBUGGER_TEST=true + - os: linux + python: "2.7" + env: DEBUGGER_TEST_RELEASE=true + - os: linux + python: "2.7" + env: SINGLE_WORKSPACE_TEST=true + - os: linux + python: "2.7" + env: MULTIROOT_WORKSPACE_TEST=true + - os: linux + python: "3.6-dev" + env: DEBUGGER_TEST=true + - os: linux + python: "3.6-dev" + env: DEBUGGER_TEST_RELEASE=true + - os: linux + python: "3.6-dev" + env: SINGLE_WORKSPACE_TEST=true + - os: linux + python: "3.6-dev" + env: MULTIROOT_WORKSPACE_TEST=true + - os: linux + python: "3.6-dev" + env: PERFORMANCE_TEST=true + - os: linux + python: "3.7-dev" + env: DEBUGGER_TEST=true + - os: linux + python: "3.7-dev" + env: DEBUGGER_TEST_RELEASE=true + - os: linux + python: "3.7-dev" + env: SINGLE_WORKSPACE_TEST=true + - os: linux + python: "3.7-dev" + env: MULTIROOT_WORKSPACE_TEST=true + allow_failures: + - os: linux + python: "2.7" + env: DEBUGGER_TEST=true + - os: linux + python: "2.7" + env: DEBUGGER_TEST_RELEASE=true - os: linux python: "3.6-dev" + env: DEBUGGER_TEST=true + - os: linux + python: "3.6-dev" + env: DEBUGGER_TEST_RELEASE=true + - os: linux + python: "3.7-dev" + env: DEBUGGER_TEST=true + - os: linux + python: "3.7-dev" + env: DEBUGGER_TEST_RELEASE=true before_install: | if [ $TRAVIS_OS_NAME == "linux" ]; then export CXX="g++-4.9" CC="gcc-4.9" DISPLAY=:99.0; sh -e /etc/init.d/xvfb start; sleep 3; fi - git submodule update --init --recursive git clone https://github.com/creationix/nvm.git ./.nvm source ./.nvm/nvm.sh nvm install 8.9.1 nvm use 8.9.1 - yarn global add vsce - yarn global add azure-cli - export TRAVIS_PYTHON_PATH=`which python` + npm install npm@latest -g + npm install -g vsce + npm install -g azure-cli + export CI_PYTHON_PATH=`which python` install: - - pip install --upgrade -r requirements.txt - - pip install -t ./pythonFiles/experimental/ptvsd git+https://github.com/Microsoft/ptvsd/ - - yarn + - python -m pip install --upgrade -r requirements.txt + - python -m pip install -t ./pythonFiles/experimental/ptvsd git+https://github.com/Microsoft/ptvsd/ + - npm ci script: - - yarn run clean - - yarn run vscode:prepublish - - yarn run cover:enable - - yarn run testDebugger --silent + - if [ $DEBUGGER_TEST == "true" ]; then + npm run clean; + npm run vscode:prepublish; + npm run cover:enable; + npm run testDebugger --silent; + fi + - npm run debugger-coverage - if [ $TRAVIS_UPLOAD_COVERAGE == "true" ]; then bash <(curl -s https://codecov.io/bash); fi - - rm -rf ./pythonFiles/experimental/ptvsd - - yarn run clean - - yarn run vscode:prepublish - - yarn run cover:enable - - yarn run testSingleWorkspace --silent + - npm run clean:ptvsd + - pip install -t ./pythonFiles/experimental/ptvsd ptvsd --pre --no-cache-dir; + - if [ $DEBUGGER_TEST_RELEASE == "true" ]; then + npm run clean; + npm run vscode:prepublish; + npm run cover:enable; + npm run testDebugger --silent; + fi + - npm run debugger-coverage - if [ $TRAVIS_UPLOAD_COVERAGE == "true" ]; then bash <(curl -s https://codecov.io/bash); fi - - yarn run clean - - yarn run vscode:prepublish - - yarn run cover:enable - - yarn run testMultiWorkspace --silent + - if [ $SINGLE_WORKSPACE_TEST == "true" ]; then + npm run clean; + npm run vscode:prepublish; + npm run cover:enable; + npm run testSingleWorkspace --silent; + fi - if [ $TRAVIS_UPLOAD_COVERAGE == "true" ]; then bash <(curl -s https://codecov.io/bash); fi - - if [ "$TRAVIS_PYTHON_VERSION" != "2.7" ]; then - python3 -m pip install --upgrade -r news/requirements.txt; - python3 news/announce.py --dry-run; + - if [ $MULTIROOT_WORKSPACE_TEST == "true" ]; then + npm run clean; + npm run vscode:prepublish; + npm run cover:enable; + npm run testMultiWorkspace --silent; fi -after_success: - - if [ $AZURE_STORAGE_ACCOUNT ]; then + - if [ $TRAVIS_UPLOAD_COVERAGE == "true" ]; then + bash <(curl -s https://codecov.io/bash); + fi + - if [[ "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" == "false" && "$PERFORMANCE_TEST" == "true" ]]; then yarn run clean; + yarn run vscode:prepublish; + yarn run testPerformance --silent; + fi + - if [ "$TRAVIS_PYTHON_VERSION" == "3.7" ]; then + python3 -m pip install --upgrade -r news/requirements.txt; + python3 news/announce.py --dry_run; + fi + - if [[ $AZURE_STORAGE_ACCOUNT && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" == "false" ]]; then + npm run clean; vsce package; - azure storage blob upload --container $AZURE_STORAGE_CONTAINER --blob ms-python-insiders.vsix --account-name $AZURE_STORAGE_ACCOUNT --account-key $AZURE_STORAGE_ACCESS_KEY --file python*.vsix --quiet; + azure storage blob upload python*.vsix $AZURE_STORAGE_CONTAINER ms-python-insiders.vsix --account-name $AZURE_STORAGE_ACCOUNT --account-key $AZURE_STORAGE_ACCESS_KEY --quiet; fi diff --git a/.vscode/launch.json b/.vscode/launch.json index 264d49e34f11..4a1a1f9bd12a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,9 +11,10 @@ "--extensionDevelopmentPath=${workspaceFolder}" ], "stopOnEntry": false, + "smartStep": true, "sourceMaps": true, "outFiles": [ - "${workspaceFolder}/out/**/*.js" + "${workspaceFolder}/out/**/*" ], "preLaunchTask": "Compile" }, @@ -23,6 +24,7 @@ "request": "launch", "program": "${workspaceFolder}/out/client/debugger/Main.js", "stopOnEntry": false, + "smartStep": true, "args": [ "--server=4711" ], @@ -39,6 +41,7 @@ "request": "launch", "program": "${workspaceFolder}/out/client/debugger/mainV2.js", "stopOnEntry": false, + "smartStep": true, "args": [ "--server=4711" ], @@ -66,6 +69,22 @@ ], "preLaunchTask": "Compile" }, + { + "name": "Debug Unit Tests", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/out/test/unittests.js", + "stopOnEntry": false, + "sourceMaps": true, + "args": [ + "timeout=60000", + "grep=" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "Compile" + }, { "name": "Launch Multiroot Tests", "type": "extensionHost", @@ -78,11 +97,32 @@ ], "stopOnEntry": false, "sourceMaps": true, + "smartStep": true, "outFiles": [ - "${workspaceFolder}/out/**/*.js" + "${workspaceFolder}/out/**/*" ], "preLaunchTask": "Compile" }, + { + "name": "Launch Language Server Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/src/test", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" + ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "Compile", + "env": { + "VSC_PYTHON_LANGUAGE_SERVER": "1" + } + }, { "name": "Launch Tests (with code coverage)", "type": "extensionHost", @@ -94,6 +134,7 @@ "--extensionTestsPath=${workspaceFolder}/out/test" ], "stopOnEntry": false, + "smartStep": true, "sourceMaps": true, "outFiles": [ "${workspaceFolder}/out/**/*.js" diff --git a/.vscode/settings.json b/.vscode/settings.json index be66f967c5c4..7ce29c7c0813 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,8 @@ "files.exclude": { "out": true, // set this to true to hide the "out" folder with the compiled JS files "**/*.pyc": true, + "obj": true, + "bin": true, "**/__pycache__": true, "node_modules": true, ".vscode-test": true, @@ -19,4 +21,4 @@ "python.unitTest.promptToConfigure": false, "python.workspaceSymbols.enabled": false, "python.formatting.provider": "none" -} \ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f4384e2537bf..6165d465d00f 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -9,6 +9,32 @@ "tasks": [ { "label": "Compile", + "type": "npm", + "script": "compile", + "isBackground": true, + "problemMatcher": [ + "$tsc-watch", + { + "base": "$tslint5", + "fileLocation": "relative" + } + ], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "Run Unit Tests", + "type": "npm", + "script": "test:unittests", + "group": { + "kind": "test", + "isDefault": true + } + }, + { + "label": "Hygiene", "type": "gulp", "task": "watch", "isBackground": true, @@ -18,6 +44,24 @@ "focus": false, "panel": "dedicated" }, + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [] + }, + { + // Use this to run hygiene and display errors in problems window (very slow on Windows) + "label": "Hygiene (Problems Window)", + "type": "gulp", + "task": "watchProblems", + "isBackground": true, + "presentation": { + "echo": true, + "reveal": "never", + "focus": false, + "panel": "dedicated" + }, "problemMatcher": [ { "applyTo": "allDocuments", diff --git a/.vscodeignore b/.vscodeignore index a9397c1a60b1..4037e6fc7e54 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,37 +1,58 @@ -.vscode/** -.vscode-test/** -.github/** -.nvm/** -typings/** -out/test/** -out/src/** -out/pythonFiles/** -out/testMultiRootWkspc/** -out/coverconfig.json -test/** -src/** -scripts/** **/*.map -.gitignore -.gitmodules +.appveyor.yml .editorconfig .eslintrc .gitattributes -images/**/*.gif -images/**/*.png -tsconfig.json -typings.json +.gitignore +.gitmodules +.npmrc +.travis.yml +CODE_OF_CONDUCT.md +CODING_STANDARDS.md +CONTRIBUTING.md +CONTRIBUTING - LANGUAGE SERVER.md coverconfig.json -tslint.json -tsfmt.json gulpfile.js -pythonFiles/**/*.pyc -requirements.txt -vsc-extension-quickstart.md -.travis.yml +packageExtension.cmd +tsconfig.json +tsfmt.json +tslint.json +typings.json +vscode-python-signing.* webpack.config.js yarn.lock + +.github/** +.nvm/** +.vscode/** +.vscode-test/** +languageServer/** +bin/** +BuildOutput/** coverage/** -CODE_OF_CONDUCT.md -CODING_STANDARDS.md -CONTRIBUTING.md +debug_coverage*/** +images/**/*.gif +images/**/*.png +news/** +obj/** +out/coverconfig.json +out/pythonFiles/** +out/src/** +out/test/** +out/testMultiRootWkspc/** +pythonFiles/**/*.pyc +requirements.txt +scripts/** +src/** +test/** +tmp/** +typeshed/tests/** +typeshed/.flake8 +typeshed/.git +typeshed/.gitignore +typeshed/.travis.yml +typeshed/CONTRIBUTING.md +typeshed/README.md +typeshed/*.txt +typings/** +vsc-extension-quickstart.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d746daec36e..bc8c712281b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,662 @@ # Changelog +## 2018.7.1 (23 July 2018) + +### Fixes + +1. Update the language server to code as of + [651468731500ec1cc644029c3666c57b82f77d76](https://github.com/Microsoft/PTVS/commit/651468731500ec1cc644029c3666c57b82f77d76). + ([#2233](https://github.com/Microsoft/vscode-python/issues/2233)) + + +## 2018.7.0 (18 July 2018) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [ptvsd 3.0.0](https://pypi.org/project/ptvsd/3.0.0/) and [4.1.11a5](https://pypi.org/project/ptvsd/4.1.11a5/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +### Enhancements + +1. Language server now reports code analysis progress in the status bar. + ([#1591](https://github.com/Microsoft/vscode-python/issues/1591)) +1. Only report Language Server download progress once. + ([#2000](https://github.com/Microsoft/vscode-python/issues/2000)) +1. Messages changes to reflect name of the language server: 'Microsoft Python Language Server'; + folder name changed from `analysis` to `languageServer`. + ([#2107](https://github.com/Microsoft/vscode-python/issues/2107)) +1. Set default analysis for language server to open files only. + ([#2113](https://github.com/Microsoft/vscode-python/issues/2113)) +1. Add two popups to the extension: one to ask users to move to the new language server, the other to request feedback from users of that language server. + ([#2127](https://github.com/Microsoft/vscode-python/issues/2127)) + +### Fixes + +1. Ensure dunder variables are always displayed in code completion when using the new language server. + ([#2013](https://github.com/Microsoft/vscode-python/issues/2013)) +1. Store testId for files & suites during unittest discovery. + ([#2044](https://github.com/Microsoft/vscode-python/issues/2044)) +1. `editor.formatOnType` no longer adds space after `*` in multi-line arguments. + ([#2048](https://github.com/Microsoft/vscode-python/issues/2048)) +1. Fix bug where tooltips would popup whenever a comma is typed within a string. + ([#2057](https://github.com/Microsoft/vscode-python/issues/2057)) +1. Change keyboard shortcut for `Run Selection/Line in Python Terminal` to not + interfere with the Find/Replace dialog box. + ([#2068](https://github.com/Microsoft/vscode-python/issues/2068)) +1. Relax validation of the environment `Path` variable. + ([#2076](https://github.com/Microsoft/vscode-python/issues/2076)) +1. `editor.formatOnType` is more reliable handling floating point numbers. + ([#2079](https://github.com/Microsoft/vscode-python/issues/2079)) +1. Change the default port used in remote debugging using `Experimental` debugger to `5678`. + ([#2146](https://github.com/Microsoft/vscode-python/issues/2146)) +1. Register test manager when using the new language server. + ([#2186](https://github.com/Microsoft/vscode-python/issues/2186)) + +### Code Health + +1. Removed pre-commit hook that ran unit tests. + ([#1986](https://github.com/Microsoft/vscode-python/issues/1986)) +1. Pass OS type to the debugger. + ([#2128](https://github.com/Microsoft/vscode-python/issues/2128)) +1. Ensure 'languageServer' directory is excluded from the build output. + ([#2150](https://github.com/Microsoft/vscode-python/issues/2150)) +1. Change the download links of the language server files. + ([#2180](https://github.com/Microsoft/vscode-python/issues/2180)) + + + +## 2018.6.0 (20 June 2018) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [ptvsd 3.0.0](https://pypi.org/project/ptvsd/3.0.0/) and [4.1.11a5](https://pypi.org/project/ptvsd/4.1.11a5/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +### Enhancements + +1. Add setting to control automatic test discovery on save, `python.unitTest.autoTestDiscoverOnSaveEnabled`. + (thanks [Lingyu Li](http://github.com/lingyv-li/)) + ([#1037](https://github.com/Microsoft/vscode-python/issues/1037)) +1. Add `gevent` launch configuration option to enable debugging of gevent monkey patched code. + (thanks [Bence Nagy](https://github.com/underyx)) + ([#127](https://github.com/Microsoft/vscode-python/issues/127)) +1. Add support for the `"source.organizeImports"` setting for `"editor.codeActionsOnSave"` (thanks [Nathan Gaberel](https://github.com/n6g7)); you can turn this on just for Python using: + ```json + "[python]": { + "editor.codeActionsOnSave": { + "source.organizeImports": true + } + } + ``` + ([#156](https://github.com/Microsoft/vscode-python/issues/156)) +1. Added Spanish translation. + (thanks [Mario Rubio](https://github.com/mario-mra/)) + ([#1902](https://github.com/Microsoft/vscode-python/issues/1902)) +1. Add a French translation (thanks to [Jérémy](https://github.com/PixiBixi) for + the initial patch, and thanks to [Nathan Gaberel](https://github.com/n6g7), + [Bruno Alla](https://github.com/browniebroke), and + [Tarek Ziade](https://github.com/tarekziade) for reviews). + ([#1959](https://github.com/Microsoft/vscode-python/issues/1959)) +1. Add syntax highlighting for [Pipenv](http://pipenv.readthedocs.io/en/latest/)-related + files (thanks [Nathan Gaberel](https://github.com/n6g7)). + ([#995](https://github.com/Microsoft/vscode-python/issues/995)) + +### Fixes + +1. Modified to change error message displayed when path to a tool (`linter`, `formatter`, etc) is invalid. + ([#1064](https://github.com/Microsoft/vscode-python/issues/1064)) +1. Improvements to the logic used to parse the arguments passed into the test frameworks. + ([#1070](https://github.com/Microsoft/vscode-python/issues/1070)) +1. Ensure navigation to definitons follows imports and is transparent to decoration. + (thanks [Peter Law](https://github.com/PeterJCLaw)) + ([#1638](https://github.com/Microsoft/vscode-python/issues/1638)) +1. Fix for intellisense failing when using the new `Outline` feature. + ([#1721](https://github.com/Microsoft/vscode-python/issues/1721)) +1. When debugging unit tests, use the `env` file configured in `settings.json` under `python.envFile`. + ([#1759](https://github.com/Microsoft/vscode-python/issues/1759)) +1. Fix to display all interpreters in the interpreter list when a workspace contains a `Pipfile`. + ([#1800](https://github.com/Microsoft/vscode-python/issues/1800)) +1. Use file system API to perform file path comparisons when performing code navigation. + (thanks to [bstaint](https://github.com/bstaint) for the problem diagnosis) + ([#1811](https://github.com/Microsoft/vscode-python/issues/1811)) +1. Automatically add path mappings for remote debugging when attaching to the localhost. + ([#1829](https://github.com/Microsoft/vscode-python/issues/1829)) +1. Change keyboard shortcut for `Run Selection/Line in Python Terminal` to `Shift+Enter`. + ([#1875](https://github.com/Microsoft/vscode-python/issues/1875)) +1. Fix unhandled rejected promises in unit tests. + ([#1919](https://github.com/Microsoft/vscode-python/issues/1919)) +1. Fix debugger issue that causes the debugger to hang and silently exit stepping over a line of code instantiating an ITK vector object. + ([#459](https://github.com/Microsoft/vscode-python/issues/459)) + +### Code Health + +1. Add telemetry to capture type of python interpreter used in workspace. + ([#1237](https://github.com/Microsoft/vscode-python/issues/1237)) +1. Enabled multi-thrreaded debugger tests for the `experimental` debugger. + ([#1250](https://github.com/Microsoft/vscode-python/issues/1250)) +1. Log relevant environment information when the existence of `pipenv` cannot be determined. + ([#1338](https://github.com/Microsoft/vscode-python/issues/1338)) +1. Use [dotenv](https://www.npmjs.com/package/dotenv) package to parse [environment variables definition files](https://code.visualstudio.com/docs/python/environments#_environment-variable-definitions-file). + ([#1376](https://github.com/Microsoft/vscode-python/issues/1376)) +1. Move from yarn to npm. + ([#1402](https://github.com/Microsoft/vscode-python/issues/1402)) +1. Fix django and flask debugger tests when using the experimental debugger. + ([#1407](https://github.com/Microsoft/vscode-python/issues/1407)) +1. Capture telemetry for the usage of the `Create Terminal` command along with other instances when a terminal is created implicitly. + ([#1542](https://github.com/Microsoft/vscode-python/issues/1542)) +1. Add telemetry to capture availability of Python 3, version of Python used in workspace and the number of workspace folders. + ([#1545](https://github.com/Microsoft/vscode-python/issues/1545)) +1. Ensure all CI tests (except for debugger) are no longer allowed to fail. + ([#1614](https://github.com/Microsoft/vscode-python/issues/1614)) +1. Capture telemetry for the usage of the feature that formats a line as you type (`editor.formatOnType`). + ([#1766](https://github.com/Microsoft/vscode-python/issues/1766)) +1. Capture telemetry for the new debugger. + ([#1767](https://github.com/Microsoft/vscode-python/issues/1767)) +1. Capture telemetry for usage of the setting `python.autocomplete.addBrackets` + ([#1770](https://github.com/Microsoft/vscode-python/issues/1770)) +1. Speed up githook by skipping commits not containing any `.ts` files. + ([#1803](https://github.com/Microsoft/vscode-python/issues/1803)) +1. Update typescript package to 2.9.1. + ([#1815](https://github.com/Microsoft/vscode-python/issues/1815)) +1. Log Conda not existing message as an information instead of an error. + ([#1817](https://github.com/Microsoft/vscode-python/issues/1817)) +1. Make use of `ILogger` to log messages instead of using `console.error`. + ([#1821](https://github.com/Microsoft/vscode-python/issues/1821)) +1. Update `parso` package to 0.2.1. + ([#1833](https://github.com/Microsoft/vscode-python/issues/1833)) +1. Update `isort` package to 4.3.4. + ([#1842](https://github.com/Microsoft/vscode-python/issues/1842)) +1. Add better exception handling when parsing responses received from the Jedi language service. + ([#1867](https://github.com/Microsoft/vscode-python/issues/1867)) +1. Resolve warnings in CI Tests and fix some broken CI tests. + ([#1885](https://github.com/Microsoft/vscode-python/issues/1885)) +1. Reduce sample count used to capture performance metrics in order to reduce time taken to complete the tests. + ([#1887](https://github.com/Microsoft/vscode-python/issues/1887)) +1. Ensure workspace information is passed into installer when determining whether a product/tool is installed. + ([#1893](https://github.com/Microsoft/vscode-python/issues/1893)) +1. Add JUnit file output to enable CI integration with VSTS. + ([#1897](https://github.com/Microsoft/vscode-python/issues/1897)) +1. Log unhandled rejected promises when running unit tests. + ([#1918](https://github.com/Microsoft/vscode-python/issues/1918)) +1. Add ability to run tests without having to launch VS Code. + ([#1922](https://github.com/Microsoft/vscode-python/issues/1922)) +1. Fix rename refactoring unit tests. + ([#1953](https://github.com/Microsoft/vscode-python/issues/1953)) +1. Fix failing test on Mac when validating the path of a python interperter. + ([#1957](https://github.com/Microsoft/vscode-python/issues/1957)) +1. Display banner prompting user to complete a survey for the use of the `Experimental Debugger`. + ([#1968](https://github.com/Microsoft/vscode-python/issues/1968)) +1. Use a glob pattern to look for `conda` executables. + ([#256](https://github.com/Microsoft/vscode-python/issues/256)) +1. Create tests to measure activation times for the extension. + ([#932](https://github.com/Microsoft/vscode-python/issues/932)) + + + + + +## 2018.5.0 (05 Jun 2018) + +Thanks to the following projects which we fully rely on to provide some of +our features: +- [isort 4.2.15](https://pypi.org/project/isort/4.2.15/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.0](https://pypi.org/project/parso/0.2.0/) +- [ptvsd 3.0.0](https://pypi.org/project/ptvsd/3.0.0/) and [4.1.1a5](https://pypi.org/project/ptvsd/4.1.1a5/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +And thanks to the many other projects which users can optionally choose from +and install to work with the extension. Without them the extension would not be +nearly as feature-rich and useful as it is. + +### Enhancements + +1. Add support for the [Black formatter](https://pypi.org/project/black/) + (thanks to [Josh Smeaton](https://github.com/jarshwah) for the initial patch) + ([#1153](https://github.com/Microsoft/vscode-python/issues/1153)) +1. Add the command `Discover Unit Tests`. + ([#1474](https://github.com/Microsoft/vscode-python/issues/1474)) +1. Auto detect `*.jinja2` and `*.j2` extensions as Jinja templates, to enable debugging of Jinja templates. + ([#1484](https://github.com/Microsoft/vscode-python/issues/1484)) + +### Fixes + +1. Ensure debugger breaks on `assert` failures. + ([#1194](https://github.com/Microsoft/vscode-python/issues/1194)) +1. Ensure debugged program is terminated when `Stop` debugging button is clicked. + ([#1345](https://github.com/Microsoft/vscode-python/issues/1345)) +1. Fix indentation when function contains type hints. + ([#1461](https://github.com/Microsoft/vscode-python/issues/1461)) +1. Ensure python environment activation works as expected within a multi-root workspace. + ([#1476](https://github.com/Microsoft/vscode-python/issues/1476)) +1. Close communication channel before exiting the test runner. + ([#1529](https://github.com/Microsoft/vscode-python/issues/1529)) +1. Allow for negative column numbers in messages returned by `pylint`. + ([#1628](https://github.com/Microsoft/vscode-python/issues/1628)) +1. Modify the `FLASK_APP` environment variable in the flask debug configuration to include just the name of the application file. + ([#1634](https://github.com/Microsoft/vscode-python/issues/1634)) +1. Ensure the display name of an interpreter does not get prefixed twice with the words `Python`. + ([#1651](https://github.com/Microsoft/vscode-python/issues/1651)) +1. Enable code refactoring when using the new Analysis Engine. + ([#1774](https://github.com/Microsoft/vscode-python/issues/1774)) +1. `editor.formatOnType` no longer breaks numbers formatted with underscores. + ([#1779](https://github.com/Microsoft/vscode-python/issues/1779)) +1. `editor.formatOnType` now better handles multiline function arguments + ([#1796](https://github.com/Microsoft/vscode-python/issues/1796)) +1. `Go to Definition` now works for functions which have numbers that use `_` as a separator (as part of our Jedi 0.12.0 upgrade). + ([#180](https://github.com/Microsoft/vscode-python/issues/180)) +1. Display documentation for auto completion items when the feature to automatically insert of brackets for selected item is turned on. + ([#452](https://github.com/Microsoft/vscode-python/issues/452)) +1. Ensure empty paths do not get added into `sys.path` by the Jedi language server. (this was fixed in the previous release in [#1471](https://github.com/Microsoft/vscode-python/pull/1471)) + ([#677](https://github.com/Microsoft/vscode-python/issues/677)) +1. Resolves rename refactor issue that remvoes the last line of the source file when the line is being refactored and source does not end with an EOL. + ([#695](https://github.com/Microsoft/vscode-python/issues/695)) +1. Ensure the prompt to install missing packages is not displayed more than once. + ([#980](https://github.com/Microsoft/vscode-python/issues/980)) + +### Code Health + +1. Add syntax highlighting to `constraints.txt` files to match that of `requirements.txt` files. + (thanks [Waleed Sehgal](https://github.com/waleedsehgal)) + ([#1053](https://github.com/Microsoft/vscode-python/issues/1053)) +1. Refactor unit testing functionality to improve testability of individual components. + ([#1068](https://github.com/Microsoft/vscode-python/issues/1068)) +1. Add unit tests for evaluating expressions in the experimental debugger. + ([#1109](https://github.com/Microsoft/vscode-python/issues/1109)) +1. Add tests to ensure custom arguments get passed into python program when using the experimental debugger. + ([#1280](https://github.com/Microsoft/vscode-python/issues/1280)) +1. Ensure custom environment variables are always used when spawning any process from within the extension. + ([#1339](https://github.com/Microsoft/vscode-python/issues/1339)) +1. Add tests for hit count breakpoints for the experimental debugger. + ([#1410](https://github.com/Microsoft/vscode-python/issues/1410)) +1. Ensure none of the npm packages (used by the extension) rely on native dependencies. + ([#1416](https://github.com/Microsoft/vscode-python/issues/1416)) +1. Remove explicit initialization of `PYTHONPATH` with the current workspace path in unit testing of modules with the experimental debugger. + ([#1465](https://github.com/Microsoft/vscode-python/issues/1465)) +1. Flag `program` in `launch.json` configuration items as an optional attribute. + ([#1503](https://github.com/Microsoft/vscode-python/issues/1503)) +1. Remove unused setting `disablePromptForFeatures`. + ([#1551](https://github.com/Microsoft/vscode-python/issues/1551)) +1. Remove unused Unit Test setting `debugHost`. + ([#1552](https://github.com/Microsoft/vscode-python/issues/1552)) +1. Create a new API to retrieve interpreter details with the ability to cache the details. + ([#1569](https://github.com/Microsoft/vscode-python/issues/1569)) +1. Add tests for log points in the experimental debugger. + ([#1582](https://github.com/Microsoft/vscode-python/issues/1582)) +1. Update typescript package to 2.8.3. + ([#1604](https://github.com/Microsoft/vscode-python/issues/1604)) +1. Fix typescript compilation error. + ([#1623](https://github.com/Microsoft/vscode-python/issues/1623)) +1. Fix unit tests used to test flask template debugging on AppVeyor for the experimental debugger. + ([#1640](https://github.com/Microsoft/vscode-python/issues/1640)) +1. Change yarn install script to include the keyword `--lock-file`. + (thanks [Lingyu Li](https://github.com/lingyv-li/)) + ([#1682](https://github.com/Microsoft/vscode-python/issues/1682)) +1. Run unit tests as a pre-commit hook. + ([#1703](https://github.com/Microsoft/vscode-python/issues/1703)) +1. Update debug capabilities to add support for the setting `supportTerminateDebuggee` due to an upstream update from [PTVSD](https://github.com/Microsoft/ptvsd/issues). + ([#1719](https://github.com/Microsoft/vscode-python/issues/1719)) +1. Build and upload development build of the extension to the Azure blob store even if CI tests fail on the `master` branch. + ([#1730](https://github.com/Microsoft/vscode-python/issues/1730)) +1. Changes to the script used to upload the extension to the Azure blob store. + ([#1732](https://github.com/Microsoft/vscode-python/issues/1732)) +1. Prompt user to reload Visual Studio Code when toggling between the analysis engines. + ([#1747](https://github.com/Microsoft/vscode-python/issues/1747)) +1. Fix typo in unit test. + ([#1794](https://github.com/Microsoft/vscode-python/issues/1794)) +1. Fix failing Prospector unit tests and add more tests for linters (with and without workspaces). + ([#1836](https://github.com/Microsoft/vscode-python/issues/1836)) +1. Ensure `Outline` view doesn't overload the language server with too many requets, while user is editing text in the editor. + ([#1856](https://github.com/Microsoft/vscode-python/issues/1856)) + + + + + +## 2018.4.0 (2 May 2018) + +Thanks to the following projects which we fully rely on to provide some of +our features: +- [isort 4.2.15](https://pypi.org/project/isort/4.2.15/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.0](https://pypi.org/project/parso/0.2.0/) +- [ptvsd 3.0.0](https://pypi.org/project/ptvsd/3.0.0/) and [4.1.1a1](https://pypi.org/project/ptvsd/4.1.1a1/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +And a special thanks to [Patryk Zawadzki](https://github.com/patrys) for all of +his help on [our issue tracker](https://github.com/Microsoft/vscode-python)! + +### Enhancements + +1. Enable debugging of Jinja templates in the experimental debugger. + This is made possible with the addition of the `jinja` setting in the `launch.json` file as follows: + ```json + "request": "launch or attach", + ... + "jinja": true + ``` + ([#1206](https://github.com/Microsoft/vscode-python/issues/1206)) +1. Remove empty spaces from the selected text of the active editor when executing in a terminal. + ([#1207](https://github.com/Microsoft/vscode-python/issues/1207)) +1. Add prelimnary support for remote debugging using the experimental debugger. + Attach to a Python program started using the command `python -m ptvsd --server --port 9091 --file pythonFile.py` + ([#1229](https://github.com/Microsoft/vscode-python/issues/1229)) +1. Add support for [logpoints](https://code.visualstudio.com/docs/editor/debugging#_logpoints) in the experimental debugger. + ([#1306](https://github.com/Microsoft/vscode-python/issues/1306)) +1. Set focus to the terminal upon creation of a terminal using the `Python: Create Terminal` command. + ([#1315](https://github.com/Microsoft/vscode-python/issues/1315)) +1. Save the python file before running it in the terminal using the command/menu `Run Python File in Terminal`. + ([#1316](https://github.com/Microsoft/vscode-python/issues/1316)) +1. Added support for source references (remote debugging without having the source code locally) in the experimental debugger. + ([#1333](https://github.com/Microsoft/vscode-python/issues/1333)) +1. Add `Ctrl+Enter` keyboard shortcut for `Run Selection/Line in Python Terminal`. + ([#1349](https://github.com/Microsoft/vscode-python/issues/1349)) +1. Settings configured within the `debugOptions` property of `launch.json` for the old debugger are now defined as individual (boolean) properties in the new experimental debugger (e.g. `"debugOptions": ["RedirectOutput"]` becomes `"redirectOutput": true`). + ([#1395](https://github.com/Microsoft/vscode-python/issues/1395)) +1. Intergrate Jedi 0.12. See https://github.com/davidhalter/jedi/issues/1063#issuecomment-381417297 for details. + ([#1400](https://github.com/Microsoft/vscode-python/issues/1400)) +1. Enable Jinja template debugging as a default behaivour when using the Watson debug configuration for debugging of Watson applications. + ([#1480](https://github.com/Microsoft/vscode-python/issues/1480)) +1. Enable Jinja template debugging as a default behavior when debugging Pyramid applications. + ([#1492](https://github.com/Microsoft/vscode-python/issues/1492)) +1. Add prelimnary support for remote debugging using the experimental debugger. + Attach to a Python program after having imported `ptvsd` and enabling the debugger to attach as follows: + ```python + import ptvsd + ptvsd.enable_attach(('0.0.0.0', 5678)) + ``` + Additional capabilities: + * `ptvsd.break_into_debugger()` to break into the attached debugger. + * `ptvsd.wait_for_attach(timeout)` to cause the program to wait untill a debugger attaches. + * `ptvsd.is_attached()` to determine whether a debugger is attached to the program. + ([#907](https://github.com/Microsoft/vscode-python/issues/907)) + +### Fixes + +1. Use an existing method to identify the active interpreter. + ([#1015](https://github.com/Microsoft/vscode-python/issues/1015)) +1. Fix `go to definition` functionality across files. + ([#1033](https://github.com/Microsoft/vscode-python/issues/1033)) +1. IntelliSense under Python 2 for inherited attributes works again (thanks to an upgraded Jedi). + ([#1072](https://github.com/Microsoft/vscode-python/issues/1072)) +1. Reverted change that ended up considering symlinked interpreters as duplicate interpreter. + ([#1192](https://github.com/Microsoft/vscode-python/issues/1192)) +1. Display errors returned by the PipEnv command when identifying the corresonding environment. + ([#1254](https://github.com/Microsoft/vscode-python/issues/1254)) +1. When `editor.formatOnType` is on, don't add a space for `*args` or `**kwargs` + ([#1257](https://github.com/Microsoft/vscode-python/issues/1257)) +1. When `editor.formatOnType` is on, don't add a space between a string type specifier and the string literal + ([#1257](https://github.com/Microsoft/vscode-python/issues/1257)) +1. Reduce the frequency within which the memory usage of the language server is checked, also ensure memory usage is not checked unless language server functionality is used. + ([#1277](https://github.com/Microsoft/vscode-python/issues/1277)) +1. Ensure interpreter file exists on the file system before including into list of interpreters. + ([#1305](https://github.com/Microsoft/vscode-python/issues/1305)) +1. Do not have the formatter consider single-quoted string multiline even if it is not terminated. + ([#1364](https://github.com/Microsoft/vscode-python/issues/1364)) +1. IntelliSense works in module-level `if` statements (thanks to Jedi 0.12.0 upgrade). + ([#142](https://github.com/Microsoft/vscode-python/issues/142)) +1. Clicking the codelens `Run Test` on a test class should run that specific test class instead of all tests in the file. + ([#1472](https://github.com/Microsoft/vscode-python/issues/1472)) +1. Clicking the codelens `Run Test` on a test class or method should run that specific test instead of all tests in the file. + ([#1473](https://github.com/Microsoft/vscode-python/issues/1473)) +1. Check whether the selected python interpreter is valid before starting the language server. Failing to do so could result in the extension failing to load. + ([#1487](https://github.com/Microsoft/vscode-python/issues/1487)) +1. Fixes the issue where Conda environments created using the latest version of Anaconda are not activated in Powershell. + ([#1520](https://github.com/Microsoft/vscode-python/issues/1520)) +1. Increase the delay for the activation of environments in Powershell terminals. + ([#1533](https://github.com/Microsoft/vscode-python/issues/1533)) +1. Fix activation of environments with spaces in the python path when using Powershell. + ([#1534](https://github.com/Microsoft/vscode-python/issues/1534)) +1. Ensure Flask application is launched with multi-threading disabled, when run in the CI tests. + ([#1535](https://github.com/Microsoft/vscode-python/issues/1535)) +1. IntelliSense works appropriately when a project contains multiple files with the same name (thanks to Jedi 0.12.0 update). + ([#178](https://github.com/Microsoft/vscode-python/issues/178)) +1. Add blank lines to separate blocks of indented code (function defs, classes, and the like) so as to ensure the code can be run within a Python interactive prompt. + ([#259](https://github.com/Microsoft/vscode-python/issues/259)) +1. Provide type details appropriate for the iterable in a `for` loop when the line has a `# type` comment. + ([#338](https://github.com/Microsoft/vscode-python/issues/338)) +1. Parameter hints following an f-string work again. + ([#344](https://github.com/Microsoft/vscode-python/issues/344)) +1. When `editor.formatOnType` is on, don't indent after a single-line statement block + ([#726](https://github.com/Microsoft/vscode-python/issues/726)) +1. Fix debugging of Pyramid applications on Windows. + ([#737](https://github.com/Microsoft/vscode-python/issues/737)) + +### Code Health + +1. Improved developer experience of the Python Extension on Windows. + ([#1216](https://github.com/Microsoft/vscode-python/issues/1216)) +1. Parallelize jobs (unit tests) on CI server. + ([#1247](https://github.com/Microsoft/vscode-python/issues/1247)) +1. Run CI tests against the release version and master branch of PTVSD (experimental debugger), allowing tests to fail against the master branch of PTVSD. + ([#1253](https://github.com/Microsoft/vscode-python/issues/1253)) +1. Only trigger the extension for `file` and `untitled` in preparation for + [Visual Studio Live Share](https://aka.ms/vsls) + (thanks to [Jonathan Carter](https://github.com/lostintangent)) + ([#1298](https://github.com/Microsoft/vscode-python/issues/1298)) +1. Ensure all unit tests run on Travis use the right Python interpreter. + ([#1318](https://github.com/Microsoft/vscode-python/issues/1318)) +1. Pin all production dependencies. + ([#1374](https://github.com/Microsoft/vscode-python/issues/1374)) +1. Add support for [hit count breakpoints](https://code.visualstudio.com/docs/editor/debugging#_advanced-breakpoint-topics) in the experimental debugger. + ([#1409](https://github.com/Microsoft/vscode-python/issues/1409)) +1. Ensure custom environment variables defined in `.env` file are passed onto the `pipenv` command. + ([#1428](https://github.com/Microsoft/vscode-python/issues/1428)) +1. Remove unwanted python packages no longer used in unit tests. + ([#1494](https://github.com/Microsoft/vscode-python/issues/1494)) +1. Register language server functionality in the extension against specific resource types supporting the python language. + ([#1530](https://github.com/Microsoft/vscode-python/issues/1530)) + + +## 2018.3.1 (29 Mar 2018) + +### Fixes + +1. Fixes issue that causes linter to fail when file path contains spaces. +([#1239](https://github.com/Microsoft/vscode-python/issues/1239)) + +## 2018.3.0 (28 Mar 2018) + +### Enhancements + +1. Add a PySpark debug configuration for the experimental debugger. + ([#1029](https://github.com/Microsoft/vscode-python/issues/1029)) +1. Add a Pyramid debug configuration for the experimental debugger. + ([#1030](https://github.com/Microsoft/vscode-python/issues/1030)) +1. Add a Watson debug configuration for the experimental debugger. + ([#1031](https://github.com/Microsoft/vscode-python/issues/1031)) +1. Add a Scrapy debug configuration for the experimental debugger. + ([#1032](https://github.com/Microsoft/vscode-python/issues/1032)) +1. When using pipenv, install packages (such as linters, test frameworks) in dev-packages. + ([#1110](https://github.com/Microsoft/vscode-python/issues/1110)) +1. Added commands translation for italian locale. +(thanks [Dotpys](https://github.com/Dotpys/)) ([#1152](https://github.com/Microsoft/vscode-python/issues/1152)) +1. Add support for Django Template debugging in experimental debugger. + ([#1189](https://github.com/Microsoft/vscode-python/issues/1189)) +1. Add support for Flask Template debugging in experimental debugger. + ([#1190](https://github.com/Microsoft/vscode-python/issues/1190)) +1. Add support for Jinja template debugging. ([#1210](https://github.com/Microsoft/vscode-python/issues/1210)) +1. When debugging, use `Integrated Terminal` as the default console. + ([#526](https://github.com/Microsoft/vscode-python/issues/526)) +1. Disable the display of errors messages when rediscovering of tests fail in response to changes to files, e.g. don't show a message if there's a syntax error in the test code. + ([#704](https://github.com/Microsoft/vscode-python/issues/704)) +1. Bundle python depedencies (PTVSD package) in the extension for the experimental debugger. + ([#741](https://github.com/Microsoft/vscode-python/issues/741)) +1. Add support for expermental debugger when debugging Python Unit Tests. + ([#906](https://github.com/Microsoft/vscode-python/issues/906)) +1. Support `Debug Console` as a `console` option for the Experimental Debugger. + ([#950](https://github.com/Microsoft/vscode-python/issues/950)) +1. Enable syntax highlighting for `requirements.in` files as used by +e.g. [pip-tools](https://github.com/jazzband/pip-tools) +(thanks [Lorenzo Villani](https://github.com/lvillani)) + ([#961](https://github.com/Microsoft/vscode-python/issues/961)) +1. Add support to read name of Pipfile from environment variable. + ([#999](https://github.com/Microsoft/vscode-python/issues/999)) + +### Fixes + +1. Fixes issue that causes debugging of unit tests to hang indefinitely. ([#1009](https://github.com/Microsoft/vscode-python/issues/1009)) +1. Add ability to disable the check on memory usage of language server (Jedi) process. +To turn off this check, add `"python.jediMemoryLimit": -1` to your user or workspace settings (`settings.json`) file. + ([#1036](https://github.com/Microsoft/vscode-python/issues/1036)) +1. Ignore test results when debugging unit tests. + ([#1043](https://github.com/Microsoft/vscode-python/issues/1043)) +1. Fixes auto formatting of conditional statements containing expressions with `<=` symbols. + ([#1096](https://github.com/Microsoft/vscode-python/issues/1096)) +1. Resolve debug configuration information in `launch.json` when debugging without opening a python file. + ([#1098](https://github.com/Microsoft/vscode-python/issues/1098)) +1. Disables auto completion when editing text at the end of a comment string. + ([#1123](https://github.com/Microsoft/vscode-python/issues/1123)) +1. Ensures file paths are properly encoded when passing them as arguments to linters. + ([#199](https://github.com/Microsoft/vscode-python/issues/199)) +1. Fix occasionally having unverified breakpoints + ([#87](https://github.com/Microsoft/vscode-python/issues/87)) +1. Ensure conda installer is not used for non-conda environments. + ([#969](https://github.com/Microsoft/vscode-python/issues/969)) +1. Fixes issue that display incorrect interpreter briefly before updating it to the right value. + ([#981](https://github.com/Microsoft/vscode-python/issues/981)) + +### Code Health + +1. Exclude 'news' folder from getting packaged into the extension. + ([#1020](https://github.com/Microsoft/vscode-python/issues/1020)) +1. Remove Jupyter commands. +(thanks [Yu Zhang](https://github.com/neilsustc)) + ([#1034](https://github.com/Microsoft/vscode-python/issues/1034)) +1. Trigger incremental build compilation only when typescript files are modified. + ([#1040](https://github.com/Microsoft/vscode-python/issues/1040)) +1. Updated npm dependencies in devDependencies and fix TypeScript compilation issues. + ([#1042](https://github.com/Microsoft/vscode-python/issues/1042)) +1. Enable unit testing of stdout and stderr redirection for the experimental debugger. + ([#1048](https://github.com/Microsoft/vscode-python/issues/1048)) +1. Update npm package `vscode-extension-telemetry` to fix the warning 'os.tmpDir() deprecation'. +(thanks [osya](https://github.com/osya)) + ([#1066](https://github.com/Microsoft/vscode-python/issues/1066)) +1. Prevent the debugger stepping into JS code while developing the extension when debugging async TypeScript code. + ([#1090](https://github.com/Microsoft/vscode-python/issues/1090)) +1. Increase timeouts for the debugger unit tests. + ([#1094](https://github.com/Microsoft/vscode-python/issues/1094)) +1. Change the command used to install pip on AppVeyor to avoid installation errors. + ([#1107](https://github.com/Microsoft/vscode-python/issues/1107)) +1. Check whether a document is active when detecthing changes in the active document. + ([#1114](https://github.com/Microsoft/vscode-python/issues/1114)) +1. Remove SIGINT handler in debugger adapter, thereby preventing it from shutting down the debugger. + ([#1122](https://github.com/Microsoft/vscode-python/issues/1122)) +1. Improve compilation speed of the extension's TypeScript code. + ([#1146](https://github.com/Microsoft/vscode-python/issues/1146)) +1. Changes to how debug options are passed into the experimental version of PTVSD (debugger). + ([#1168](https://github.com/Microsoft/vscode-python/issues/1168)) +1. Ensure file paths are not sent in telemetry when running unit tests. + ([#1180](https://github.com/Microsoft/vscode-python/issues/1180)) +1. Change `DjangoDebugging` to `Django` in `debugOptions` of launch.json. + ([#1198](https://github.com/Microsoft/vscode-python/issues/1198)) +1. Changed property name used to capture the trigger source of Unit Tests. ([#1213](https://github.com/Microsoft/vscode-python/issues/1213)) +1. Enable unit testing of the experimental debugger on CI servers + ([#742](https://github.com/Microsoft/vscode-python/issues/742)) +1. Generate code coverage for debug adapter unit tests. + ([#778](https://github.com/Microsoft/vscode-python/issues/778)) +1. Execute prospector as a module (using -m). + ([#982](https://github.com/Microsoft/vscode-python/issues/982)) +1. Launch unit tests in debug mode as opposed to running and attaching the debugger to the already-running interpreter. + ([#983](https://github.com/Microsoft/vscode-python/issues/983)) + +## 2018.2.1 (09 Mar 2018) + +### Fixes + +1. Check for `Pipfile` and not `pipfile` when looking for pipenv usage + (thanks to [Will Thompson for the fix](https://github.com/wjt)) + ## 2018.2.0 (08 Mar 2018) [Release pushed by one week] @@ -717,4 +1374,3 @@ the following people who contributed code: ## Version 0.0.3 * Added support for debugging using PDB - diff --git a/CODING_STANDARDS.md b/CODING_STANDARDS.md index bafa2de536e7..1a1a8bfc401e 100644 --- a/CODING_STANDARDS.md +++ b/CODING_STANDARDS.md @@ -1,5 +1,5 @@ ## Coding guidelines for TypeScript -* The following standards are inspired from [Coding guidelines for TypeScript](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines). +* The following standards are inspired from [Coding guidelines for TypeScript](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines) (which you should follow when something is not specified in this document, although any pre-existing practices in a file being edited trump either style guide). ### Names @@ -31,6 +31,11 @@ Use undefined. Do not use null. Use single quotes for strings. +### Imports + +* Use ES6 module imports. +* Do not use bare `import *`; all imports should either explicitly pull in an object or import an entire module, otherwise you're implicitly polluting the global namespace and making it difficult to figure out from code examination where a name originates from. + ### Style * Use arrow functions over anonymous function expressions. diff --git a/CONTRIBUTING - LANGUAGE SERVER.md b/CONTRIBUTING - LANGUAGE SERVER.md new file mode 100644 index 000000000000..7c363ce23b81 --- /dev/null +++ b/CONTRIBUTING - LANGUAGE SERVER.md @@ -0,0 +1,63 @@ +# Contributing to Microsoft Python Language Server +[![Contributing to Python Tools for Visual Studio](https://github.com/Microsoft/PTVS/blob/master/CONTRIBUTING.md)] + +[![Build Status (Travis)](https://travis-ci.org/Microsoft/vscode-python.svg?branch=master)](https://travis-ci.org/Microsoft/vscode-python) [![Build status (AppVeyor)](https://ci.appveyor.com/api/projects/status/s0pt8d79gqw222j7?svg=true)](https://ci.appveyor.com/project/DonJayamanne/vscode-python-v3vd6) [![codecov](https://codecov.io/gh/Microsoft/vscode-python/branch/master/graph/badge.svg)](https://codecov.io/gh/Microsoft/vscode-python) + + +## Contributing a pull request + +### Prerequisites + +1. .NET Core 2.1 SDK + - [Windows](https://www.microsoft.com/net/learn/get-started/windows) + - [Mac OS](https://www.microsoft.com/net/learn/get-started/macos) + - [Linux](https://www.microsoft.com/net/learn/get-started/linux/rhel) +2. C# Extension to VS Code (all platforms) +3. Python 2.7 +4. Python 3.6 + +*Alternative:* [Visual Studio 2017](https://www.visualstudio.com/downloads/) (Windows only) with .NET Core and C# Workloads. Community Edition is free and is fully functional. + +### Setup + +```shell +git clone https://github.com/microsoft/ptvs +cd ptvs/Python/Product/VSCode/AnalysisVsc +dotnet build +``` + +Visual Studio 2017: +1. Open solution in Python/Product/VsCode +2. Build AnalysisVsc project +3. Binaries arrive in *Python/BuildOutput/VsCode/raw* +4. Delete contents of the *languageServer* folder in the Python Extension folder +5. Copy *.dll, *.pdb, *.json fron *Python/BuildOutput/VsCode/raw* to *languageServer* +6. In VS Code set setting *python.downloadLanguageServer* to *false* +7. In VS Code set setting *python.jediEnabled* to *false* + +### Debugging code in Python Extension to VS Code +Folow regular TypeScript debugging steps + +### Debugging C# code in Python Analysis Engine +1. Launch another instance of VS Code +2. Open Python/Product/VsCode/AnalysisVsc folder +3. Python Analysis Engine code is in *Python/Product/VsCode/Analysis* +4. Run extension from VS Code +5. In the instance with C# code select Dotnet Attach launch task. +6. Attach to *dotnet* process running *Microsoft.Python.languageServer.dll* + +On Windows you can also attach from Visual Studio 2017. + +### Validate your changes + +1. Build C# code +2. Copy binaries to *analysis* folder +3. Use the `Launch Extension` launch option. + +### Unit Tests +1. Run the Unit Tests via the `Launch Analysis Engine Tests`. +2. On Windows you can also open complete PTVS solution in Visual Studio and run its tests (or at least the Analysis section). + + +### Coding Standards +See [![Contributing to Python Tools for Visual Studio](https://github.com/Microsoft/PTVS/blob/master/CONTRIBUTING.md)] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef238b941add..14bee772eea4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,14 +1,23 @@ # Contributing to the Python extension for Visual Studio Code -[![Build Status (Travis)](https://travis-ci.org/Microsoft/vscode-python.svg?branch=master)](https://travis-ci.org/Microsoft/vscode-python) [![Build status (AppVeyor)](https://ci.appveyor.com/api/projects/status/s0pt8d79gqw222j7?svg=true)](https://ci.appveyor.com/project/DonJayamanne/vscode-python-v3vd6) [![codecov](https://codecov.io/gh/Microsoft/vscode-python/branch/master/graph/badge.svg)](https://codecov.io/gh/Microsoft/vscode-python) +--- + +| macOS/Windows CI | Linux CI | Rolling CI (macOS/Windows) | Code Coverage | +|-|-|-|-| +|[![Build status](https://vscode-python.visualstudio.com/VSCode-Python/_apis/build/status/VSCode-Python-CI)](https://vscode-python.visualstudio.com/VSCode-Python/_build/latest?definitionId=4) | [![Build Status (Travis)](https://travis-ci.org/Microsoft/vscode-python.svg?branch=master)](https://travis-ci.org/Microsoft/vscode-python) | [![Build status](https://vscode-python.visualstudio.com/VSCode-Python/_apis/build/status/VSCode-Python-Rolling-CI)](https://vscode-python.visualstudio.com/VSCode-Python/_build/latest?definitionId=9) | [![codecov](https://codecov.io/gh/Microsoft/vscode-python/branch/master/graph/badge.svg)](https://codecov.io/gh/Microsoft/vscode-python)| +--- + +# Contributing to Microsoft Python Analysis Engine +[Contributing to Python Analysis Engine](https://github.com/Microsoft/vscode-python/blob/master/CONTRIBUTING%20-%20PYTHON_ANALYSIS.md) + ## Contributing a pull request ### Prerequisites -1. Node.js (>= 8.9.1, < 9.0.0), and [Yarn](https://yarnpkg.com/lang/en/docs/install/) +1. Node.js (>= 8.9.1, < 9.0.0) 2. Python 2.7 or later (required only for testing the extension and running unit tests) 3. Windows, macOS, or Linux 4. Visual Studio Code @@ -19,16 +28,16 @@ ### Setup ```shell -git clone https://github.com/microsoft/vscode-python +git clone --recurse-submodules https://github.com/microsoft/vscode-python cd vscode-python -yarn install +npm install ``` You may see warnings that ```The engine "vscode" appears to be invalid.```, you can ignore these. ### Incremental Build -Run the `Compile` build Tasks from the [Command Palette](https://code.visualstudio.com/docs/editor/tasks) (short cut `CTRL+SHIFT+B` or `⇧⌘B`) +Run the `Compile` and `Hygiene` build Tasks from the [Command Palette](https://code.visualstudio.com/docs/editor/tasks) (short cut `CTRL+SHIFT+B` or `⇧⌘B`) ### Errors and Warnings @@ -88,73 +97,60 @@ person to do (long term this project aims to automate as much of the development process as possible). The current issues being worked on for a release are tracked in a [milestone](https://github.com/Microsoft/vscode-python/milestones) -(which is actively updated as plans change). +(which is actively updated as plans change). All +[P0](https://github.com/Microsoft/vscode-python/labels/P0) are expected to +be fixed in a milestone, else the release will be blocked. +[P1](https://github.com/Microsoft/vscode-python/labels/P1) issues are a +top-priority in a milestone, but if they are not completed they will not +block a milestone. All other issues are considered best-effort for that +milestone. The overall schedule for a release is to feature freeze for on the last Monday of the month to coincide with Visual Studio Code's code freeze. We then aim to release later that week so the latest version of the -extension is already live by the time Visual Studio Code launches -their new release. This is so we are ready to use any new features +extension is already live by the time Visual Studio Code does their +release the following week. This is so we are ready to use any new features of Visual Studio Code the day they go live. We do bugfix-only releases -between scheduled releases as necessary. +between scheduled releases as necessary, but otherwise we aim to do one +release a month. All development is actively done in the `master` branch of the -repository. It is what allows us to have an -[insiders build](#insiders-build) which is expected to be stable at +repository. It is what allows us to have a +[development build](#development-build) which is expected to be stable at all times. We do keep the most recent release as a branch in case the need for a bugfix release arises. But once a new release is made we delete the older release branch (all releases are appropriately -tagged, so history is lost). +tagged, so no history is lost). ### Issue triaging -To help actively track what stage issues are at, various labels are -used. Which labels are expected to be set vary from when an issue is -open to when an issue is closed. - -#### Open issues - -When an -[issue is first opened](https://github.com/Microsoft/vscode-python/issues), -it is triaged to contain at least three types of labels: +To help actively track what stage +[issues](https://github.com/Microsoft/vscode-python/issues) +are at, various labels are used. The following label types are expected to +be set on all open issues (otherwise the issue is not considered triaged): 1. `needs` 1. `feature` 1. `type` -These labels cover what is blocking the issue from closing, what -feature(s) of the extension are related to the issue, and what type of -issue it is, respectively. +These labels cover what is blocking the issue from closing, what is affected by +the issue, and what kind of issue it is. #### Closed issues -When an -[issue is closed](https://github.com/Microsoft/vscode-python/issues?q=is%3Aissue+is%3Aclosed), -it should have an appropriate `closed-` label. +When an issue is closed by a pull request we add a +[`validate fix`](https://github.com/Microsoft/vscode-python/issues?q=label%3A%22validate+fix%22+is%3Aclosed) +label in order to request people help us test the fix to validate the issue was +resolved successfully. Once the fix has been manually validated we remove the label. ### Pull request workflow -1. Check that there is an issue corresponding to what the pull request - is attempting to address - * If an issue exists, make sure it has reached the stage of - `awaiting 2-PR` - * If no issue exists, open one and wait for it to reach the - `awaiting 2-PR` stage before submitting the pull request -1. Create the pull request, mentioning the appropriate issue(s) in the - pull request message body - * The pull request is expected to have appropriate unit tests - * The pull request must pass its CI run before merging will be - considered - * Code coverage is expected to (at minimum) not worsen - * A [news entry file](https://github.com/Microsoft/vscode-python/tree/master/news) (as appropriate) -1. Make sure all status checks are green (e.g. CLA check, CI, etc.) -1. Address any review comments -1. [Maintainers only] Merge the pull request -1. [Maintainers only] Update affected issues to be: - 1. Closed (with an appropriate `closed-` label) - 1. The issue(s) are attached to the current milestone - 1. Register OSS usage - 1. Email CELA about any 3rd-party usage changes +Key details that all pull requests are expected to handle should be +in the PR template. The only key detail not covered in that template is +that any change in our dependencies must be properly reflected in our +third-party notices file and registered with the OSPO internally at +Microsoft (obviously external developers do not need to concern themselves +with these legal/technical issues). ### Versioning @@ -176,8 +172,7 @@ build of the extension onto a cloud storage provider. If you are interested in helping us test our development builds or would like to stay ahead of the curve, then please feel free to download and install the extension from the following -[location](https://pvsc.blob.core.windows.net/extension-builds/ms-python-insiders.vsix) -(if the CI build is passing: [![Build Status](https://travis-ci.org/Microsoft/vscode-python.svg?branch=master)](https://travis-ci.org/Microsoft/vscode-python)). +[location](https://pvsc.blob.core.windows.net/extension-builds/ms-python-insiders.vsix). Once you have downloaded the [ms-python-insiders.vsix](https://pvsc.blob.core.windows.net/extension-builds/ms-python-insiders.vsix) file, please follow the instructions on diff --git a/README.md b/README.md index 46915398f950..ae7464ae31c1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Python extension for Visual Studio Code -A [Visual Studio Code](https://code.visualstudio.com/) [extension](https://marketplace.visualstudio.com/VSCode) with rich support for the [Python language](https://www.python.org/) (_including Python 3.6_), including features such as linting, debugging, IntelliSense, code navigation, code formatting, refactoring, unit tests, snippets, and more! +A [Visual Studio Code](https://code.visualstudio.com/) [extension](https://marketplace.visualstudio.com/VSCode) with rich support for the [Python language](https://www.python.org/) (2.7, >=3.4), including features such as linting, debugging, IntelliSense, code navigation, code formatting, refactoring, unit tests, snippets, and more! ## Quick start @@ -37,11 +37,15 @@ To see all available Python commands, open the Command Palette and type ```Pytho The extension is available in multiple languages thanks to external contributors (if you would like to contribute a translation, see the -[pull request which added simplified Chinese](https://github.com/Microsoft/vscode-python/pull/240)): +[pull request which added Italian](https://github.com/Microsoft/vscode-python/pull/1152)): * `en` +* `es` +* `fr` +* `it` * `ja` * `ko-kr` +* `pt-br` * `ru` * `zh-cn` * `zh-tw` @@ -54,6 +58,7 @@ contributors (if you would like to contribute a translation, see the * Any and all feedback is appreciated and welcome! - If someone has already [file an issue](https://github.com/Microsoft/vscode-python) that encompasses your feedback, please leave a 👍/👎 reaction on the issue - Otherwise please file a new issue +* If you're interested in the development of the extension, you can read about our [development process](https://github.com/Microsoft/vscode-python/blob/master/CONTRIBUTING.md#development-process) ## Feature details @@ -67,9 +72,9 @@ contributors (if you would like to contribute a translation, see the + Ability to include custom module paths (e.g. include paths for libraries like Google App Engine, etc.; use the setting `python.autoComplete.extraPaths = []`) * Code formatting + Auto formatting of code upon saving changes (default to 'Off') - + Use either [yapf](https://pypi.io/project/yapf/) or [autopep8](https://pypi.io/project/autopep8/) for code formatting (defaults to autopep8) + + Use either [yapf](https://pypi.org/project/yapf/), [autopep8](https://pypi.org/project/autopep8/), or [Black](https://pypi.org/project/black/) for code formatting (defaults to autopep8) * Linting - + Support for multiple linters with custom settings (default is [Pylint](https://pypi.io/project/pylint/), but [Prospector](https://pypi.io/project/prospector/), [pycodestyle](https://pypi.io/project/pycodestyle/), [Flake8](https://pypi.io/project/flake8/), [pylama](https://github.com/klen/pylama), [pydocstyle](https://pypi.io/project/pydocstyle/), and [mypy](http://mypy-lang.org/) are also supported) + + Support for multiple linters with custom settings (default is [Pylint](https://pypi.org/project/pylint/), but [Prospector](https://pypi.org/project/prospector/), [Flake8](https://pypi.org/project/flake8/), [pylama](https://pypi.org/project/pylama/), [pydocstyle](https://pypi.org/project/pydocstyle/), and [mypy](https://pypi.org/project/mypy/) are also supported) * Debugging + Watch window + Evaluate expressions @@ -85,7 +90,7 @@ contributors (if you would like to contribute a translation, see the + Debugging in the integrated or external terminal window + Debugging as sudo * Unit testing - + Support for [unittest](https://docs.python.org/3/library/unittest.html#module-unittest), [pytest](https://pypi.io/project/pytest/), and [nose](https://pypi.io/project/nose/) + + Support for [unittest](https://docs.python.org/3/library/unittest.html#module-unittest), [pytest](https://pypi.org/project/pytest/), and [nose](https://pypi.org/project/nose/) + Ability to run all failed tests, individual tests + Debugging unit tests * Snippets diff --git a/ThirdPartyNotices-Distribution.txt b/ThirdPartyNotices-Distribution.txt index 3b8e433c8a9b..762ca4aa3577 100644 --- a/ThirdPartyNotices-Distribution.txt +++ b/ThirdPartyNotices-Distribution.txt @@ -1,53 +1,67 @@ - + THIRD-PARTY SOFTWARE NOTICES AND INFORMATION Do Not Translate or Localize Microsoft Python extension for Visual Studio Code incorporates components from the projects listed below. Microsoft licenses these components to you under Microsoft's licensing terms for the Microsoft Python extension for Visual Studio Code. The original copyright notices and the licenses under which Microsoft received such components are set forth below for informational purposes. Microsoft reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise. - 1. Arch (https://github.com/feross/arch) 2. diff-match-patch (https://github.com/ForbesLindesay-Unmaintained/diff-match-patch) -3. Files from the Python Project (https://www.python.org/) -4. fuzzy (https://github.com/mattyork/fuzzy) -5. Get-port (https://github.com/sindresorhus/get-port) -6. Go for Visual Studio Code (https://github.com/Microsoft/vscode-go) -7. Google Diff Match and Patch (https://github.com/GerHobbelt/google-diff-match-patch) -8. Iconv-lite (https://github.com/ashtuchkin/iconv-lite) -9. Inversify (https://github.com/inversify/InversifyJS) -10. isort (https://github.com/timothycrosley/isort) -11. jedi (https://github.com/davidhalter/jedi) -12. line-by-line (https://github.com/Osterjour/line-by-line) -13. lodash (https://github.com/lodash/lodash) +3. dotenv (https://github.com/motdotla/dotenv) +4. Files from the Python Project (https://www.python.org/) +5. fuzzy (https://github.com/mattyork/fuzzy) +6. Get-port (https://github.com/sindresorhus/get-port) +7. Glob (https://github.com/isaacs/node-glob) +8. Go for Visual Studio Code (https://github.com/Microsoft/vscode-go) +9. Google Diff Match and Patch (https://github.com/GerHobbelt/google-diff-match-patch) +10. Iconv-lite (https://github.com/ashtuchkin/iconv-lite) +11. Inversify (https://github.com/inversify/InversifyJS) +12. isort (https://github.com/timothycrosley/isort) +13. jedi (https://github.com/davidhalter/jedi) +14. line-by-line (https://github.com/Osterjour/line-by-line) +15. lodash (https://github.com/lodash/lodash) Includes:Sizzle CSS Selector Engine Includes:Webpack -14. MD5 (https://github.com/pvorb/node-md5) -15. minimatch (https://github.com/isaacs/minimatch) -16. named-js-regexp (https://github.com/edvinv/named-js-regexp) -17. node-fs-extra (https://github.com/jprichardson/node-fs-extra) -18. node-semver (https://github.com/npm/node-semver) -19. node-tmp (https://github.com/raszi/node-tmp) -20. node-tree-kill (https://github.com/pkrumins/node-tree-kill) -21. node-winreg (https://github.com/fresc81/node-winreg) -22. node-xml2js (https://github.com/Leonidas-from-XIV/node-xml2js) -23. omnisharp-vscode (https://github.com/OmniSharp/omnisharp-vscode) -24. opn (https://github.com/sindresorhus/opn) -25. pidusage (https://github.com/soyuka/pidusage) -26. Python documentation (https://docs.python.org/) -27. python-functools32 (https://github.com/MiCHiLU/python-functools32/blob/master/functools32/functools32.py) -28. pythonVSCode (https://github.com/DonJayamanne/pythonVSCode) -29. Reflect-metadata (https://github.com/rbuckton/reflect-metadata) -30. RxJS (https://github.com/ReactiveX/RxJS) +16. MD5 (https://github.com/pvorb/node-md5) +17. minimatch (https://github.com/isaacs/minimatch) +18. named-js-regexp (https://github.com/edvinv/named-js-regexp) +19. node-fs-extra (https://github.com/jprichardson/node-fs-extra) +20. node-semver (https://github.com/npm/node-semver) +21. node-stream-zip (https://github.com/antelle/node-stream-zip) +22. node-tmp (https://github.com/raszi/node-tmp) +23. node-tree-kill (https://github.com/pkrumins/node-tree-kill) +24. node-winreg (https://github.com/fresc81/node-winreg) +25. node-xml2js (https://github.com/Leonidas-from-XIV/node-xml2js) +26. omnisharp-vscode (https://github.com/OmniSharp/omnisharp-vscode) +27. opn (https://github.com/sindresorhus/opn) +28. pidusage (https://github.com/soyuka/pidusage) +29. PTVS (https://github.com/Microsoft/PTVS) +30. PTVSD (https://github.com/Microsoft/PTVSD) +31. PyDev.Debugger (https://github.com/fabioz/PyDev.Debugger) + Includes:Files copyright Yuli Fitterman + Includes:IPython + Includes:py2app + Includes:Python (various files) +32. Python documentation (https://docs.python.org/) +33. python-functools32 (https://github.com/MiCHiLU/python-functools32/blob/master/functools32/functools32.py) +34. pythonVSCode (https://github.com/DonJayamanne/pythonVSCode) +35. Reflect-metadata (https://github.com/rbuckton/reflect-metadata) +36. request (https://github.com/request/request) +37. request-progress (https://github.com/IndigoUnited/node-request-progress) +38. RxJS (https://github.com/ReactiveX/RxJS) Includes:Contributor Covenant v1.1.0, v1.4 Includes:File from Angular.io Includes:File from setImmediate -31. Sphinx (http://sphinx-doc.org/) -32. uint64be (https://github.com/mafintosh/uint64be) -33. untildify (https://github.com/sindresorhus/untildify) -34. vscode-debugadapter (https://github.com/Microsoft/vscode-debugadapter-node/tree/master/adapter) -35. vscode-debugprotocol (https://github.com/Microsoft/vscode-debugadapter-node/tree/master/protocol) -36. vscode-extension-telemetry (https://github.com/Microsoft/vscode-extension-telemetry) -37. vscode-languageclient (https://github.com/Microsoft/vscode-languageserver-node) -38. vscode-languageserver (https://github.com/Microsoft/vscode-languageserver-node/) +39. Sphinx (http://sphinx-doc.org/) +40. sudo-prompt (https://github.com/jorangreef/sudo-prompt) +41. uint64be (https://github.com/mafintosh/uint64be) +42. untangle (https://github.com/stchris/untangle) +43. untildify (https://github.com/sindresorhus/untildify) +44. vscode-debugadapter (https://github.com/Microsoft/vscode-debugadapter-node/tree/master/adapter) +45. vscode-debugprotocol (https://github.com/Microsoft/vscode-debugadapter-node/tree/master/protocol) +46. vscode-extension-telemetry (https://github.com/Microsoft/vscode-extension-telemetry) +47. vscode-languageclient (https://github.com/Microsoft/vscode-languageserver-node) +48. vscode-languageserver (https://github.com/Microsoft/vscode-languageserver-node/) +49. Typeshed (https://github.com/python/typeshed/tree/95eff73ab2092f2c3158198d404a921447172418) %% Arch NOTICES AND INFORMATION BEGIN HERE @@ -94,6 +108,35 @@ limitations under the License. ========================================= END OF diff-match-patch NOTICES AND INFORMATION +%% dotenv NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2015, Scott Motte +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +========================================= +END OF dotenv NOTICES AND INFORMATION + %% Files from the Python Project NOTICES AND INFORMATION BEGIN HERE ========================================= PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 @@ -333,6 +376,26 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ========================================= END OF Get-port NOTICES AND INFORMATION +%% Glob NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF Glob NOTICES AND INFORMATION + %% Go for Visual Studio Code NOTICES AND INFORMATION BEGIN HERE ========================================= @@ -745,6 +808,55 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ========================================= END OF node-semver NOTICES AND INFORMATION +%% node-stream-zip NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2015 Antelle https://github.com/antelle + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +== dependency license: adm-zip == + +Copyright (c) 2012 Another-D-Mention Software and other contributors, +http://www.another-d-mention.ro/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF node-stream-zip NOTICES AND INFORMATION + %% node-tmp NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) @@ -774,7 +886,9 @@ END OF node-tmp NOTICES AND INFORMATION %% node-tree-kill NOTICES AND INFORMATION BEGIN HERE ========================================= -Copyright (c) 2014 Peteris Krumins +MIT License + +Copyright (c) 2018 Peter Krumins Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -898,6 +1012,520 @@ SOFTWARE. ========================================= END OF pidusage NOTICES AND INFORMATION +%% PTVS NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) Microsoft Corporation. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +========================================= +END OF PTVS NOTICES AND INFORMATION + +%% PTVSD NOTICES AND INFORMATION BEGIN HERE +========================================= + ptvsd + + Copyright (c) Microsoft Corporation + All rights reserved. + + MIT License + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +========================================= +END OF PTVSD NOTICES AND INFORMATION + +%% PyDev.Debugger NOTICES AND INFORMATION BEGIN HERE +========================================= +Eclipse Public License - v 1.0 + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC +LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM +CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + +a) in the case of the initial Contributor, the initial code and documentation + distributed under this Agreement, and +b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + + where such changes and/or additions to the Program originate from and are + distributed by that particular Contributor. A Contribution 'originates' + from a Contributor if it was added to the Program by such Contributor + itself or anyone acting on such Contributor's behalf. Contributions do not + include additions to the Program which: (i) are separate modules of + software distributed in conjunction with the Program under their own + license agreement, and (ii) are not derivative works of the Program. + +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which are +necessarily infringed by the use or sale of its Contribution alone or when +combined with the Program. + +"Program" means the Contributions distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, +including all Contributors. + +2. GRANT OF RIGHTS + a) Subject to the terms of this Agreement, each Contributor hereby grants + Recipient a non-exclusive, worldwide, royalty-free copyright license to + reproduce, prepare derivative works of, publicly display, publicly + perform, distribute and sublicense the Contribution of such Contributor, + if any, and such derivative works, in source code and object code form. + b) Subject to the terms of this Agreement, each Contributor hereby grants + Recipient a non-exclusive, worldwide, royalty-free patent license under + Licensed Patents to make, use, sell, offer to sell, import and otherwise + transfer the Contribution of such Contributor, if any, in source code and + object code form. This patent license shall apply to the combination of + the Contribution and the Program if, at the time the Contribution is + added by the Contributor, such addition of the Contribution causes such + combination to be covered by the Licensed Patents. The patent license + shall not apply to any other combinations which include the Contribution. + No hardware per se is licensed hereunder. + c) Recipient understands that although each Contributor grants the licenses + to its Contributions set forth herein, no assurances are provided by any + Contributor that the Program does not infringe the patent or other + intellectual property rights of any other entity. Each Contributor + disclaims any liability to Recipient for claims brought by any other + entity based on infringement of intellectual property rights or + otherwise. As a condition to exercising the rights and licenses granted + hereunder, each Recipient hereby assumes sole responsibility to secure + any other intellectual property rights needed, if any. For example, if a + third party patent license is required to allow Recipient to distribute + the Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + d) Each Contributor represents that to its knowledge it has sufficient + copyright rights in its Contribution, if any, to grant the copyright + license set forth in this Agreement. + +3. REQUIREMENTS + +A Contributor may choose to distribute the Program in object code form under +its own license agreement, provided that: + + a) it complies with the terms and conditions of this Agreement; and + b) its license agreement: + i) effectively disclaims on behalf of all Contributors all warranties + and conditions, express and implied, including warranties or + conditions of title and non-infringement, and implied warranties or + conditions of merchantability and fitness for a particular purpose; + ii) effectively excludes on behalf of all Contributors all liability for + damages, including direct, indirect, special, incidental and + consequential damages, such as lost profits; + iii) states that any provisions which differ from this Agreement are + offered by that Contributor alone and not by any other party; and + iv) states that source code for the Program is available from such + Contributor, and informs licensees how to obtain it in a reasonable + manner on or through a medium customarily used for software exchange. + +When the Program is made available in source code form: + + a) it must be made available under this Agreement; and + b) a copy of this Agreement must be included with each copy of the Program. + Contributors may not remove or alter any copyright notices contained + within the Program. + +Each Contributor must identify itself as the originator of its Contribution, +if +any, in a manner that reasonably allows subsequent Recipients to identify the +originator of the Contribution. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities with +respect to end users, business partners and the like. While this license is +intended to facilitate the commercial use of the Program, the Contributor who +includes the Program in a commercial product offering should do so in a manner +which does not create potential liability for other Contributors. Therefore, +if a Contributor includes the Program in a commercial product offering, such +Contributor ("Commercial Contributor") hereby agrees to defend and indemnify +every other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits and +other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such Commercial +Contributor in connection with its distribution of the Program in a commercial +product offering. The obligations in this section do not apply to any claims +or Losses relating to any actual or alleged intellectual property +infringement. In order to qualify, an Indemnified Contributor must: +a) promptly notify the Commercial Contributor in writing of such claim, and +b) allow the Commercial Contributor to control, and cooperate with the +Commercial Contributor in, the defense and any related settlement +negotiations. The Indemnified Contributor may participate in any such claim at +its own expense. + +For example, a Contributor might include the Program in a commercial product +offering, Product X. That Contributor is then a Commercial Contributor. If +that Commercial Contributor then makes performance claims, or offers +warranties related to Product X, those performance claims and warranties are +such Commercial Contributor's responsibility alone. Under this section, the +Commercial Contributor would have to defend claims against the other +Contributors related to those performance claims and warranties, and if a +court requires any other Contributor to pay any damages as a result, the +Commercial Contributor must pay those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, +NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each +Recipient is solely responsible for determining the appropriateness of using +and distributing the Program and assumes all risks associated with its +exercise of rights under this Agreement , including but not limited to the +risks and costs of program errors, compliance with applicable laws, damage to +or loss of data, programs or equipment, and unavailability or interruption of +operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY +CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION +LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY +OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of the +remainder of the terms of this Agreement, and without further action by the +parties hereto, such provision shall be reformed to the minimum extent +necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Program itself +(excluding combinations of the Program with other software or hardware) +infringes such Recipient's patent(s), then such Recipient's rights granted +under Section 2(b) shall terminate as of the date such litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails to +comply with any of the material terms or conditions of this Agreement and does +not cure such failure in a reasonable period of time after becoming aware of +such noncompliance. If all Recipient's rights under this Agreement terminate, +Recipient agrees to cease use and distribution of the Program as soon as +reasonably practicable. However, Recipient's obligations under this Agreement +and any licenses granted by Recipient relating to the Program shall continue +and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in +order to avoid inconsistency the Agreement is copyrighted and may only be +modified in the following manner. The Agreement Steward reserves the right to +publish new versions (including revisions) of this Agreement from time to +time. No one other than the Agreement Steward has the right to modify this +Agreement. The Eclipse Foundation is the initial Agreement Steward. The +Eclipse Foundation may assign the responsibility to serve as the Agreement +Steward to a suitable separate entity. Each new version of the Agreement will +be given a distinguishing version number. The Program (including +Contributions) may always be distributed subject to the version of the +Agreement under which it was received. In addition, after a new version of the +Agreement is published, Contributor may elect to distribute the Program +(including its Contributions) under the new version. Except as expressly +stated in Sections 2(a) and 2(b) above, Recipient receives no rights or +licenses to the intellectual property of any Contributor under this Agreement, +whether expressly, by implication, estoppel or otherwise. All rights in the +Program not expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the +intellectual property laws of the United States of America. No party to this +Agreement will bring a legal action under this Agreement more than one year +after the cause of action arose. Each party waives its rights to a jury trial in +any resulting litigation. +========================================= +Includes files copyright Yuli Fitterman + +Copyright (c) Yuli Fitterman + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +========================================= +Includes Ipython + +Copyright (c) 2008-2010, IPython Development Team +Copyright (c) 2001-2007, Fernando Perez. +Copyright (c) 2001, Janko Hauser +Copyright (c) 2001, Nathaniel Gray + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the IPython Development Team nor the names of its +contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +Includes py2app + +This is the MIT license. This software may also be distributed under the same terms as Python (the PSF license). + +Copyright (c) 2004 Bob Ippolito. + +Some parts copyright (c) 2010-2014 Ronald Oussoren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +Includes lib2to3 and other Python files + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012 Python Software Foundation; All Rights Reserved" are retained in Python +alone or in any derivative version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF PyDev.Debugger NOTICES AND INFORMATION + %% Python documentation NOTICES AND INFORMATION BEGIN HERE ========================================= Terms and conditions for accessing or otherwise using Python @@ -1310,6 +1938,90 @@ and limitations under the License. ========================================= END OF Reflect-metadata NOTICES AND INFORMATION +%% request NOTICES AND INFORMATION BEGIN HERE +========================================= +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS +========================================= +END OF request NOTICES AND INFORMATION + +%% request-progress NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2012 IndigoUnited + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF request-progress NOTICES AND INFORMATION + %% RxJS NOTICES AND INFORMATION BEGIN HERE ========================================= Copyright (c) 2015-2017 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors @@ -1415,6 +2127,34 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ========================================= END OF Sphinx NOTICES AND INFORMATION +%% sudo-prompt NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2015 Joran Dirk Greef + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +========================================= +END OF sudo-prompt NOTICES AND INFORMATION + %% uint64be NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) @@ -1442,6 +2182,41 @@ THE SOFTWARE. ========================================= END OF uint64be NOTICES AND INFORMATION +%% untangle NOTICES AND INFORMATION BEGIN HERE +========================================= +# Author: Christian Stefanescu + +# Contributions from: + +Florian Idelberger +Apalala + +// Copyright (c) 2011 + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +========================================= +END OF untangle NOTICES AND INFORMATION + %% untildify NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) @@ -1573,6 +2348,246 @@ THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ========================================= END OF vscode-languageserver NOTICES AND INFORMATION +%% Typeshed NOTICES AND INFORMATION BEGIN HERE +========================================= + +The "typeshed" project is licensed under the terms of the Apache license, as +reproduced below. + += = = = = + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + += = = = = + +Parts of typeshed are licensed under different licenses (like the MIT +license), reproduced below. + += = = = = + +The MIT License +Copyright (c) 2015 Jukka Lehtosalo and contributors +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + += = = = = +========================================= +END OF Typeshed NOTICES AND INFORMATION diff --git a/ThirdPartyNotices-Repository.txt b/ThirdPartyNotices-Repository.txt index 8d0449fa5d4f..29bbf7b86c56 100644 --- a/ThirdPartyNotices-Repository.txt +++ b/ThirdPartyNotices-Repository.txt @@ -2,7 +2,7 @@ THIRD-PARTY SOFTWARE NOTICES AND INFORMATION Do Not Translate or Localize -Microsoft Python extension for Visual Studio Code incorporates third party material from the projects listed below. The original copyright notice and the license under which Microsoft received such third party material are set forth below. Microsoft reserves all other rights not expressly granted, whether by implication, estoppel or otherwise. +Microsoft Python extension for Visual Studio Code incorporates third party material from the projects listed below. The original copyright notice and the license under which Microsoft received such third party material are set forth below. Microsoft reserves all other rights not expressly granted, whether by implication, estoppel or otherwise. 1. Go for Visual Studio Code (https://github.com/Microsoft/vscode-go) @@ -12,10 +12,11 @@ Go for Visual Studio Code (https://github.com/Microsoft/vscode-go) 5. jedi (https://github.com/davidhalter/jedi) 6. omnisharp-vscode (https://github.com/OmniSharp/omnisharp-vscode) 7. parso (https://github.com/davidhalter/parso) -8. Python documentation (https://docs.python.org/) -9. python-functools32 (https://github.com/MiCHiLU/python-functools32/blob/master/functools32/functools32.py) -10. pythonVSCode (https://github.com/DonJayamanne/pythonVSCode) -11. Sphinx (http://sphinx-doc.org/) +8. PTVS (https://github.com/Microsoft/PTVS) +9. Python documentation (https://docs.python.org/) +10. python-functools32 (https://github.com/MiCHiLU/python-functools32/blob/master/functools32/functools32.py) +11. pythonVSCode (https://github.com/DonJayamanne/pythonVSCode) +12. Sphinx (http://sphinx-doc.org/) %% @@ -427,6 +428,24 @@ Agreement. ========================================= END OF parso NOTICES, INFORMATION, AND LICENSE +%% PTVS NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +Copyright (c) Microsoft Corporation. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +========================================= +END OF PTVS NOTICES, INFORMATION, AND LICENSE + %% Python documentation NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= Terms and conditions for accessing or otherwise using Python diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index b4eca06c93b4..000000000000 --- a/appveyor.yml +++ /dev/null @@ -1,30 +0,0 @@ -environment: - matrix: - - PYTHON: "C:\\Python36" - PYTHON_VERSION: "3.6.3" - PYTHON_ARCH: "32" - nodejs_version: "8.9.1" - APPVEYOR: "true" - -init: - - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" - -install: - - ps: Install-Product node $env:nodejs_version - - npm i -g yarn - - yarn - - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - - pip install -U pip - - pip install -t ./pythonFiles/experimental/ptvsd git+https://github.com/Microsoft/ptvsd/ - - python --version - - python -m easy_install -U setuptools - - "%PYTHON%/Scripts/pip.exe install --upgrade -r requirements.txt" - -test_script: - - yarn run clean - - yarn run vscode:prepublish - - yarn run testDebugger --silent - - yarn run testSingleWorkspace --silent - - yarn run testMultiWorkspace --silent - -build: off diff --git a/coverconfig.json b/coverconfig.json index a84634e92ecc..201980b0fd63 100644 --- a/coverconfig.json +++ b/coverconfig.json @@ -11,7 +11,8 @@ "json", "html", "lcov", - "lcovonly" + "lcovonly", + "cobertura" ], "verbose": false } diff --git a/gulpfile.js b/gulpfile.js index 05918ab7bc6d..708c3cb132ea 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -3,6 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/* jshint node: true */ +/* jshint esversion: 6 */ + 'use strict'; const gulp = require('gulp'); @@ -13,6 +16,7 @@ const tslint = require('tslint'); const relative = require('relative'); const ts = require('gulp-typescript'); const cp = require('child_process'); +const spawn = require('cross-spawn'); const colors = require('colors/safe'); const gitmodified = require('gulp-gitmodified'); const path = require('path'); @@ -21,6 +25,14 @@ const jeditor = require("gulp-json-editor"); const del = require('del'); const sourcemaps = require('gulp-sourcemaps'); const fs = require('fs'); +const remapIstanbul = require('remap-istanbul'); +const istanbul = require('istanbul'); +const glob = require('glob'); +const os = require('os'); +const _ = require('lodash'); +const nativeDependencyChecker = require('node-has-native-dependencies'); +const flat = require('flat'); +const inlinesource = require('gulp-inline-source'); /** * Hygiene works by creating cascading subsets of all our files and @@ -36,6 +48,10 @@ const all = [ 'src/client/**/*', ]; +const tsFilter = [ + 'src/**/*.ts', +]; + const indentationFilter = [ 'src/**/*.ts', '!**/typings/**/*', @@ -52,7 +68,7 @@ const tslintFilter = [ '!resources/**/*', '!snippets/**/*', '!syntaxes/**/*', - '!**/typings/**/*', + '!**/typings/**/*' ]; const copyrightHeader = [ @@ -60,7 +76,8 @@ const copyrightHeader = [ '// Licensed under the MIT License.', '', '\'use strict\';' -].join('\n'); +]; +const copyrightHeaders = [copyrightHeader.join('\n'), copyrightHeader.join('\r\n')]; gulp.task('hygiene', () => run({ mode: 'all', skipFormatCheck: true, skipIndentationCheck: true })); @@ -68,7 +85,12 @@ gulp.task('compile', () => run({ mode: 'compile', skipFormatCheck: true, skipInd gulp.task('watch', ['hygiene-modified', 'hygiene-watch']); -gulp.task('hygiene-watch', () => gulp.watch(all, debounce(() => run({ mode: 'changes' }), 1000))); +// Duplicate to allow duplicate task in tasks.json (one ith problem matching, and one without) +gulp.task('watchProblems', ['hygiene-modified', 'hygiene-watch']); + +gulp.task('debugger-coverage', () => buildDebugAdapterCoverage()); + +gulp.task('hygiene-watch', () => gulp.watch(tsFilter, debounce(() => run({ mode: 'changes', skipFormatCheck: true, skipIndentationCheck: true, skipCopyrightCheck: true }), 100))); gulp.task('hygiene-all', () => run({ mode: 'all' })); @@ -76,9 +98,17 @@ gulp.task('hygiene-modified', ['compile'], () => run({ mode: 'changes' })); gulp.task('clean', ['output:clean', 'cover:clean'], () => { }); -gulp.task('output:clean', () => del('coverage')); +gulp.task('output:clean', () => del(['coverage', 'debug_coverage*'])); + +gulp.task('cover:clean', () => del(['coverage', 'debug_coverage*'])); -gulp.task('cover:clean', () => del('coverage')); +gulp.task('clean:ptvsd', () => del(['coverage', 'pythonFiles/experimental/ptvsd*'])); + +gulp.task('checkNativeDependencies', () => { + if (hasNativeDependencies()) { + throw new Error('Native dependencies deteced'); + } +}); gulp.task('cover:enable', () => { return gulp.src("./coverconfig.json") @@ -92,36 +122,120 @@ gulp.task('cover:enable', () => { gulp.task('cover:disable', () => { return gulp.src("./coverconfig.json") .pipe(jeditor((json) => { - json.enabled = true; + json.enabled = false; return json; })) .pipe(gulp.dest("./out", { 'overwrite': true })); }); +/** + * Inline CSS into the coverage report for better visualizations on + * the VSTS report page for code coverage. + */ +gulp.task('inlinesource', () => { + return gulp.src('./coverage/lcov-report/*.html') + .pipe(inlinesource({attribute: false})) + .pipe(gulp.dest('./coverage/lcov-report-inline')); +}); + +function hasNativeDependencies() { + let nativeDependencies = nativeDependencyChecker.check(path.join(__dirname, 'node_modules')); + if (!Array.isArray(nativeDependencies) || nativeDependencies.length === 0) { + return false; + } + const dependencies = JSON.parse(spawn.sync('npm', ['ls', '--json', '--prod']).stdout.toString()); + const jsonProperties = Object.keys(flat.flatten(dependencies)); + nativeDependencies = _.flatMap(nativeDependencies, item => path.dirname(item.substring(item.indexOf('node_modules') + 'node_modules'.length)).split(path.sep)) + .filter(item => item.length > 0) + .filter(item => jsonProperties.findIndex(flattenedDependency => flattenedDependency.endsWith(`dependencies.${item}.version`)) >= 0); + if (nativeDependencies.length > 0) { + console.error('Native dependencies detected', nativeDependencies); + return true; + } + return false; +} + +function buildDebugAdapterCoverage() { + const matches = glob.sync(path.join(__dirname, 'debug_coverage*/coverage.json')); + matches.forEach(coverageFile => { + const finalCoverageFile = path.join(path.dirname(coverageFile), 'coverage-final-upload.json'); + const remappedCollector = remapIstanbul.remap(JSON.parse(fs.readFileSync(coverageFile, 'utf8')), { + warn: warning => { + // We expect some warnings as any JS file without a typescript mapping will cause this. + // By default, we'll skip printing these to the console as it clutters it up. + console.warn(warning); + } + }); + + const reporter = new istanbul.Reporter(undefined, path.dirname(coverageFile)); + reporter.add('lcov'); + reporter.write(remappedCollector, true, () => { }); + }); +} + /** * @typedef {Object} hygieneOptions - creates a new type named 'SpecialType' * @property {'changes'|'staged'|'all'|'compile'} [mode=] - Mode. * @property {boolean=} skipIndentationCheck - Skip indentation checks. * @property {boolean=} skipFormatCheck - Skip format checks. +* @property {boolean=} skipCopyrightCheck - Skip copyright checks. * @property {boolean=} skipLinter - Skip linter. */ +const tsProjectMap = {}; +/** + * + * @param {hygieneOptions} options + */ +function getTsProject(options) { + const tsOptions = options.mode === 'compile' ? undefined : { strict: true, noImplicitAny: false, noImplicitThis: false }; + const mode = tsOptions && tsOptions.mode ? tsOptions.mode : ''; + return tsProjectMap[mode] ? tsProjectMap[mode] : tsProjectMap[mode] = ts.createProject('tsconfig.json', tsOptions); +} + +let configuration; +/** + * + * @param {hygieneOptions} options + */ +function getLinter(options) { + configuration = configuration ? configuration : tslint.Configuration.findConfiguration(null, '.'); + const program = tslint.Linter.createProgram('./tsconfig.json'); + const linter = new tslint.Linter({ formatter: 'json' }, program); + return { linter, configuration }; +} +let compilationInProgress = false; +let reRunCompilation = false; /** * * @param {hygieneOptions} options * @returns {NodeJS.ReadWriteStream} */ const hygiene = (options) => { + if (compilationInProgress) { + reRunCompilation = true; + return; + } + const fileListToProcess = options.mode === 'compile' ? undefined : getFileListToProcess(options); + if (Array.isArray(fileListToProcess) && fileListToProcess !== all + && fileListToProcess.filter(item => item.endsWith('.ts')).length === 0) { + return; + } + + const started = new Date().getTime(); + compilationInProgress = true; options = options || {}; let errorCount = 0; - const addedFiles = getAddedFilesSync(); + const addedFiles = options.skipCopyrightCheck ? [] : getAddedFilesSync(); console.log(colors.blue('Hygiene started.')); - const copyrights = es.through(function (file) { - if (addedFiles.indexOf(file.path) !== -1 && file.contents.toString('utf8').indexOf(copyrightHeader) !== 0) { - // Use tslint format. - console.error(`ERROR: (copyright) ${file.relative}[1,1]: Missing or bad copyright statement`); - errorCount++; + if (addedFiles.indexOf(file.path) !== -1) { + const contents = file.contents.toString('utf8'); + if (!copyrightHeaders.some(header => contents.indexOf(header) === 0)) { + // Use tslint format. + console.error(`ERROR: (copyright) ${file.relative}[1,1]: Missing or bad copyright statement`); + errorCount++; + } } this.emit('data', file); @@ -200,15 +314,16 @@ const hygiene = (options) => { .filter(reported => reported === true) .length > 0; } - const configuration = tslint.Configuration.findConfiguration(null, '.'); - const program = tslint.Linter.createProgram('./tsconfig.json'); - const linter = new tslint.Linter({ formatter: 'json' }, program); + + const { linter, configuration } = getLinter(options); const tsl = es.through(function (file) { const contents = file.contents.toString('utf8'); // Don't print anything to the console, we'll do that. // Yes this is a hack, but tslinter doesn't provide an option to prevent this. const oldWarn = console.warn; console.warn = () => { }; + linter.failures = []; + linter.fixes = []; linter.lint(file.relative, contents, configuration.results); console.warn = oldWarn; const result = linter.getResult(); @@ -231,8 +346,7 @@ const hygiene = (options) => { this.emit('data', file); }); - const tsOptions = options.mode === 'compile' ? undefined : { strict: true, noImplicitAny: false, noImplicitThis: false }; - const tsProject = ts.createProject('tsconfig.json', tsOptions); + const tsProject = getTsProject(options); const tsc = function () { function customReporter() { @@ -255,7 +369,7 @@ const hygiene = (options) => { return tsProject(reporter); } - const files = options.mode === 'compile' ? tsProject.src() : getFilesToProcess(options); + const files = options.mode === 'compile' ? tsProject.src() : getFilesToProcess(fileListToProcess); const dest = options.mode === 'compile' ? './out' : '.'; let result = files .pipe(filter(f => f && f.stat && !f.stat.isDirectory())); @@ -266,8 +380,11 @@ const hygiene = (options) => { } result = result - .pipe(filter(tslintFilter)) - .pipe(copyrights); + .pipe(filter(tslintFilter)); + + if (!options.skipCopyrightCheck) { + result = result.pipe(copyrights); + } if (!options.skipFormatCheck) { // result = result @@ -278,7 +395,7 @@ const hygiene = (options) => { result = result .pipe(tsl); } - + let totalTime = 0; result = result .pipe(tscFilesTracker) .pipe(sourcemaps.init()) @@ -295,15 +412,22 @@ const hygiene = (options) => { .pipe(gulp.dest(dest)) .pipe(es.through(null, function () { if (errorCount > 0) { - const errorMessage = `Hygiene failed with errors 👎 . Check 'gulpfile.js'.`; + const errorMessage = `Hygiene failed with errors 👎 . Check 'gulpfile.js' (completed in ${new Date().getTime() - started}ms).`; console.error(colors.red(errorMessage)); exitHandler(options); } else { - console.log(colors.green('Hygiene passed with 0 errors 👍.')); + console.log(colors.green(`Hygiene passed with 0 errors 👍 (completed in ${new Date().getTime() - started}ms).`)); } // Reset error counter. errorCount = 0; reportedLinterFailures = []; + compilationInProgress = false; + if (reRunCompilation) { + reRunCompilation = false; + setTimeout(() => { + hygiene(options); + }, 10); + } this.emit('end'); })) .on('error', exitHandler.bind(this, options)); @@ -318,6 +442,7 @@ const hygiene = (options) => { * @property {string[]=} files - Optional list of files to be modified. * @property {boolean=} skipIndentationCheck - Skip indentation checks. * @property {boolean=} skipFormatCheck - Skip format checks. +* @property {boolean=} skipCopyrightCheck - Skip copyright checks. * @property {boolean=} skipLinter - Skip linter. * @property {boolean=} watch - Watch mode. */ @@ -363,30 +488,44 @@ function getAddedFilesSync() { return out .split(/\r?\n/) .filter(l => !!l) - .filter(l => l.startsWith('A') || l.startsWith('??')) + .filter(l => _.intersection(['A', '?', 'U'], l.substring(0, 2).trim().split('')).length > 0) + .map(l => path.join(__dirname, l.substring(2).trim())); +} +function getModifiedFilesSync() { + const out = cp.execSync('git status -u -s', { encoding: 'utf8' }); + return out + .split(/\r?\n/) + .filter(l => !!l) + .filter(l => _.intersection(['M', 'A', 'R', 'C', 'U', '?'], l.substring(0, 2).trim().split('')).length > 0) .map(l => path.join(__dirname, l.substring(2).trim())); } /** * @param {hygieneOptions} options */ -function getFilesToProcess(options) { +function getFilesToProcess(fileList) { + const gulpSrcOptions = { base: '.' }; + return gulp.src(fileList, gulpSrcOptions); +} + +/** +* @param {hygieneOptions} options +*/ +function getFileListToProcess(options) { const mode = options ? options.mode : 'all'; const gulpSrcOptions = { base: '.' }; // If we need only modified files, then filter the glob. if (options && options.mode === 'changes') { - return gulp.src(all, gulpSrcOptions) - .pipe(gitmodified(['M', 'A', 'D', 'R', 'C', 'U', '??'])); + return getModifiedFilesSync(); } if (options && options.mode === 'staged') { - return gulp.src(getStagedFilesSync(), gulpSrcOptions); + return getStagedFilesSync(); } - return gulp.src(all, gulpSrcOptions); + return all; } - exports.hygiene = hygiene; // this allows us to run hygiene as a git pre-commit hook. diff --git a/news/2 Fixes/404.md b/news/2 Fixes/404.md deleted file mode 100644 index f3c6b68e7ccc..000000000000 --- a/news/2 Fixes/404.md +++ /dev/null @@ -1 +0,0 @@ -Determine pipenv usage based on finding a file named `Pipfile`, not `pipfile` (case-sensitive) diff --git a/news/2 Fixes/87.md b/news/2 Fixes/87.md deleted file mode 100644 index c024086fd1d3..000000000000 --- a/news/2 Fixes/87.md +++ /dev/null @@ -1 +0,0 @@ -Fix occasionally having unverified breakpoints diff --git a/news/2 Fixes/896.md b/news/2 Fixes/896.md deleted file mode 100644 index 2f9e652cca4b..000000000000 --- a/news/2 Fixes/896.md +++ /dev/null @@ -1 +0,0 @@ -Update npm package `vscode-extension-telemetry` to fix the warning 'os.tmpDir() deprecation'. diff --git a/news/3 Code Health/742.md b/news/3 Code Health/742.md deleted file mode 100644 index d35d1ffd391e..000000000000 --- a/news/3 Code Health/742.md +++ /dev/null @@ -1 +0,0 @@ -Enable unit testing of the experimental debugger on CI servers diff --git a/news/3 Code Health/983.md b/news/3 Code Health/983.md deleted file mode 100644 index bd0250359c96..000000000000 --- a/news/3 Code Health/983.md +++ /dev/null @@ -1 +0,0 @@ -Launch the unit tests in debug mode as opposed to running and attaching the debugger. diff --git a/news/README.md b/news/README.md index 8b8350f5ddec..d4e7d7de26a1 100644 --- a/news/README.md +++ b/news/README.md @@ -3,16 +3,18 @@ Our changelog is automatically generate from individual news entries. This alleviates the burden of having to go back and try to figure out what changed in a release. It also helps tie pull requests back to the -issue(s) it addresses. +issue(s) it addresses. Finally, it avoids merge conflicts if multiple +pull requests try to edit the changelog. ## Entries -Each news entries is represented by a Markdown file that contains the +Each news entry is represented by a Markdown file that contains the relevant details of what changed. The file name of the news entry is -the issue that corresponds to the change along with an option nonce in +the issue that corresponds to the change along with an optional nonce in case a single issue corresponds to multiple changes. The directory the news entry is saved in specifies what section of the changelog the -change corresponds to. +change corresponds to. External contributors should also make sure to +thank themselves for taking the time and effort to contribute. As an example, a change corresponding to a bug reported in issue #42 would be saved in the `1 Fixes` directory and named `42.md` @@ -22,6 +24,7 @@ regarding issue #42) and could contain the following: ```markdown [Answer](https://en.wikipedia.org/wiki/42_(number)) to the Ultimate Question of Life, the Universe, and Everything! +(thanks [Don Jaymanne](https://github.com/donjayamanne/)) ``` This would then be made into an entry in the changelog that was in the diff --git a/news/__main__.py b/news/__main__.py new file mode 100644 index 000000000000..e12476c4ecca --- /dev/null +++ b/news/__main__.py @@ -0,0 +1,3 @@ +import runpy + +runpy.run_module('announce', run_name='__main__', alter_sys=True) diff --git a/news/announce.py b/news/announce.py index a17e5d086099..0666a53a2f0c 100644 --- a/news/announce.py +++ b/news/announce.py @@ -1,58 +1,70 @@ -"""Generate the changelog.""" +"""Generate the changelog. + +Usage: announce [--dry_run | --interim | --final] [] + +""" +import dataclasses import enum import operator import os import pathlib import re import subprocess -import types +import sys -import click +import docopt FILENAME_RE = re.compile(r"(?P\d+)(?P-\S+)?\.md") -ISSUE_URL = "https://github.com/Microsoft/vscode-python/issues/{issue}" -ENTRY_TEMPLATE = "1. {entry} ([#{issue}]({issue_url}))" -SECTION_DEPTH = "###" -def NewsEntry(issue_number, description, path): - """Construct a data object for a news entry.""" - # TODO: replace with a dataclass in Python 3.7. - return types.SimpleNamespace(issue_number=issue_number, - description=description, path=path) + +@dataclasses.dataclass +class NewsEntry: + """Representation of a news entry.""" + + issue_number: int + description: str + path: pathlib.Path def news_entries(directory): """Yield news entries in the directory.""" for path in directory.iterdir(): - if path.name == 'README.md': + if path.name == "README.md": continue match = FILENAME_RE.match(path.name) if match is None: - raise ValueError(f'{path} has a bad file name') - issue = int(match.group('issue')) + raise ValueError(f"{path} has a bad file name") + issue = int(match.group("issue")) entry = path.read_text("utf-8") yield NewsEntry(issue, entry, path) -def SectionTitle(index, title, path): +@dataclasses.dataclass +class SectionTitle: """Create a data object for a section of the changelog.""" - # TODO: replace with a dataclass in Python 3.7. - return types.SimpleNamespace(index=index, title=title, path=path) + + index: int + title: str + path: pathlib.Path def sections(directory): """Yield the sections in their appropriate order.""" found = [] for path in directory.iterdir(): - if not path.is_dir(): + if not path.is_dir() or path.name.startswith("."): continue - position, sep, title = path.name.partition(' ') + position, sep, title = path.name.partition(" ") if not sep: - raise ValueError('directory is missing position part') + print( + f"directory {path.name!r} is missing a ranking; skipping", + file=sys.stderr, + ) + continue found.append(SectionTitle(int(position), title, path)) - return sorted(found, key=operator.attrgetter('index')) + return sorted(found, key=operator.attrgetter("index")) def gather(directory): @@ -65,17 +77,27 @@ def gather(directory): def entry_markdown(entry): """Generate the Markdown for the specified entry.""" - issue_url = ISSUE_URL.format(issue=entry.issue_number) - return ENTRY_TEMPLATE.format(entry=entry.description, - issue=entry.issue_number, - issue_url=issue_url) + enumerated_item = "1. " + indent = " " * len(enumerated_item) + issue_url = ( + f"https://github.com/Microsoft/vscode-python/issues/{entry.issue_number}" + ) + issue_md = f"([#{entry.issue_number}]({issue_url}))" + entry_lines = entry.description.strip().splitlines() + formatted_lines = [f"{enumerated_item}{entry_lines[0]}"] + formatted_lines.extend(f"{indent}{line}" for line in entry_lines[1:]) + formatted_lines.append(f"{indent}{issue_md}") + return "\n".join(formatted_lines) + return ENTRY_TEMPLATE.format( + entry=entry.description.strip(), issue=entry.issue_number, issue_url=issue_url + ) def changelog_markdown(data): """Generate the Markdown for the release.""" changelog = [] for section, entries in data: - changelog.append(f"{SECTION_DEPTH} {section.title}") + changelog.append(f"### {section.title}") changelog.append("") changelog.extend(map(entry_markdown, entries)) changelog.append("") @@ -84,9 +106,17 @@ def changelog_markdown(data): def git_rm(path): """Run git-rm on the path.""" - status = subprocess.run(['git', 'rm', os.fspath(path.resolve())], - shell=True) - status.check_returncode() + status = subprocess.run( + ["git", "rm", os.fspath(path.resolve())], + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + try: + status.check_returncode() + except Exception: + print(status.stdout, file=sys.stderr) + raise def cleanup(data): @@ -104,23 +134,24 @@ class RunType(enum.Enum): final = 2 -@click.command() -@click.option('--dry-run', 'run_type', flag_value=RunType.dry_run, - help='validate input') -@click.option('--interim', 'run_type', flag_value=RunType.interim, default=True, - help='generate Markdown') -@click.option('--final', 'run_type', flag_value=RunType.final, - help='generate Markdown & `git rm` news files') -@click.argument('directory', default=pathlib.Path(__file__).parent, - type=click.Path(exists=True, file_okay=False)) def main(run_type, directory): + directory = pathlib.Path(directory) data = gather(directory) markdown = changelog_markdown(data) if run_type != RunType.dry_run: + # XXX This can lead to mojibake; hopefully Python 3.7 will resolve this. print(markdown) if run_type == RunType.final: cleanup(data) -if __name__ == '__main__': - main() +if __name__ == "__main__": + arguments = docopt.docopt(__doc__) + for possible_run_type in RunType: + if arguments[f"--{possible_run_type.name}"]: + run_type = possible_run_type + break + else: + run_type = RunType.interim + directory = arguments[""] or pathlib.Path(__file__).parent + main(run_type, directory) diff --git a/news/requirements.txt b/news/requirements.txt index d47e9da45e5a..316eef65b021 100644 --- a/news/requirements.txt +++ b/news/requirements.txt @@ -1,2 +1,2 @@ -click~=6.7.0 +docopt==0.6.2 pytest~=3.4.1 diff --git a/news/test_announce.py b/news/test_announce.py index 6c9aa256bdc1..3270088b06b6 100644 --- a/news/test_announce.py +++ b/news/test_announce.py @@ -1,5 +1,6 @@ import pathlib +import docopt import pytest import announce as ann @@ -48,8 +49,7 @@ def test_sections_sorting(directory): def test_sections_naming(directory): (directory / 'Hello').mkdir() - with pytest.raises(ValueError): - list(ann.sections(directory)) + assert not ann.sections(directory) def test_gather(directory): @@ -79,8 +79,6 @@ def test_gather(directory): assert entries[1].description == 'Fix 2' - - def test_entry_markdown(): markdown = ann.entry_markdown(ann.NewsEntry(42, 'Hello, world!', None)) assert '42' in markdown @@ -126,3 +124,14 @@ def fake_git_rm(path): section, entries = results.pop() assert len(entries) == 1 assert rm_path == entries[0].path + + +def test_cli(): + for option in ("--"+opt for opt in ["dry_run", "interim", "final"]): + args = docopt.docopt(ann.__doc__, [option]) + assert args[option] + args = docopt.docopt(ann.__doc__, ["./news"]) + assert args[""] == "./news" + args = docopt.docopt(ann.__doc__, ["--dry_run", "./news"]) + assert args["--dry_run"] + assert args[""] == "./news" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000000..8f255d8aabab --- /dev/null +++ b/package-lock.json @@ -0,0 +1,9819 @@ +{ + "name": "python", + "version": "2018.7.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@gulp-sourcemaps/identity-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-1.0.1.tgz", + "integrity": "sha1-z6I7xYQPkQTOMqZedNt+epdLvuE=", + "dev": true, + "requires": { + "acorn": "5.5.3", + "css": "2.2.3", + "normalize-path": "2.1.1", + "source-map": "0.5.7", + "through2": "2.0.3" + } + }, + "@gulp-sourcemaps/map-sources": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", + "integrity": "sha1-iQrnxdjId/bThIYCFazp1+yUW9o=", + "dev": true, + "requires": { + "normalize-path": "2.1.1", + "through2": "2.0.3" + } + }, + "@sindresorhus/is": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", + "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", + "dev": true + }, + "@sinonjs/formatio": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", + "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", + "dev": true, + "requires": { + "samsam": "1.3.0" + } + }, + "@types/caseless": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz", + "integrity": "sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A==", + "dev": true + }, + "@types/chai": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.3.tgz", + "integrity": "sha512-f5dXGzOJycyzSMdaXVhiBhauL4dYydXwVpavfQ1mVCaGjR56a9QfklXObUxlIY9bGTmCPHEEZ04I16BZ/8w5ww==", + "dev": true + }, + "@types/chai-arrays": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/chai-arrays/-/chai-arrays-1.0.2.tgz", + "integrity": "sha512-/kgYvj5Pwiv/bOlJ6c5GlRF/W6lUGSLrpQGl/7Gg6w7tvBYcf0iF91+wwyuwDYGO2zM0wNpcoPixZVif8I/r6g==", + "dev": true, + "requires": { + "@types/chai": "4.1.3" + } + }, + "@types/chai-as-promised": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.0.tgz", + "integrity": "sha512-MFiW54UOSt+f2bRw8J7LgQeIvE/9b4oGvwU7XW30S9QGAiHGnU/fmiOprsyMkdmH2rl8xSPc0/yrQw8juXU6bQ==", + "dev": true, + "requires": { + "@types/chai": "4.1.3" + } + }, + "@types/commander": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.2.tgz", + "integrity": "sha512-0QEFiR8ljcHp9bAbWxecjVRuAMr16ivPiGOw6KFQBVrVd0RQIcM3xKdRisH2EDWgVWujiYtHwhSkSUoAAGzH7Q==", + "dev": true, + "requires": { + "commander": "2.15.1" + } + }, + "@types/decompress": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.2.tgz", + "integrity": "sha512-2jlSsNAVhrWJtgOV3V85MJ09yRoeUTUWQeeusNYAcJVkUmoVRVElvmkWN0TK+Lgdlyd9pIRyja/DTBcyqD8xyA==", + "dev": true, + "requires": { + "@types/node": "9.4.7" + } + }, + "@types/del": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/del/-/del-3.0.1.tgz", + "integrity": "sha512-y6qRq6raBuu965clKgx6FHuiPu3oHdtmzMPXi8Uahsjdq1L6DL5fS/aY5/s71YwM7k6K1QIWvem5vNwlnNGIkQ==", + "dev": true, + "requires": { + "@types/glob": "5.0.35" + } + }, + "@types/dotenv": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-4.0.3.tgz", + "integrity": "sha512-mmhpINC/HcLGQK5ikFJlLXINVvcxhlrV+ZOUJSN7/ottYl+8X4oSXzS9lBtDkmWAl96EGyGyLrNvk9zqdSH8Fw==", + "dev": true, + "requires": { + "@types/node": "9.4.7" + } + }, + "@types/download": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@types/download/-/download-6.2.2.tgz", + "integrity": "sha512-gwRnrp1yFweJhPGBR01nfesxYcml8SayxHEwA6x+1T+Lqez5iMdCRJgt/I9HqpjMi5Mmtb/7MswY6FN4bMypNg==", + "dev": true, + "requires": { + "@types/decompress": "4.2.2", + "@types/got": "8.3.1", + "@types/node": "9.4.7" + } + }, + "@types/event-stream": { + "version": "3.3.34", + "resolved": "https://registry.npmjs.org/@types/event-stream/-/event-stream-3.3.34.tgz", + "integrity": "sha512-LLiivgWKii4JeMzFy3trrxqkRrVSdue8WmbXyHuSJLwNrhIQU5MTrc65jhxEPwMyh5HR1xevSdD+k2nnSRKw9g==", + "dev": true, + "requires": { + "@types/node": "9.4.7" + } + }, + "@types/events": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==", + "dev": true + }, + "@types/form-data": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", + "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", + "dev": true, + "requires": { + "@types/node": "9.4.7" + } + }, + "@types/fs-extra": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.0.2.tgz", + "integrity": "sha512-Q3FWsbdmkQd1ib11A4XNWQvRD//5KpPoGawA8aB2DR7pWKoW9XQv3+dGxD/Z1eVFze23Okdo27ZQytVFlweKvQ==", + "dev": true, + "requires": { + "@types/node": "9.4.7" + } + }, + "@types/get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha512-TiNg8R1kjDde5Pub9F9vCwZA/BNW9HeXP5b9j7Qucqncy/McfPZ6xze/EyBdXS5FhMIGN6Fx3vg75l5KHy3V1Q==", + "dev": true + }, + "@types/glob": { + "version": "5.0.35", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-5.0.35.tgz", + "integrity": "sha512-wc+VveszMLyMWFvXLkloixT4n0harUIVZjnpzztaZ0nKLuul7Z32iMt2fUFGAaZ4y1XWjFRMtCI5ewvyh4aIeg==", + "dev": true, + "requires": { + "@types/events": "1.2.0", + "@types/minimatch": "3.0.3", + "@types/node": "9.4.7" + } + }, + "@types/got": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@types/got/-/got-8.3.1.tgz", + "integrity": "sha512-CGEPw67/Ub6gNMusk062tueurxN+HyjDCvYl4QVBKiSO+fqluXmRX/wSqST/4RtKth4mz8lDZiaZIpXr/uPROg==", + "dev": true, + "requires": { + "@types/node": "9.4.7" + } + }, + "@types/iconv-lite": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@types/iconv-lite/-/iconv-lite-0.0.1.tgz", + "integrity": "sha1-qjuL2ivlErGuCgV7lC6GnDcKVWk=", + "dev": true, + "requires": { + "@types/node": "9.4.7" + } + }, + "@types/istanbul": { + "version": "0.4.30", + "resolved": "https://registry.npmjs.org/@types/istanbul/-/istanbul-0.4.30.tgz", + "integrity": "sha512-+hQU4fh2G96ze78uI5/V6+SRDZD1UnVrFn23i2eDetwfbBq3s0/zYP92xj/3qyvVMM3WnvS88N56zjz+HmL04A==", + "dev": true + }, + "@types/lodash": { + "version": "4.14.109", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.109.tgz", + "integrity": "sha512-hop8SdPUEzbcJm6aTsmuwjIYQo1tqLseKCM+s2bBqTU2gErwI4fE+aqUVOlscPSQbKHKgtMMPoC+h4AIGOJYvw==", + "dev": true + }, + "@types/md5": { + "version": "2.1.32", + "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.1.32.tgz", + "integrity": "sha1-k+I0N/zRenucqY0CqmAC6DWEL+g=", + "dev": true, + "requires": { + "@types/node": "9.4.7" + } + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/mocha": { + "version": "2.2.48", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.48.tgz", + "integrity": "sha512-nlK/iyETgafGli8Zh9zJVCTicvU3iajSkRwOh3Hhiva598CMqNJ4NcVCGMTGKpGpTYj/9R8RLzS9NAykSSCqGw==", + "dev": true + }, + "@types/node": { + "version": "9.4.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-9.4.7.tgz", + "integrity": "sha512-4Ba90mWNx8ddbafuyGGwjkZMigi+AWfYLSDCpovwsE63ia8w93r3oJ8PIAQc3y8U+XHcnMOHPIzNe3o438Ywcw==", + "dev": true + }, + "@types/request": { + "version": "2.47.0", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.47.0.tgz", + "integrity": "sha512-/KXM5oev+nNCLIgBjkwbk8VqxmzI56woD4VUxn95O+YeQ8hJzcSmIZ1IN3WexiqBb6srzDo2bdMbsXxgXNkz5Q==", + "dev": true, + "requires": { + "@types/caseless": "0.12.1", + "@types/form-data": "2.2.1", + "@types/node": "9.4.7", + "@types/tough-cookie": "2.3.3" + } + }, + "@types/semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", + "dev": true + }, + "@types/shortid": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz", + "integrity": "sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps=", + "dev": true + }, + "@types/sinon": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.3.3.tgz", + "integrity": "sha512-Tt7w/ylBS/OEAlSCwzB0Db1KbxnkycP/1UkQpbvKFYoUuRn4uYsC3xh5TRPrOjTy0i8TIkSz1JdNL4GPVdf3KQ==", + "dev": true + }, + "@types/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", + "dev": true + }, + "@types/tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha512-MDQLxNFRLasqS4UlkWMSACMKeSm1x4Q3TxzUC7KQUsh6RK1ZrQ0VEyE3yzXcBu+K8ejVj4wuX32eUG02yNp+YQ==", + "dev": true + }, + "@types/untildify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/untildify/-/untildify-3.0.0.tgz", + "integrity": "sha512-FTktI3Y1h+gP9GTjTvXBP5v8xpH4RU6uS9POoBcGy4XkS2Np6LNtnP1eiNNth4S7P+qw2c/rugkwBasSHFzJEg==", + "dev": true + }, + "@types/uuid": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.3.tgz", + "integrity": "sha512-5fRLCYhLtDb3hMWqQyH10qtF+Ud2JnNCXTCZ+9ktNdCcgslcuXkDTkFcJNk++MT29yDntDnlF1+jD+uVGumsbw==", + "dev": true, + "requires": { + "@types/node": "9.4.7" + } + }, + "@types/winreg": { + "version": "1.2.30", + "resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.30.tgz", + "integrity": "sha1-kdZxDlNtNFucmwF8V0z2qNpkxRg=", + "dev": true + }, + "@types/xml2js": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.2.tgz", + "integrity": "sha512-8aKUBSj3oGcnuiBmDLm3BIk09RYg01mz9HlQ2u4aS17oJ25DxjQrEUVGFSBVNOfM45pQW4OjcBPplq6r/exJdA==", + "dev": true, + "requires": { + "@types/node": "9.4.7" + } + }, + "JSONStream": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.3.tgz", + "integrity": "sha512-3Sp6WZZ/lXl+nTDoGpGWHEpTnnC6X5fnkolYZR6nwIfzbxxvA8utPWe1gCt7i0m9uVGsSz2IS8K8mJ7HmlduMg==", + "dev": true, + "requires": { + "jsonparse": "1.2.0", + "through": "2.3.8" + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "acorn": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", + "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==", + "dev": true + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.1.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-cyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", + "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-gray": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", + "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-red": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", + "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", + "dev": true + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "2.3.11", + "normalize-path": "2.1.1" + }, + "dependencies": { + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + } + } + }, + "append-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", + "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=", + "dev": true, + "requires": { + "buffer-equal": "1.0.0" + } + }, + "applicationinsights": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-1.0.1.tgz", + "integrity": "sha1-U0Rrgw/o1dYZ7uKieLMdPSUDCSc=", + "requires": { + "diagnostic-channel": "0.2.0", + "diagnostic-channel-publishers": "0.2.1", + "zone.js": "0.7.6" + } + }, + "arch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.0.tgz", + "integrity": "sha1-NhOqRhSQZLPB8GB5Gb8dR4boKIk=" + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "argv": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", + "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=", + "dev": true + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-differ": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", + "dev": true + }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", + "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" + }, + "azure-storage": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/azure-storage/-/azure-storage-2.8.3.tgz", + "integrity": "sha512-gcbdUY0tLivJvjUZD9BAxWrRDcige4OLFHhN3kY0p9oZYAFNNNqwgO7rBXvV+zdoX9HajeMOEog9/S/wxabeGg==", + "dev": true, + "requires": { + "browserify-mime": "1.2.9", + "extend": "1.2.1", + "json-edm-parser": "0.1.2", + "md5.js": "1.3.4", + "readable-stream": "2.0.6", + "request": "2.87.0", + "underscore": "1.8.3", + "uuid": "3.2.1", + "validator": "9.4.1", + "xml2js": "0.2.8", + "xmlbuilder": "0.4.3" + }, + "dependencies": { + "extend": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz", + "integrity": "sha1-oPX9bPyDpf5J72mNYOyKYk3UV2w=", + "dev": true + }, + "request": { + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "dev": true, + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.7.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.0.3", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.2", + "safe-buffer": "5.1.2", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.2.1" + }, + "dependencies": { + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + } + } + }, + "sax": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", + "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=", + "dev": true + }, + "xml2js": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz", + "integrity": "sha1-m4FpCTFjH/CdGVdUn69U9PmAs8I=", + "dev": true, + "requires": { + "sax": "0.5.8" + } + }, + "xmlbuilder": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.3.tgz", + "integrity": "sha1-xGFLp04K0ZbmCcknLNnh3bKKilg=", + "dev": true + } + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "1.0.1", + "class-utils": "0.3.6", + "component-emitter": "1.2.1", + "define-property": "1.0.0", + "isobject": "3.0.1", + "mixin-deep": "1.3.1", + "pascalcase": "0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "beeper": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", + "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=", + "dev": true + }, + "binary-extensions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", + "dev": true + }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "dev": true, + "requires": { + "readable-stream": "2.3.6", + "safe-buffer": "5.1.2" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + } + } + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", + "dev": true + }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "requires": { + "hoek": "4.2.1" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "repeat-element": "1.1.2", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserify-mime": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/browserify-mime/-/browserify-mime-1.2.9.tgz", + "integrity": "sha1-rrGvKN5sDXpqLOQK22j/GEIq8x8=", + "dev": true + }, + "buffer": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-3.6.0.tgz", + "integrity": "sha1-pyyTb3e5a/UvX357RnGAYoVR3vs=", + "dev": true, + "requires": { + "base64-js": "0.0.8", + "ieee754": "1.1.11", + "isarray": "1.0.0" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "requires": { + "buffer-alloc-unsafe": "1.1.0", + "buffer-fill": "1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, + "buffer-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", + "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", + "dev": true + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "dev": true + }, + "buffer-from": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", + "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "1.0.0", + "component-emitter": "1.2.1", + "get-value": "2.0.6", + "has-value": "1.0.0", + "isobject": "3.0.1", + "set-value": "2.0.0", + "to-object-path": "0.3.0", + "union-value": "1.0.0", + "unset-value": "1.0.0" + } + }, + "cacheable-request": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", + "integrity": "sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=", + "dev": true, + "requires": { + "clone-response": "1.0.2", + "get-stream": "3.0.0", + "http-cache-semantics": "3.8.1", + "keyv": "3.0.0", + "lowercase-keys": "1.0.0", + "normalize-url": "2.0.1", + "responselike": "1.0.2" + }, + "dependencies": { + "lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", + "dev": true + } + } + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", + "dev": true + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "caw": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", + "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", + "dev": true, + "requires": { + "get-proxy": "2.1.0", + "isurl": "1.0.0", + "tunnel-agent": "0.6.0", + "url-to-options": "1.0.1" + } + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chai": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", + "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", + "dev": true, + "requires": { + "assertion-error": "1.1.0", + "check-error": "1.0.2", + "deep-eql": "3.0.1", + "get-func-name": "2.0.0", + "pathval": "1.1.0", + "type-detect": "4.0.8" + } + }, + "chai-arrays": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chai-arrays/-/chai-arrays-2.0.0.tgz", + "integrity": "sha512-jWAvZu1BV8tL3pj0iosBECzzHEg+XB1zSnMjJGX83bGi/1GlGdDO7J/A0sbBBS6KJT0FVqZIzZW9C6WLiMkHpQ==", + "dev": true + }, + "chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "requires": { + "check-error": "1.0.2" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "1.3.2", + "async-each": "1.0.1", + "fsevents": "1.2.4", + "glob-parent": "2.0.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "2.0.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0" + }, + "dependencies": { + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + } + } + }, + "ci-info": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.3.tgz", + "integrity": "sha512-SK/846h/Rcy8q9Z9CAwGBLfCJ6EkjJWdpelWDufQpqVDYq2Wnnv8zlSO6AMQap02jvhVruKKpEtQOufo3pFhLg==", + "dev": true + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "clap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", + "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", + "dev": true, + "requires": { + "chalk": "1.1.3" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "3.1.0", + "define-property": "0.2.5", + "isobject": "3.0.1", + "static-extend": "0.1.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "optional": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true, + "optional": true + } + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", + "dev": true + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "requires": { + "mimic-response": "1.0.0" + } + }, + "clone-stats": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", + "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", + "dev": true + }, + "cloneable-readable": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.2.tgz", + "integrity": "sha512-Bq6+4t+lbM8vhTs/Bef5c5AdEMtapp/iFb6+s4/Hh9MVTt8OLKH7ZOOZSCT+Ys7hsHvqv0GuMPJ1lnQJVHvxpg==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "process-nextick-args": "2.0.0", + "readable-stream": "2.3.6" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "coa": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", + "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=", + "dev": true, + "requires": { + "q": "1.5.1" + } + }, + "codecov": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.0.2.tgz", + "integrity": "sha512-9ljtIROIjPIUmMRqO+XuDITDoV8xRrZmA0jcEq6p2hg2+wY9wGmLfreAZGIL72IzUfdEDZaU8+Vjidg1fBQ8GQ==", + "dev": true, + "requires": { + "argv": "0.0.2", + "request": "2.85.0", + "urlgrey": "0.4.4" + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "1.0.0", + "object-visit": "1.0.1" + } + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true + }, + "colors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.0.tgz", + "integrity": "sha512-EDpX3a7wHMWFA7PUHWPHNWqOxIIRSJetuwl0AS5Oi/5FMV8kWm69RTlgm00GKjBO1xFHMtBbL49yRtMMdticBw==", + "dev": true + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "commandpost": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/commandpost/-/commandpost-1.3.0.tgz", + "integrity": "sha512-T62tyrmYTkaRDbV2z1k2yXTyxk0cFptXYwo1cUbnfHtp7ThLgQ9/90jG1Ym5WLZgFhvOTaHA5VSARWJ9URpLDw==", + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "config-chain": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.11.tgz", + "integrity": "sha1-q6CXR9++TD5w52am5BWG4YWfxvI=", + "dev": true, + "requires": { + "ini": "1.3.5", + "proto-list": "1.2.4" + } + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", + "dev": true + }, + "convert-source-map": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", + "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", + "dev": true + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "1.0.4", + "path-key": "2.0.1", + "semver": "5.5.0", + "shebang-command": "1.2.0", + "which": "1.3.1" + } + }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "requires": { + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "requires": { + "hoek": "4.2.1" + } + } + } + }, + "css": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/css/-/css-2.2.3.tgz", + "integrity": "sha512-0W171WccAjQGGTKLhw4m2nnl0zPHUlTO/I8td4XzJgIB8Hg3ZZx71qT4G4eX8OVsSiaAKiUMy73E3nsbPlg2DQ==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "source-map": "0.1.43", + "source-map-resolve": "0.5.2", + "urix": "0.1.0" + }, + "dependencies": { + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "css-tree": { + "version": "1.0.0-alpha25", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha25.tgz", + "integrity": "sha512-XC6xLW/JqIGirnZuUWHXCHRaAjje2b3OIB0Vj5RIJo6mIi/AdJo30quQl5LxUl0gkXDIrTrFGbMlcZjyFplz1A==", + "dev": true, + "requires": { + "mdn-data": "1.1.4", + "source-map": "0.5.7" + } + }, + "csso": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-3.4.0.tgz", + "integrity": "sha1-V7J+9VPMy/WqlkxkF0hkHprxE/M=", + "dev": true, + "requires": { + "css-tree": "1.0.0-alpha25" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "1.0.2" + } + }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true, + "requires": { + "es5-ext": "0.10.43" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "dateformat": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", + "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=", + "dev": true + }, + "debounce": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.1.0.tgz", + "integrity": "sha512-ZQVKfRVlwRfD150ndzEK8M90ABT+Y/JQKs4Y7U4MXdpuoUkkrr4DwKbVux3YjylA5bUMUj0Nc3pMxPJX6N2QQQ==", + "dev": true + }, + "debounce-hashed": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/debounce-hashed/-/debounce-hashed-0.1.2.tgz", + "integrity": "sha1-oN/jB8Gn2zD2kRyM+8DvhB831K8=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "debug-fabulous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-1.1.0.tgz", + "integrity": "sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg==", + "dev": true, + "requires": { + "debug": "3.1.0", + "memoizee": "0.4.12", + "object-assign": "4.1.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "decache": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/decache/-/decache-4.4.0.tgz", + "integrity": "sha1-b232uF1+fEQQqTL/wmSJt46azRM=", + "dev": true, + "requires": { + "callsite": "1.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "decompress": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz", + "integrity": "sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=", + "dev": true, + "requires": { + "decompress-tar": "4.1.1", + "decompress-tarbz2": "4.1.1", + "decompress-targz": "4.1.1", + "decompress-unzip": "4.0.1", + "graceful-fs": "4.1.11", + "make-dir": "1.3.0", + "pify": "2.3.0", + "strip-dirs": "2.1.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "dev": true, + "requires": { + "mimic-response": "1.0.0" + } + }, + "decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "requires": { + "file-type": "5.2.0", + "is-stream": "1.1.0", + "tar-stream": "1.6.1" + }, + "dependencies": { + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "dev": true + } + } + }, + "decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "requires": { + "decompress-tar": "4.1.1", + "file-type": "6.2.0", + "is-stream": "1.1.0", + "seek-bzip": "1.0.5", + "unbzip2-stream": "1.2.5" + }, + "dependencies": { + "file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true + } + } + }, + "decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "requires": { + "decompress-tar": "4.1.1", + "file-type": "5.2.0", + "is-stream": "1.1.0" + }, + "dependencies": { + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "dev": true + } + } + }, + "decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", + "dev": true, + "requires": { + "file-type": "3.9.0", + "get-stream": "2.3.1", + "pify": "2.3.0", + "yauzl": "2.9.1" + }, + "dependencies": { + "file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", + "dev": true + }, + "get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", + "dev": true, + "requires": { + "object-assign": "4.1.1", + "pinkie-promise": "2.0.1" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "deep-assign": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/deep-assign/-/deep-assign-1.0.0.tgz", + "integrity": "sha1-sJJ0O+hCfcYh6gBnzex+cN0Z83s=", + "dev": true, + "requires": { + "is-obj": "1.0.1" + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "deepmerge": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.1.1.tgz", + "integrity": "sha512-urQxA1smbLZ2cBbXbaYObM1dJ82aJ2H57A1C/Kklfh/ZN1bgH4G/n5KWhdNfOK11W98gqZfyYj7W4frJJRwA2w==", + "dev": true + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "1.0.4" + } + }, + "define-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", + "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "dev": true, + "requires": { + "foreach": "2.0.5", + "object-keys": "1.0.11" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "1.0.2", + "isobject": "3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "del": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", + "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", + "dev": true, + "requires": { + "globby": "6.1.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.1", + "p-map": "1.2.0", + "pify": "3.0.0", + "rimraf": "2.6.2" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "deprecated": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/deprecated/-/deprecated-0.0.1.tgz", + "integrity": "sha1-+cmvVGSvoeepcUWKi97yqpTVuxk=", + "dev": true + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true + }, + "detect-indent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", + "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=", + "dev": true + }, + "detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "dev": true + }, + "diagnostic-channel": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz", + "integrity": "sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc=", + "requires": { + "semver": "5.5.0" + } + }, + "diagnostic-channel-publishers": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz", + "integrity": "sha1-ji1geottef6IC1SLxYzGvrKIxPM=" + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "diff-match-patch": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.0.tgz", + "integrity": "sha1-HMPIOkkNZ/ldkeOfatHy4Ia2MEg=" + }, + "doctrine": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz", + "integrity": "sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM=", + "dev": true, + "requires": { + "esutils": "1.1.6", + "isarray": "0.0.1" + }, + "dependencies": { + "esutils": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz", + "integrity": "sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "dev": true, + "requires": { + "domelementtype": "1.1.3", + "entities": "1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", + "dev": true + } + } + }, + "domelementtype": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", + "dev": true + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "requires": { + "domelementtype": "1.3.0" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "requires": { + "dom-serializer": "0.1.0", + "domelementtype": "1.3.0" + } + }, + "dotenv": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz", + "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==" + }, + "download": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/download/-/download-7.0.0.tgz", + "integrity": "sha512-0Fe/CAjKycx12IG9We9gYlLP03BEcWTpttg7P5mwfOiQTg584kpuHqP7F61RkUJM+mfEdEU9TJonm0PJp5rQLw==", + "dev": true, + "requires": { + "caw": "2.0.1", + "content-disposition": "0.5.2", + "decompress": "4.2.0", + "ext-name": "5.0.0", + "file-type": "7.7.1", + "filenamify": "2.0.0", + "get-stream": "3.0.0", + "got": "8.3.1", + "make-dir": "1.3.0", + "p-event": "1.3.0", + "pify": "3.0.0" + } + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, + "duplexer2": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", + "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", + "dev": true, + "requires": { + "readable-stream": "1.1.14" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + } + } + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, + "duplexify": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz", + "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==", + "dev": true, + "requires": { + "end-of-stream": "1.4.1", + "inherits": "2.0.3", + "readable-stream": "2.0.6", + "stream-shift": "1.0.0" + }, + "dependencies": { + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "1.4.0" + } + } + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "editorconfig": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.13.3.tgz", + "integrity": "sha512-WkjsUNVCu+ITKDj73QDvi0trvpdDWdkDyHybDGSXPfekLCqwmpD7CP7iPbvBgosNuLcI96XTDwNa75JyFl7tEQ==", + "dev": true, + "requires": { + "bluebird": "3.5.1", + "commander": "2.15.1", + "lru-cache": "3.2.0", + "semver": "5.5.0", + "sigmund": "1.0.1" + }, + "dependencies": { + "lru-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-3.2.0.tgz", + "integrity": "sha1-cXibO39Tmb7IVl3aOKow0qCX7+4=", + "dev": true, + "requires": { + "pseudomap": "1.0.2" + } + } + } + }, + "end-of-stream": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-0.1.5.tgz", + "integrity": "sha1-jhdyBsPICDfYVjLouTWd/osvbq8=", + "dev": true, + "requires": { + "once": "1.3.3" + }, + "dependencies": { + "once": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + } + } + }, + "entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", + "dev": true + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "es5-ext": { + "version": "0.10.43", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.43.tgz", + "integrity": "sha512-cZd1vezWuTM5qMlasKWqQFioFKwO352nVBzhOTMUf/pKQl5Gcq5EdJzqtSNXKnFQSCJDiQZjCYlYbnzFB657OA==", + "dev": true, + "requires": { + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1", + "next-tick": "1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.43", + "es6-symbol": "3.1.1" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.43" + } + }, + "es6-weak-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.43", + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "2.7.3", + "estraverse": "1.9.3", + "esutils": "2.0.2", + "optionator": "0.8.2", + "source-map": "0.2.0" + }, + "dependencies": { + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.43" + } + }, + "event-stream": { + "version": "3.3.4", + "resolved": "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "dev": true, + "requires": { + "duplexer": "0.1.1", + "from": "0.1.7", + "map-stream": "0.1.0", + "pause-stream": "0.0.11", + "split": "0.3.3", + "stream-combiner": "0.0.4", + "through": "2.3.8" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "2.2.4" + }, + "dependencies": { + "fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "3.0.0", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "1.0.1" + } + }, + "ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dev": true, + "requires": { + "mime-db": "1.33.0" + } + }, + "ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dev": true, + "requires": { + "ext-list": "2.2.2", + "sort-keys-length": "1.0.1" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fancy-log": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.2.tgz", + "integrity": "sha1-9BEl49hPLn2JpD0G2VjI94vha+E=", + "dev": true, + "requires": { + "ansi-gray": "0.1.1", + "color-support": "1.1.3", + "time-stamp": "1.1.0" + } + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "1.2.0" + } + }, + "file-type": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-7.7.1.tgz", + "integrity": "sha512-bTrKkzzZI6wH+NXhyD3SOXtb2zXTw2SbwI2RxUlRcXVsnN7jNL5hJzVQLYv7FOQhxFkK4XWdAflEaWFpaLLWpQ==", + "dev": true + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", + "dev": true + }, + "filenamify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.0.0.tgz", + "integrity": "sha1-vRYiYsC26Uv7zc8Zo7uzdk94VpU=", + "dev": true, + "requires": { + "filename-reserved-regex": "2.0.0", + "strip-outer": "1.0.1", + "trim-repeated": "1.0.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "find-index": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-index/-/find-index-0.1.1.tgz", + "integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=", + "dev": true + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "dev": true, + "requires": { + "detect-file": "1.0.0", + "is-glob": "3.1.0", + "micromatch": "3.1.10", + "resolve-dir": "1.0.1" + } + }, + "fined": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.1.0.tgz", + "integrity": "sha1-s33IRLdqL15wgeiE98CuNE8VNHY=", + "dev": true, + "requires": { + "expand-tilde": "2.0.2", + "is-plain-object": "2.0.4", + "object.defaults": "1.1.0", + "object.pick": "1.3.0", + "parse-filepath": "1.0.2" + } + }, + "first-chunk-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz", + "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=", + "dev": true + }, + "flagged-respawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.0.tgz", + "integrity": "sha1-Tnmumy6zi/hrO7Vr8+ClaqX8q9c=", + "dev": true + }, + "flat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.0.0.tgz", + "integrity": "sha512-ji/WMv2jdsE+LaznpkIF9Haax0sdpTBozrz/Dtg4qSRMfbs8oVg4ypJunIRYPiMLvH/ed6OflXbnbTIKJhtgeg==", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "flush-write-stream": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", + "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.0.6" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "0.2.2" + } + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", + "dev": true + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.0.6" + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + }, + "fs-mkdirp-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", + "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "through2": "2.0.3" + } + }, + "fs-walk": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/fs-walk/-/fs-walk-0.0.1.tgz", + "integrity": "sha1-9/yRw64e6tB8mYvF0N1B8tvr0zU=", + "dev": true, + "requires": { + "async": "1.5.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "dev": true, + "optional": true, + "requires": { + "nan": "2.10.0", + "node-pre-gyp": "0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": "2.1.2" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "minipass": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "minizlib": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "2.6.9", + "iconv-lite": "0.4.21", + "sax": "1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "1.0.3", + "mkdirp": "0.5.1", + "needle": "2.2.0", + "nopt": "4.0.1", + "npm-packlist": "1.1.10", + "npmlog": "4.1.2", + "rc": "1.2.7", + "rimraf": "2.6.2", + "semver": "5.5.0", + "tar": "4.4.1" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.5" + } + }, + "npm-bundled": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "3.0.1", + "npm-bundled": "1.0.3" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "0.5.1", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true, + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "1.0.1", + "fs-minipass": "1.2.5", + "minipass": "2.2.4", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true, + "dev": true + } + } + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "fuzzy": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", + "integrity": "sha1-THbsL/CsGjap3M+aAN+GIweNTtg=" + }, + "gaze": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.5.2.tgz", + "integrity": "sha1-QLcJU30k0dRXZ9takIaJ3+aaxE8=", + "dev": true, + "requires": { + "globule": "0.1.0" + } + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=" + }, + "get-proxy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", + "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", + "dev": true, + "requires": { + "npm-conf": "1.1.3" + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + }, + "dependencies": { + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "3.1.0", + "path-dirname": "1.0.2" + } + }, + "glob-stream": { + "version": "3.1.18", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-3.1.18.tgz", + "integrity": "sha1-kXCl8St5Awb9/lmPMT+PeVT9FDs=", + "dev": true, + "requires": { + "glob": "4.5.3", + "glob2base": "0.0.12", + "minimatch": "2.0.10", + "ordered-read-streams": "0.1.0", + "through2": "0.6.5", + "unique-stream": "1.0.0" + }, + "dependencies": { + "glob": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-4.5.3.tgz", + "integrity": "sha1-xstz0yJsHv7wTePFbQEvAzd+4V8=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "2.0.10", + "once": "1.4.0" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "minimatch": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", + "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "xtend": "4.0.1" + } + } + } + }, + "glob-watcher": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-0.0.6.tgz", + "integrity": "sha1-uVtKjfdLOcgymLDAXJeLTZo7cQs=", + "dev": true, + "requires": { + "gaze": "0.5.2" + } + }, + "glob2base": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz", + "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=", + "dev": true, + "requires": { + "find-index": "0.1.1" + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "1.0.2", + "is-windows": "1.0.2", + "resolve-dir": "1.0.1" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "2.0.2", + "homedir-polyfill": "1.0.1", + "ini": "1.3.5", + "is-windows": "1.0.2", + "which": "1.3.1" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "globule": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globule/-/globule-0.1.0.tgz", + "integrity": "sha1-2cjt3h2nnRJaFRt5UzuXhnY0auU=", + "dev": true, + "requires": { + "glob": "3.1.21", + "lodash": "1.0.2", + "minimatch": "0.2.14" + }, + "dependencies": { + "glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "dev": true, + "requires": { + "graceful-fs": "1.2.3", + "inherits": "1.0.2", + "minimatch": "0.2.14" + } + }, + "graceful-fs": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", + "dev": true + }, + "inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", + "dev": true + }, + "lodash": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.0.2.tgz", + "integrity": "sha1-j1dWDIO1n8JwvT1WG2kAQ0MOJVE=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + } + } + }, + "glogg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.1.tgz", + "integrity": "sha512-ynYqXLoluBKf9XGR1gA59yEJisIL7YHEH4xr3ZziHB5/yl4qWfaK8Js9jGe6gBGCSCKVqiyO30WnRZADvemUNw==", + "dev": true, + "requires": { + "sparkles": "1.0.1" + } + }, + "got": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/got/-/got-8.3.1.tgz", + "integrity": "sha512-tiLX+bnYm5A56T5N/n9Xo89vMaO1mrS9qoDqj3u/anVooqGozvY/HbXzEpDfbNeKsHCBpK40gSbz8wGYSp3i1w==", + "dev": true, + "requires": { + "@sindresorhus/is": "0.7.0", + "cacheable-request": "2.1.4", + "decompress-response": "3.3.0", + "duplexer3": "0.1.4", + "get-stream": "3.0.0", + "into-stream": "3.1.0", + "is-retry-allowed": "1.1.0", + "isurl": "1.0.0", + "lowercase-keys": "1.0.1", + "mimic-response": "1.0.0", + "p-cancelable": "0.4.1", + "p-timeout": "2.0.1", + "pify": "3.0.0", + "safe-buffer": "5.1.2", + "timed-out": "4.0.1", + "url-parse-lax": "3.0.0", + "url-to-options": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "gulp": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-3.9.1.tgz", + "integrity": "sha1-VxzkWSjdQK9lFPxAEYZgFsE4RbQ=", + "dev": true, + "requires": { + "archy": "1.0.0", + "chalk": "1.1.3", + "deprecated": "0.0.1", + "gulp-util": "3.0.8", + "interpret": "1.1.0", + "liftoff": "2.5.0", + "minimist": "1.2.0", + "orchestrator": "0.3.8", + "pretty-hrtime": "1.0.3", + "semver": "4.3.6", + "tildify": "1.2.0", + "v8flags": "2.1.1", + "vinyl-fs": "0.3.14" + }, + "dependencies": { + "semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "dev": true + } + } + }, + "gulp-chmod": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/gulp-chmod/-/gulp-chmod-2.0.0.tgz", + "integrity": "sha1-AMOQuSigeZslGsz2MaoJ4BzGKZw=", + "dev": true, + "requires": { + "deep-assign": "1.0.0", + "stat-mode": "0.2.2", + "through2": "2.0.3" + } + }, + "gulp-debounced-watch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/gulp-debounced-watch/-/gulp-debounced-watch-1.0.4.tgz", + "integrity": "sha1-WkfU4kzkY2XOguysMqKjA+QysSo=", + "dev": true, + "requires": { + "debounce-hashed": "0.1.2", + "gulp-watch": "4.3.11", + "object-assign": "3.0.0" + }, + "dependencies": { + "gulp-watch": { + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/gulp-watch/-/gulp-watch-4.3.11.tgz", + "integrity": "sha1-Fi/FY96fx3DpH5p845VVE6mhGMA=", + "dev": true, + "requires": { + "anymatch": "1.3.2", + "chokidar": "1.7.0", + "glob-parent": "3.1.0", + "gulp-util": "3.0.8", + "object-assign": "4.1.1", + "path-is-absolute": "1.0.1", + "readable-stream": "2.3.6", + "slash": "1.0.0", + "vinyl": "1.2.0", + "vinyl-file": "2.0.0" + }, + "dependencies": { + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + } + } + }, + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true, + "requires": { + "clone": "1.0.4", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } + } + } + }, + "gulp-filter": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/gulp-filter/-/gulp-filter-5.1.0.tgz", + "integrity": "sha1-oF4Rr/sHz33PQafeHLe2OsN4PnM=", + "dev": true, + "requires": { + "multimatch": "2.1.0", + "plugin-error": "0.1.2", + "streamfilter": "1.0.7" + } + }, + "gulp-gitmodified": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/gulp-gitmodified/-/gulp-gitmodified-1.1.1.tgz", + "integrity": "sha1-hfNnWRXB1RtmgH8o3g67WR5+nfQ=", + "dev": true, + "requires": { + "gulp-util": "2.2.20", + "lodash.find": "3.2.1", + "through2": "2.0.3", + "vinyl": "0.4.6", + "which": "1.0.9" + }, + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true, + "requires": { + "ansi-styles": "1.1.0", + "escape-string-regexp": "1.0.5", + "has-ansi": "0.1.0", + "strip-ansi": "0.3.0", + "supports-color": "0.2.0" + } + }, + "clone": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", + "dev": true + }, + "dateformat": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", + "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", + "dev": true, + "requires": { + "get-stdin": "4.0.1", + "meow": "3.7.0" + } + }, + "gulp-util": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-2.2.20.tgz", + "integrity": "sha1-1xRuVyiRC9jwR6awseVJvCLb1kw=", + "dev": true, + "requires": { + "chalk": "0.5.1", + "dateformat": "1.0.12", + "lodash._reinterpolate": "2.4.1", + "lodash.template": "2.4.1", + "minimist": "0.2.0", + "multipipe": "0.1.2", + "through2": "0.5.1", + "vinyl": "0.2.3" + }, + "dependencies": { + "through2": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz", + "integrity": "sha1-390BLrnHAOIyP9M084rGIqs3Lac=", + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "xtend": "3.0.0" + } + }, + "vinyl": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.2.3.tgz", + "integrity": "sha1-vKk4IJWC7FpJrVOKAPofEl5RMlI=", + "dev": true, + "requires": { + "clone-stats": "0.0.1" + } + } + } + }, + "has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true, + "requires": { + "ansi-regex": "0.2.1" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "lodash._reinterpolate": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-2.4.1.tgz", + "integrity": "sha1-TxInqlqHEfxjL1sHofRgequLMiI=", + "dev": true + }, + "lodash.escape": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-2.4.1.tgz", + "integrity": "sha1-LOEsXghNsKV92l5dHu659dF1o7Q=", + "dev": true, + "requires": { + "lodash._escapehtmlchar": "2.4.1", + "lodash._reunescapedhtml": "2.4.1", + "lodash.keys": "2.4.1" + } + }, + "lodash.keys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", + "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "dev": true, + "requires": { + "lodash._isnative": "2.4.1", + "lodash._shimkeys": "2.4.1", + "lodash.isobject": "2.4.1" + } + }, + "lodash.template": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-2.4.1.tgz", + "integrity": "sha1-nmEQB+32KRKal0qzxIuBez4c8g0=", + "dev": true, + "requires": { + "lodash._escapestringchar": "2.4.1", + "lodash._reinterpolate": "2.4.1", + "lodash.defaults": "2.4.1", + "lodash.escape": "2.4.1", + "lodash.keys": "2.4.1", + "lodash.templatesettings": "2.4.1", + "lodash.values": "2.4.1" + } + }, + "lodash.templatesettings": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-2.4.1.tgz", + "integrity": "sha1-6nbHXRHrhtTb6JqDiTu4YZKaxpk=", + "dev": true, + "requires": { + "lodash._reinterpolate": "2.4.1", + "lodash.escape": "2.4.1" + } + }, + "minimist": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.2.0.tgz", + "integrity": "sha1-Tf/lJdriuGTGbC4jxicdev3s784=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "0.2.1" + } + }, + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + }, + "vinyl": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "dev": true, + "requires": { + "clone": "0.2.0", + "clone-stats": "0.0.1" + } + }, + "which": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", + "dev": true + }, + "xtend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=", + "dev": true + } + } + }, + "gulp-gunzip": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulp-gunzip/-/gulp-gunzip-1.0.0.tgz", + "integrity": "sha1-FbdBFF6Dqcb1CIYkG1fMWHHxUak=", + "dev": true, + "requires": { + "through2": "0.6.5", + "vinyl": "0.4.6" + }, + "dependencies": { + "clone": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "xtend": "4.0.1" + } + }, + "vinyl": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "dev": true, + "requires": { + "clone": "0.2.0", + "clone-stats": "0.0.1" + } + } + } + }, + "gulp-inline-source": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/gulp-inline-source/-/gulp-inline-source-3.2.0.tgz", + "integrity": "sha512-Ky5SKDgM517QZ6Jw9rKhYz+ynX9T4sr72iUW2fbOhqzN74D37DzWZDZnbKLSwdlTbbqt7JFq/JmUmT12qroW+A==", + "dev": true, + "requires": { + "inline-source": "5.2.7", + "plugin-error": "1.0.1", + "through2": "2.0.3" + }, + "dependencies": { + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "dev": true, + "requires": { + "ansi-colors": "1.1.0", + "arr-diff": "4.0.0", + "arr-union": "3.1.0", + "extend-shallow": "3.0.2" + } + } + } + }, + "gulp-json-editor": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/gulp-json-editor/-/gulp-json-editor-2.4.1.tgz", + "integrity": "sha512-20nYwO5Bec5X6DfXmBmHEtDAyluTkMguhuvCzqwrHDv/NzwOn3qS4ofAMw9L2gnWAmzxKzHAkFO19LNDWyTwlg==", + "dev": true, + "requires": { + "deepmerge": "2.1.1", + "detect-indent": "5.0.0", + "js-beautify": "1.7.5", + "plugin-error": "1.0.1", + "through2": "2.0.3" + }, + "dependencies": { + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "dev": true, + "requires": { + "ansi-colors": "1.1.0", + "arr-diff": "4.0.0", + "arr-union": "3.1.0", + "extend-shallow": "3.0.2" + } + } + } + }, + "gulp-remote-src-vscode": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/gulp-remote-src-vscode/-/gulp-remote-src-vscode-0.5.0.tgz", + "integrity": "sha512-/9vtSk9eI9DEWCqzGieglPqmx0WUQ9pwPHyHFpKmfxqdgqGJC2l0vFMdYs54hLdDsMDEZFLDL2J4ikjc4hQ5HQ==", + "dev": true, + "requires": { + "event-stream": "3.3.4", + "node.extend": "1.1.6", + "request": "2.85.0", + "through2": "2.0.3", + "vinyl": "2.1.0" + }, + "dependencies": { + "clone": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", + "dev": true + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + }, + "vinyl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.1.0.tgz", + "integrity": "sha1-Ah+cLPlR1rk5lDyJ617lrdT9kkw=", + "dev": true, + "requires": { + "clone": "2.1.1", + "clone-buffer": "1.0.0", + "clone-stats": "1.0.0", + "cloneable-readable": "1.1.2", + "remove-trailing-separator": "1.1.0", + "replace-ext": "1.0.0" + } + } + } + }, + "gulp-sourcemaps": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-2.6.4.tgz", + "integrity": "sha1-y7IAhFCxvM5s0jv5gze+dRv24wo=", + "dev": true, + "requires": { + "@gulp-sourcemaps/identity-map": "1.0.1", + "@gulp-sourcemaps/map-sources": "1.0.0", + "acorn": "5.5.3", + "convert-source-map": "1.5.1", + "css": "2.2.3", + "debug-fabulous": "1.1.0", + "detect-newline": "2.1.0", + "graceful-fs": "4.1.11", + "source-map": "0.6.1", + "strip-bom-string": "1.0.0", + "through2": "2.0.3" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "gulp-symdest": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gulp-symdest/-/gulp-symdest-1.1.0.tgz", + "integrity": "sha1-wWUyBzLRks5W/ZQnH/oSMjS/KuA=", + "dev": true, + "requires": { + "event-stream": "3.3.4", + "mkdirp": "0.5.1", + "queue": "3.1.0", + "vinyl-fs": "2.4.4" + }, + "dependencies": { + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-stream": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-5.3.5.tgz", + "integrity": "sha1-pVZlqajM3EGRWofHAeMtTgFvrSI=", + "dev": true, + "requires": { + "extend": "3.0.1", + "glob": "5.0.15", + "glob-parent": "3.1.0", + "micromatch": "2.3.11", + "ordered-read-streams": "0.3.0", + "through2": "0.6.5", + "to-absolute-glob": "0.1.1", + "unique-stream": "2.2.1" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "xtend": "4.0.1" + } + } + } + }, + "gulp-sourcemaps": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz", + "integrity": "sha1-uG/zSdgBzrVuHZ59x7vLS33uYAw=", + "dev": true, + "requires": { + "convert-source-map": "1.5.1", + "graceful-fs": "4.1.11", + "strip-bom": "2.0.0", + "through2": "2.0.3", + "vinyl": "1.2.0" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-valid-glob": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz", + "integrity": "sha1-1LVcafUYhvm2XHDWwmItN+KfSP4=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, + "ordered-read-streams": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz", + "integrity": "sha1-cTfmmzKYuzQiR6G77jiByA4v14s=", + "dev": true, + "requires": { + "is-stream": "1.1.0", + "readable-stream": "2.0.6" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-bom-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz", + "integrity": "sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=", + "dev": true, + "requires": { + "first-chunk-stream": "1.0.0", + "strip-bom": "2.0.0" + } + }, + "to-absolute-glob": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz", + "integrity": "sha1-HN+kcqnvUMI57maZm2YsoOs5k38=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1" + } + }, + "unique-stream": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", + "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", + "dev": true, + "requires": { + "json-stable-stringify": "1.0.1", + "through2-filter": "2.0.0" + } + }, + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true, + "requires": { + "clone": "1.0.4", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } + }, + "vinyl-fs": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-2.4.4.tgz", + "integrity": "sha1-vm/zJwy1Xf19MGNkDegfJddTIjk=", + "dev": true, + "requires": { + "duplexify": "3.6.0", + "glob-stream": "5.3.5", + "graceful-fs": "4.1.11", + "gulp-sourcemaps": "1.6.0", + "is-valid-glob": "0.3.0", + "lazystream": "1.0.0", + "lodash.isequal": "4.5.0", + "merge-stream": "1.0.1", + "mkdirp": "0.5.1", + "object-assign": "4.1.1", + "readable-stream": "2.0.6", + "strip-bom": "2.0.0", + "strip-bom-stream": "1.0.0", + "through2": "2.0.3", + "through2-filter": "2.0.0", + "vali-date": "1.0.0", + "vinyl": "1.2.0" + } + } + } + }, + "gulp-typescript": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-4.0.2.tgz", + "integrity": "sha512-Hhbn5Aa2l3T+tnn0KqsG6RRJmcYEsr3byTL2nBpNBeAK8pqug9Od4AwddU4JEI+hRw7mzZyjRbB8DDWR6paGVA==", + "dev": true, + "requires": { + "ansi-colors": "1.1.0", + "plugin-error": "0.1.2", + "source-map": "0.6.1", + "through2": "2.0.3", + "vinyl": "2.1.0", + "vinyl-fs": "3.0.3" + }, + "dependencies": { + "clone": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", + "dev": true + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "dev": true, + "requires": { + "extend": "3.0.1", + "glob": "7.1.2", + "glob-parent": "3.1.0", + "is-negated-glob": "1.0.0", + "ordered-read-streams": "1.0.1", + "pumpify": "1.5.1", + "readable-stream": "2.3.6", + "remove-trailing-separator": "1.1.0", + "to-absolute-glob": "2.0.2", + "unique-stream": "2.2.1" + } + }, + "ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", + "dev": true, + "requires": { + "readable-stream": "2.3.6" + } + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "unique-stream": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", + "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", + "dev": true, + "requires": { + "json-stable-stringify": "1.0.1", + "through2-filter": "2.0.0" + } + }, + "vinyl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.1.0.tgz", + "integrity": "sha1-Ah+cLPlR1rk5lDyJ617lrdT9kkw=", + "dev": true, + "requires": { + "clone": "2.1.1", + "clone-buffer": "1.0.0", + "clone-stats": "1.0.0", + "cloneable-readable": "1.1.2", + "remove-trailing-separator": "1.1.0", + "replace-ext": "1.0.0" + } + }, + "vinyl-fs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", + "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", + "dev": true, + "requires": { + "fs-mkdirp-stream": "1.0.0", + "glob-stream": "6.1.0", + "graceful-fs": "4.1.11", + "is-valid-glob": "1.0.0", + "lazystream": "1.0.0", + "lead": "1.0.0", + "object.assign": "4.1.0", + "pumpify": "1.5.1", + "readable-stream": "2.3.6", + "remove-bom-buffer": "3.0.0", + "remove-bom-stream": "1.2.0", + "resolve-options": "1.1.0", + "through2": "2.0.3", + "to-through": "2.0.0", + "value-or-function": "3.0.0", + "vinyl": "2.1.0", + "vinyl-sourcemap": "1.1.0" + } + } + } + }, + "gulp-untar": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/gulp-untar/-/gulp-untar-0.0.7.tgz", + "integrity": "sha512-0QfbCH2a1k2qkTLWPqTX+QO4qNsHn3kC546YhAP3/n0h+nvtyGITDuDrYBMDZeW4WnFijmkOvBWa5HshTic1tw==", + "dev": true, + "requires": { + "event-stream": "3.3.4", + "streamifier": "0.1.1", + "tar": "2.2.1", + "through2": "2.0.3", + "vinyl": "1.2.0" + }, + "dependencies": { + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true, + "requires": { + "clone": "1.0.4", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } + } + } + }, + "gulp-util": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", + "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", + "dev": true, + "requires": { + "array-differ": "1.0.0", + "array-uniq": "1.0.3", + "beeper": "1.1.1", + "chalk": "1.1.3", + "dateformat": "2.2.0", + "fancy-log": "1.3.2", + "gulplog": "1.0.0", + "has-gulplog": "0.1.0", + "lodash._reescape": "3.0.0", + "lodash._reevaluate": "3.0.0", + "lodash._reinterpolate": "3.0.0", + "lodash.template": "3.6.2", + "minimist": "1.2.0", + "multipipe": "0.1.2", + "object-assign": "3.0.0", + "replace-ext": "0.0.1", + "through2": "2.0.3", + "vinyl": "0.5.3" + }, + "dependencies": { + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", + "dev": true + } + } + }, + "gulp-vinyl-zip": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/gulp-vinyl-zip/-/gulp-vinyl-zip-2.1.0.tgz", + "integrity": "sha1-JOQGhdwFtxSZlSRQmeBZAmO+ja0=", + "dev": true, + "requires": { + "event-stream": "3.3.4", + "queue": "4.4.2", + "through2": "2.0.3", + "vinyl": "2.1.0", + "vinyl-fs": "2.4.4", + "yauzl": "2.9.1", + "yazl": "2.4.3" + }, + "dependencies": { + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "clone": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", + "dev": true + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-stream": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-5.3.5.tgz", + "integrity": "sha1-pVZlqajM3EGRWofHAeMtTgFvrSI=", + "dev": true, + "requires": { + "extend": "3.0.1", + "glob": "5.0.15", + "glob-parent": "3.1.0", + "micromatch": "2.3.11", + "ordered-read-streams": "0.3.0", + "through2": "0.6.5", + "to-absolute-glob": "0.1.1", + "unique-stream": "2.2.1" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "xtend": "4.0.1" + } + } + } + }, + "gulp-sourcemaps": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz", + "integrity": "sha1-uG/zSdgBzrVuHZ59x7vLS33uYAw=", + "dev": true, + "requires": { + "convert-source-map": "1.5.1", + "graceful-fs": "4.1.11", + "strip-bom": "2.0.0", + "through2": "2.0.3", + "vinyl": "1.2.0" + }, + "dependencies": { + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "clone-stats": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", + "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", + "dev": true + }, + "replace-ext": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", + "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", + "dev": true + }, + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true, + "requires": { + "clone": "1.0.4", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } + } + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-valid-glob": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz", + "integrity": "sha1-1LVcafUYhvm2XHDWwmItN+KfSP4=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, + "ordered-read-streams": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz", + "integrity": "sha1-cTfmmzKYuzQiR6G77jiByA4v14s=", + "dev": true, + "requires": { + "is-stream": "1.1.0", + "readable-stream": "2.0.6" + } + }, + "queue": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-4.4.2.tgz", + "integrity": "sha512-fSMRXbwhMwipcDZ08enW2vl+YDmAmhcNcr43sCJL8DIg+CFOsoRLG23ctxA+fwNk1w55SePSiS7oqQQSgQoVJQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-bom-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz", + "integrity": "sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=", + "dev": true, + "requires": { + "first-chunk-stream": "1.0.0", + "strip-bom": "2.0.0" + } + }, + "to-absolute-glob": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz", + "integrity": "sha1-HN+kcqnvUMI57maZm2YsoOs5k38=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1" + } + }, + "unique-stream": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", + "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", + "dev": true, + "requires": { + "json-stable-stringify": "1.0.1", + "through2-filter": "2.0.0" + } + }, + "vinyl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.1.0.tgz", + "integrity": "sha1-Ah+cLPlR1rk5lDyJ617lrdT9kkw=", + "dev": true, + "requires": { + "clone": "2.1.1", + "clone-buffer": "1.0.0", + "clone-stats": "1.0.0", + "cloneable-readable": "1.1.2", + "remove-trailing-separator": "1.1.0", + "replace-ext": "1.0.0" + } + }, + "vinyl-fs": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-2.4.4.tgz", + "integrity": "sha1-vm/zJwy1Xf19MGNkDegfJddTIjk=", + "dev": true, + "requires": { + "duplexify": "3.6.0", + "glob-stream": "5.3.5", + "graceful-fs": "4.1.11", + "gulp-sourcemaps": "1.6.0", + "is-valid-glob": "0.3.0", + "lazystream": "1.0.0", + "lodash.isequal": "4.5.0", + "merge-stream": "1.0.1", + "mkdirp": "0.5.1", + "object-assign": "4.1.1", + "readable-stream": "2.0.6", + "strip-bom": "2.0.0", + "strip-bom-stream": "1.0.0", + "through2": "2.0.3", + "through2-filter": "2.0.0", + "vali-date": "1.0.0", + "vinyl": "1.2.0" + }, + "dependencies": { + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "clone-stats": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", + "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", + "dev": true + }, + "replace-ext": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", + "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", + "dev": true + }, + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true, + "requires": { + "clone": "1.0.4", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } + } + } + } + } + }, + "gulp-watch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gulp-watch/-/gulp-watch-5.0.0.tgz", + "integrity": "sha512-q+HLppxXd11z9ndqql4Z0sd5xOAesJjycl0PRaq6ImK7b1BqBRL37YvxEE8ngUdIfpfHa0O9OCoovoggcFpCaQ==", + "dev": true, + "requires": { + "anymatch": "1.3.2", + "chokidar": "2.0.3", + "glob-parent": "3.1.0", + "gulp-util": "3.0.8", + "object-assign": "4.1.1", + "path-is-absolute": "1.0.1", + "readable-stream": "2.3.6", + "slash": "1.0.0", + "vinyl": "2.1.0", + "vinyl-file": "2.0.0" + }, + "dependencies": { + "chokidar": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", + "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", + "dev": true, + "requires": { + "anymatch": "2.0.0", + "async-each": "1.0.1", + "braces": "2.3.2", + "fsevents": "1.2.4", + "glob-parent": "3.1.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "4.0.0", + "normalize-path": "2.1.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0", + "upath": "1.1.0" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "3.1.10", + "normalize-path": "2.1.1" + } + } + } + }, + "clone": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", + "dev": true + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "dev": true, + "requires": { + "is-extglob": "2.1.1" + } + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "vinyl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.1.0.tgz", + "integrity": "sha1-Ah+cLPlR1rk5lDyJ617lrdT9kkw=", + "dev": true, + "requires": { + "clone": "2.1.1", + "clone-buffer": "1.0.0", + "clone-stats": "1.0.0", + "cloneable-readable": "1.1.2", + "remove-trailing-separator": "1.1.0", + "replace-ext": "1.0.0" + } + } + } + }, + "gulplog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", + "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", + "dev": true, + "requires": { + "glogg": "1.0.1" + } + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "5.5.2", + "har-schema": "2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "has-gulplog": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", + "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=", + "dev": true, + "requires": { + "sparkles": "1.0.1" + } + }, + "has-symbol-support-x": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", + "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", + "dev": true + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "has-to-string-tag-x": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", + "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", + "dev": true, + "requires": { + "has-symbol-support-x": "1.4.2" + } + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "1.0.0", + "isobject": "3.0.1" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "requires": { + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.1", + "sntp": "2.1.0" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hoek": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" + }, + "homedir-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "dev": true, + "requires": { + "parse-passwd": "1.0.0" + } + }, + "hosted-git-info": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", + "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", + "dev": true + }, + "htmlparser2": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", + "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", + "dev": true, + "requires": { + "domelementtype": "1.3.0", + "domhandler": "2.4.2", + "domutils": "1.7.0", + "entities": "1.1.1", + "inherits": "2.0.3", + "readable-stream": "2.0.6" + } + }, + "http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", + "dev": true + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.14.1" + } + }, + "husky": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-0.14.3.tgz", + "integrity": "sha512-e21wivqHpstpoiWA/Yi8eFti8E+sQDSS53cpJsPptPs295QTOQR0ZwnHo2TXy1XOpZFD9rPOd3NpmqTK6uMLJA==", + "dev": true, + "requires": { + "is-ci": "1.1.0", + "normalize-path": "1.0.0", + "strip-indent": "2.0.0" + }, + "dependencies": { + "normalize-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-1.0.0.tgz", + "integrity": "sha1-MtDkcvkf80VwHBWoMRAY07CpA3k=", + "dev": true + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true + } + } + }, + "iconv-lite": { + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", + "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", + "requires": { + "safer-buffer": "2.1.2" + } + }, + "ieee754": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.11.tgz", + "integrity": "sha512-VhDzCKN7K8ufStx/CLj5/PDTMgph+qwN5Pkd5i0sGnVwk56zJ0lkT8Qzi1xqWLS0Wp29DgDtNeS7v8/wMoZeHg==", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "inline-source": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/inline-source/-/inline-source-5.2.7.tgz", + "integrity": "sha512-RvMOGMXxAqqve4ld128B7TYyNR2aP1LB38dcSpWFmqXrhKPuey1+yFU6kFUgyH8IWX+gRZdWtHN4eQ9d0IpFZg==", + "dev": true, + "requires": { + "csso": "3.4.0", + "htmlparser2": "3.9.2", + "is-plain-obj": "1.1.0", + "object-assign": "4.1.1", + "svgo": "0.7.2", + "uglify-js": "3.3.28" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "uglify-js": { + "version": "3.3.28", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.3.28.tgz", + "integrity": "sha512-68Rc/aA6cswiaQ5SrE979UJcXX+ADA1z33/ZsPd+fbAiVdjZ16OXdbtGO+rJUUBgK6qdf3SOPhQf3K/ybF5Miw==", + "dev": true, + "requires": { + "commander": "2.15.1", + "source-map": "0.6.1" + } + } + } + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "into-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", + "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=", + "dev": true, + "requires": { + "from2": "2.3.0", + "p-is-promise": "1.1.0" + } + }, + "inversify": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-4.11.1.tgz", + "integrity": "sha512-9bs/36crPdTSOCcoomHMb96s+B8W0+2c9dHFP/Srv9ZQaPnUvsMgzmMHfgVECqfHVUIW+M5S7SYOjoig8khWuQ==" + }, + "is": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", + "integrity": "sha1-0Kwq1V63sL7JJqUmb2xmKqqD3KU=", + "dev": true + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "requires": { + "is-relative": "1.0.0", + "is-windows": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "1.11.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-ci": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", + "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", + "dev": true, + "requires": { + "ci-info": "1.1.3" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "2.1.1" + } + }, + "is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", + "dev": true + }, + "is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", + "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", + "dev": true + }, + "is-odd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", + "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", + "dev": true, + "requires": { + "is-number": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "dev": true, + "requires": { + "is-path-inside": "1.0.1" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "requires": { + "is-unc-path": "1.0.0" + } + }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "dev": true + }, + "is-running": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-running/-/is-running-2.1.0.tgz", + "integrity": "sha1-MKc/9cw4VOT8JUkICen1q/jeCeA=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "requires": { + "unc-path-regex": "0.1.2" + } + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "abbrev": "1.0.9", + "async": "1.5.2", + "escodegen": "1.8.1", + "esprima": "2.7.3", + "glob": "5.0.15", + "handlebars": "4.0.11", + "js-yaml": "3.11.0", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "once": "1.4.0", + "resolve": "1.1.7", + "supports-color": "3.2.3", + "which": "1.3.1", + "wordwrap": "1.0.0" + }, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "isurl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", + "dev": true, + "requires": { + "has-to-string-tag-x": "1.4.1", + "is-object": "1.0.1" + } + }, + "js-beautify": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.7.5.tgz", + "integrity": "sha512-9OhfAqGOrD7hoQBLJMTA+BKuKmoEtTJXzZ7WDF/9gvjtey1koVLuZqIY6c51aPDjbNdNtIXAkiWKVhziawE9Og==", + "dev": true, + "requires": { + "config-chain": "1.1.11", + "editorconfig": "0.13.3", + "mkdirp": "0.5.1", + "nopt": "3.0.6" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz", + "integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==", + "dev": true, + "requires": { + "argparse": "1.0.10", + "esprima": "4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + } + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "dev": true + }, + "json-edm-parser": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/json-edm-parser/-/json-edm-parser-0.1.2.tgz", + "integrity": "sha1-HmCw/vG8CvZ7wNFG393lSGzWFbQ=", + "dev": true, + "requires": { + "jsonparse": "1.2.0" + } + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "4.1.11" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonparse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.2.0.tgz", + "integrity": "sha1-XAxWhRBxYOcv50ib3eoLRMK8Z70=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "just-extend": { + "version": "1.1.27", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-1.1.27.tgz", + "integrity": "sha512-mJVp13Ix6gFo3SBAy9U/kL+oeZqzlYYYLQBwXVBlVzIsZwBqGREnOro24oC/8s8aox+rJhtZ2DiQof++IrkA+g==", + "dev": true + }, + "keyv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", + "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", + "dev": true, + "requires": { + "json-buffer": "3.0.0" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true, + "optional": true + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "dev": true, + "requires": { + "readable-stream": "2.0.6" + } + }, + "lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", + "dev": true, + "requires": { + "flush-write-stream": "1.0.3" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, + "liftoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz", + "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=", + "dev": true, + "requires": { + "extend": "3.0.1", + "findup-sync": "2.0.0", + "fined": "1.1.0", + "flagged-respawn": "1.0.0", + "is-plain-object": "2.0.4", + "object.map": "1.0.1", + "rechoir": "0.6.2", + "resolve": "1.7.1" + } + }, + "line-by-line": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/line-by-line/-/line-by-line-0.1.6.tgz", + "integrity": "sha512-MmwVPfOyp0lWnEZ3fBA8Ah4pMFvxO6WgWovqZNu7Y4J0TNnGcsV4S1LzECHbdgqk1hoHc2mFP1Axc37YUqwafg==" + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + } + } + }, + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" + }, + "lodash._basecallback": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/lodash._basecallback/-/lodash._basecallback-3.3.1.tgz", + "integrity": "sha1-t7K7Q9whYEJKIczybFfkQ3cqjic=", + "dev": true, + "requires": { + "lodash._baseisequal": "3.0.7", + "lodash._bindcallback": "3.0.1", + "lodash.isarray": "3.0.4", + "lodash.pairs": "3.0.1" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._baseeach": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash._baseeach/-/lodash._baseeach-3.0.4.tgz", + "integrity": "sha1-z4cGVyyhROjZ11InyZDamC+TKvM=", + "dev": true, + "requires": { + "lodash.keys": "3.1.2" + } + }, + "lodash._basefind": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._basefind/-/lodash._basefind-3.0.0.tgz", + "integrity": "sha1-srugXMZF+XLeLPkl+iv2Og9gyK4=", + "dev": true + }, + "lodash._basefindindex": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/lodash._basefindindex/-/lodash._basefindindex-3.6.0.tgz", + "integrity": "sha1-8IM2ChsCJBjtgbyJm+sxLiHnSk8=", + "dev": true + }, + "lodash._baseisequal": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/lodash._baseisequal/-/lodash._baseisequal-3.0.7.tgz", + "integrity": "sha1-2AJfdjOdKTQnZ9zIh85cuVpbUfE=", + "dev": true, + "requires": { + "lodash.isarray": "3.0.4", + "lodash.istypedarray": "3.0.6", + "lodash.keys": "3.1.2" + } + }, + "lodash._basetostring": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", + "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", + "dev": true + }, + "lodash._basevalues": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", + "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=", + "dev": true + }, + "lodash._bindcallback": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", + "dev": true + }, + "lodash._escapehtmlchar": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._escapehtmlchar/-/lodash._escapehtmlchar-2.4.1.tgz", + "integrity": "sha1-32fDu2t+jh6DGrSL+geVuSr+iZ0=", + "dev": true, + "requires": { + "lodash._htmlescapes": "2.4.1" + } + }, + "lodash._escapestringchar": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._escapestringchar/-/lodash._escapestringchar-2.4.1.tgz", + "integrity": "sha1-7P4iYYoq3lC/7qQ5N+Ud9m8O23I=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._htmlescapes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._htmlescapes/-/lodash._htmlescapes-2.4.1.tgz", + "integrity": "sha1-MtFL8IRLbeb4tioFG09nwii2JMs=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash._isnative": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._isnative/-/lodash._isnative-2.4.1.tgz", + "integrity": "sha1-PqZAS3hKe+g2x7V1gOHN95sUgyw=", + "dev": true + }, + "lodash._objecttypes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", + "integrity": "sha1-fAt/admKH3ZSn4kLDNsbTf7BHBE=", + "dev": true + }, + "lodash._reescape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", + "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=", + "dev": true + }, + "lodash._reevaluate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", + "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, + "lodash._reunescapedhtml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._reunescapedhtml/-/lodash._reunescapedhtml-2.4.1.tgz", + "integrity": "sha1-dHxPxAED6zu4oJduVx96JlnpO6c=", + "dev": true, + "requires": { + "lodash._htmlescapes": "2.4.1", + "lodash.keys": "2.4.1" + }, + "dependencies": { + "lodash.keys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", + "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "dev": true, + "requires": { + "lodash._isnative": "2.4.1", + "lodash._shimkeys": "2.4.1", + "lodash.isobject": "2.4.1" + } + } + } + }, + "lodash._root": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", + "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", + "dev": true + }, + "lodash._shimkeys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._shimkeys/-/lodash._shimkeys-2.4.1.tgz", + "integrity": "sha1-bpzJZm/wgfC1psl4uD4kLmlJ0gM=", + "dev": true, + "requires": { + "lodash._objecttypes": "2.4.1" + } + }, + "lodash.defaults": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-2.4.1.tgz", + "integrity": "sha1-p+iIXwXmiFEUS24SqPNngCa8TFQ=", + "dev": true, + "requires": { + "lodash._objecttypes": "2.4.1", + "lodash.keys": "2.4.1" + }, + "dependencies": { + "lodash.keys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", + "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "dev": true, + "requires": { + "lodash._isnative": "2.4.1", + "lodash._shimkeys": "2.4.1", + "lodash.isobject": "2.4.1" + } + } + } + }, + "lodash.escape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", + "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=", + "dev": true, + "requires": { + "lodash._root": "3.0.1" + } + }, + "lodash.find": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-3.2.1.tgz", + "integrity": "sha1-BG4xnzrOkSrGySRsf2g8XsB7Nq0=", + "dev": true, + "requires": { + "lodash._basecallback": "3.3.1", + "lodash._baseeach": "3.0.4", + "lodash._basefind": "3.0.0", + "lodash._basefindindex": "3.6.0", + "lodash.isarray": "3.0.4", + "lodash.keys": "3.1.2" + } + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, + "lodash.isobject": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", + "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", + "dev": true, + "requires": { + "lodash._objecttypes": "2.4.1" + } + }, + "lodash.istypedarray": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz", + "integrity": "sha1-yaR3SYYHUB2OhJTSg7h8OSgc72I=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "3.9.1", + "lodash.isarguments": "3.1.0", + "lodash.isarray": "3.0.4" + } + }, + "lodash.pairs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.pairs/-/lodash.pairs-3.0.1.tgz", + "integrity": "sha1-u+CNV4bu6qCaFckevw3LfSvjJqk=", + "dev": true, + "requires": { + "lodash.keys": "3.1.2" + } + }, + "lodash.restparam": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", + "dev": true + }, + "lodash.template": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", + "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", + "dev": true, + "requires": { + "lodash._basecopy": "3.0.1", + "lodash._basetostring": "3.0.1", + "lodash._basevalues": "3.0.0", + "lodash._isiterateecall": "3.0.9", + "lodash._reinterpolate": "3.0.0", + "lodash.escape": "3.2.0", + "lodash.keys": "3.1.2", + "lodash.restparam": "3.6.1", + "lodash.templatesettings": "3.1.1" + } + }, + "lodash.templatesettings": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", + "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", + "dev": true, + "requires": { + "lodash._reinterpolate": "3.0.0", + "lodash.escape": "3.2.0" + } + }, + "lodash.values": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.values/-/lodash.values-2.4.1.tgz", + "integrity": "sha1-q/UUQ2s8twUAFieXjLzzCxKA7qQ=", + "dev": true, + "requires": { + "lodash.keys": "2.4.1" + }, + "dependencies": { + "lodash.keys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", + "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "dev": true, + "requires": { + "lodash._isnative": "2.4.1", + "lodash._shimkeys": "2.4.1", + "lodash.isobject": "2.4.1" + } + } + } + }, + "lolex": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.0.tgz", + "integrity": "sha512-uJkH2e0BVfU5KOJUevbTOtpDduooSarH5PopO+LfM/vZf8Z9sJzODqKev804JYM2i++ktJfUmC1le4LwFQ1VMg==", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", + "dev": true, + "requires": { + "es5-ext": "0.10.43" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "3.0.0" + } + }, + "make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "1.0.1" + } + }, + "math-random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", + "dev": true + }, + "md5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", + "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "1.1.6" + } + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "dev": true, + "requires": { + "hash-base": "3.0.4", + "inherits": "2.0.3" + } + }, + "mdn-data": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz", + "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==", + "dev": true + }, + "memoizee": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.12.tgz", + "integrity": "sha512-sprBu6nwxBWBvBOh5v2jcsGqiGLlL2xr2dLub3vR8dnE8YB17omwtm/0NSHl8jjNbcsJd5GMWJAnTSVe/O0Wfg==", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.43", + "es6-weak-map": "2.0.2", + "event-emitter": "0.3.5", + "is-promise": "2.1.0", + "lru-queue": "0.1.0", + "next-tick": "1.0.0", + "timers-ext": "0.1.5" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + } + }, + "merge-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", + "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", + "dev": true, + "requires": { + "readable-stream": "2.0.6" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "1.33.0" + } + }, + "mimic-response": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.0.tgz", + "integrity": "sha1-3z02Uqc/3ta5sLJBRub9BSNTRY4=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, + "requires": { + "for-in": "1.0.2", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "mocha-junit-reporter": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-1.17.0.tgz", + "integrity": "sha1-LlFJ7UD8XS48px5C21qx/snG2Fw=", + "dev": true, + "requires": { + "debug": "2.6.9", + "md5": "2.2.1", + "mkdirp": "0.5.1", + "strip-ansi": "4.0.0", + "xml": "1.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "multimatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", + "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", + "dev": true, + "requires": { + "array-differ": "1.0.0", + "array-union": "1.0.2", + "arrify": "1.0.1", + "minimatch": "3.0.4" + } + }, + "multipipe": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", + "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", + "dev": true, + "requires": { + "duplexer2": "0.0.2" + } + }, + "named-js-regexp": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/named-js-regexp/-/named-js-regexp-1.3.3.tgz", + "integrity": "sha512-zIUAXzGQOp16VR0Ct89SDstU62hzAPBluNUrUrsdD7MNSRbm/vyqGhEnp+4hnsMjmX3C2wh1cbIEP0joKMFLxw==" + }, + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", + "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "fragment-cache": "0.2.1", + "is-odd": "2.0.0", + "is-windows": "1.0.2", + "kind-of": "6.0.2", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + }, + "natives": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.4.tgz", + "integrity": "sha512-Q29yeg9aFKwhLVdkTAejM/HvYG0Y1Am1+HUkFQGn5k2j8GS+v60TVmZh6nujpEAj/qql+wGUrlryO8bF+b1jEg==", + "dev": true + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + }, + "nice-try": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.4.tgz", + "integrity": "sha512-2NpiFHqC87y/zFke0fC0spBXL3bBsoh/p5H1EFhshxjCR5+0g2d6BiXbUFz9v1sAcxsk2htp2eQnNIci2dIYcA==", + "dev": true + }, + "nise": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.3.3.tgz", + "integrity": "sha512-v1J/FLUB9PfGqZLGDBhQqODkbLotP0WtLo9R4EJY2PPu5f5Xg4o0rA8FDlmrjFSv9vBBKcfnOSpfYYuu5RTHqg==", + "dev": true, + "requires": { + "@sinonjs/formatio": "2.0.0", + "just-extend": "1.1.27", + "lolex": "2.7.0", + "path-to-regexp": "1.7.0", + "text-encoding": "0.6.4" + } + }, + "node-has-native-dependencies": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/node-has-native-dependencies/-/node-has-native-dependencies-1.0.2.tgz", + "integrity": "sha1-MVLsl1O2ZB5NMi0YXdSTBkmto9o=", + "dev": true, + "requires": { + "fs-walk": "0.0.1" + } + }, + "node-stream-zip": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.6.0.tgz", + "integrity": "sha512-py/b/mLnyp/VvHCAl/Pqn6y+oLJrWpLYpLxJmGEAs1vxYDoAxgdbOzYgjpjEju/jrHzxUPurF+kT6KTfb+a4tA==" + }, + "node.extend": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-1.1.6.tgz", + "integrity": "sha1-p7iCyC1sk6SGOlUEvV3o7IYli5Y=", + "dev": true, + "requires": { + "is": "3.2.1" + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1.1.1" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "2.6.0", + "is-builtin-module": "1.0.0", + "semver": "5.5.0", + "validate-npm-package-license": "3.0.3" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "normalize-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", + "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", + "dev": true, + "requires": { + "prepend-http": "2.0.0", + "query-string": "5.1.1", + "sort-keys": "2.0.0" + }, + "dependencies": { + "sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", + "dev": true, + "requires": { + "is-plain-obj": "1.1.0" + } + } + } + }, + "now-and-later": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.0.tgz", + "integrity": "sha1-vGHLtFbXnLMiB85HygUTb/Ln1u4=", + "dev": true, + "requires": { + "once": "1.4.0" + } + }, + "npm-conf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", + "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", + "dev": true, + "requires": { + "config-chain": "1.1.11", + "pify": "3.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "0.1.1", + "define-property": "0.2.5", + "kind-of": "3.2.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "object-keys": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", + "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "function-bind": "1.1.1", + "has-symbols": "1.0.0", + "object-keys": "1.0.11" + } + }, + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "dev": true, + "requires": { + "array-each": "1.0.1", + "array-slice": "1.1.0", + "for-own": "1.0.0", + "isobject": "3.0.1" + } + }, + "object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "dev": true, + "requires": { + "for-own": "1.0.0", + "make-iterator": "1.0.1" + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + }, + "dependencies": { + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + } + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "opn": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", + "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", + "requires": { + "is-wsl": "1.1.0" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "0.0.10", + "wordwrap": "0.0.3" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + } + }, + "orchestrator": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/orchestrator/-/orchestrator-0.3.8.tgz", + "integrity": "sha1-FOfp4nZPcxX7rBhOUGx6pt+UrX4=", + "dev": true, + "requires": { + "end-of-stream": "0.1.5", + "sequencify": "0.0.7", + "stream-consume": "0.1.1" + } + }, + "ordered-read-streams": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.1.0.tgz", + "integrity": "sha1-/VZamvjrRHO6abbtijQ1LLVS8SY=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "p-cancelable": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", + "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", + "dev": true + }, + "p-event": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-1.3.0.tgz", + "integrity": "sha1-jmtPT2XHK8W2/ii3XtqHT5akoIU=", + "dev": true, + "requires": { + "p-timeout": "1.2.1" + }, + "dependencies": { + "p-timeout": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", + "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=", + "dev": true, + "requires": { + "p-finally": "1.0.0" + } + } + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-is-promise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", + "dev": true + }, + "p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "dev": true + }, + "p-timeout": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", + "dev": true, + "requires": { + "p-finally": "1.0.0" + } + }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "dev": true, + "requires": { + "is-absolute": "1.0.0", + "map-cache": "0.2.2", + "path-root": "0.1.1" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + } + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "dev": true, + "requires": { + "path-root-regex": "0.1.2" + } + }, + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", + "dev": true + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "dev": true, + "requires": { + "through": "2.3.8" + } + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pidusage": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-1.2.0.tgz", + "integrity": "sha512-OGo+iSOk44HRJ8q15AyG570UYxcm5u+R99DI8Khu8P3tKGkVu5EZX4ywHglWSTMNNXQ274oeGpYrvFEhDIFGPg==" + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "plugin-error": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", + "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", + "dev": true, + "requires": { + "ansi-cyan": "0.1.1", + "ansi-red": "0.1.1", + "arr-diff": "1.1.0", + "arr-union": "2.1.0", + "extend-shallow": "1.1.4" + }, + "dependencies": { + "arr-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", + "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0", + "array-slice": "0.2.3" + } + }, + "arr-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", + "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=", + "dev": true + }, + "array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", + "dev": true + }, + "extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=", + "dev": true, + "requires": { + "kind-of": "1.1.0" + } + }, + "kind-of": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", + "dev": true + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "postinstall-build": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.1.tgz", + "integrity": "sha1-uRepB5smF42aJK9aXNjLSpkdEbk=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "1.4.1", + "once": "1.4.0" + }, + "dependencies": { + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "1.4.0" + } + } + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "3.6.0", + "inherits": "2.0.3", + "pump": "2.0.1" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "dev": true, + "requires": { + "decode-uri-component": "0.2.0", + "object-assign": "4.1.1", + "strict-uri-encode": "1.1.0" + } + }, + "querystringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz", + "integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw==", + "dev": true + }, + "queue": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/queue/-/queue-3.1.0.tgz", + "integrity": "sha1-bEnQHwCeIlZ4h4nyv/rGuLmZBYU=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "randomatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", + "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", + "dev": true, + "requires": { + "is-number": "4.0.0", + "kind-of": "6.0.2", + "math-random": "1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.0.6", + "set-immediate-shim": "1.0.1" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "1.7.1" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "reflect-metadata": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz", + "integrity": "sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==" + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "3.0.2", + "safe-regex": "1.1.0" + } + }, + "relative": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/relative/-/relative-3.0.2.tgz", + "integrity": "sha1-Dc2OxUpdNaPBXhBFA9ZTdbWlNn8=", + "dev": true, + "requires": { + "isobject": "2.1.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "remap-istanbul": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/remap-istanbul/-/remap-istanbul-0.10.1.tgz", + "integrity": "sha512-gsNQXs5kJLhErICSyYhzVZ++C8LBW8dgwr874Y2QvzAUS75zBlD/juZgXs39nbYJ09fZDlX2AVLVJAY2jbFJoQ==", + "dev": true, + "requires": { + "amdefine": "1.0.1", + "istanbul": "0.4.5", + "minimatch": "3.0.4", + "plugin-error": "0.1.2", + "source-map": "0.6.1", + "through2": "2.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "through2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.1.tgz", + "integrity": "sha1-OE51MU1J8y3hLuu4E2uOtrXVnak=", + "dev": true, + "requires": { + "readable-stream": "2.0.6", + "xtend": "4.0.1" + } + } + } + }, + "remove-bom-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", + "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", + "dev": true, + "requires": { + "is-buffer": "1.1.6", + "is-utf8": "0.2.1" + } + }, + "remove-bom-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", + "integrity": "sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=", + "dev": true, + "requires": { + "remove-bom-buffer": "3.0.0", + "safe-buffer": "5.1.2", + "through2": "2.0.3" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "replace-ext": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", + "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", + "dev": true + }, + "request": { + "version": "2.85.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", + "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.7.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.2", + "safe-buffer": "5.1.2", + "stringstream": "0.0.6", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.2.1" + } + }, + "request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", + "requires": { + "throttleit": "1.0.0" + } + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", + "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "2.0.2", + "global-modules": "1.0.0" + } + }, + "resolve-options": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", + "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=", + "dev": true, + "requires": { + "value-or-function": "3.0.0" + } + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dev": true, + "requires": { + "lowercase-keys": "1.0.1" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "retyped-diff-match-patch-tsd-ambient": { + "version": "1.0.0-1", + "resolved": "https://registry.npmjs.org/retyped-diff-match-patch-tsd-ambient/-/retyped-diff-match-patch-tsd-ambient-1.0.0-1.tgz", + "integrity": "sha1-Jkgr9JFcftn4MAu1y+xI/U/1vGI=", + "dev": true + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "rxjs": { + "version": "5.5.9", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.9.tgz", + "integrity": "sha512-DHG9AHmCmgaFWgjBcXp6NxFDmh3MvIA62GqTWmLnTzr/3oZ6h5hLD8NA+9j+GF0jEwklNIpI4KuuyLG8UWMEvQ==", + "requires": { + "symbol-observable": "1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "0.1.15" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "samsam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", + "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", + "dev": true + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "seek-bzip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", + "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", + "dev": true, + "requires": { + "commander": "2.8.1" + }, + "dependencies": { + "commander": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "dev": true, + "requires": { + "graceful-readlink": "1.0.1" + } + } + } + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + }, + "sequencify": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/sequencify/-/sequencify-0.0.7.tgz", + "integrity": "sha1-kM/xnQLgcCf9dn9erT57ldHnOAw=", + "dev": true + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "split-string": "3.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shortid": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.8.tgz", + "integrity": "sha1-AzsRfWoul1gE9vCWnb59PQs1UTE=", + "dev": true + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "sinon": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.5.0.tgz", + "integrity": "sha512-trdx+mB0VBBgoYucy6a9L7/jfQOmvGeaKZT4OOJ+lPAtI8623xyGr8wLiE4eojzBS8G9yXbhx42GHUOVLr4X2w==", + "dev": true, + "requires": { + "@sinonjs/formatio": "2.0.0", + "diff": "3.5.0", + "lodash.get": "4.4.2", + "lolex": "2.7.0", + "nise": "1.3.3", + "supports-color": "5.4.0", + "type-detect": "4.0.8" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "0.11.2", + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "map-cache": "0.2.2", + "source-map": "0.5.7", + "source-map-resolve": "0.5.2", + "use": "3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "1.0.0", + "isobject": "3.0.1", + "snapdragon-util": "3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "sntp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "requires": { + "hoek": "4.2.1" + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "1.1.0" + } + }, + "sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", + "dev": true, + "requires": { + "sort-keys": "1.1.2" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "2.1.1", + "decode-uri-component": "0.2.0", + "resolve-url": "0.2.1", + "source-map-url": "0.4.0", + "urix": "0.1.0" + } + }, + "source-map-support": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz", + "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", + "dev": true, + "requires": { + "buffer-from": "1.0.0", + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "sparkles": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", + "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", + "dev": true + }, + "spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "dev": true, + "requires": { + "spdx-expression-parse": "3.0.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "2.1.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", + "dev": true + }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "dev": true, + "requires": { + "through": "2.3.8" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "3.0.2" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", + "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + } + }, + "stat-mode": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.2.2.tgz", + "integrity": "sha1-5sgLYjEj19gM8TLOU480YokHJQI=", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "0.2.5", + "object-copy": "0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "dev": true, + "requires": { + "duplexer": "0.1.1" + } + }, + "stream-consume": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.1.tgz", + "integrity": "sha512-tNa3hzgkjEP7XbCkbRXe1jpg+ievoa0O4SCFlMOYEscGSS4JJsckGL8swUyAa/ApGU3Ae4t6Honor4HhL+tRyg==", + "dev": true + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "streamfilter": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/streamfilter/-/streamfilter-1.0.7.tgz", + "integrity": "sha512-Gk6KZM+yNA1JpW0KzlZIhjo3EaBJDkYfXtYSbOwNIQ7Zd6006E6+sCFlW1NDvFG/vnXhKmw6TJJgiEQg/8lXfQ==", + "dev": true, + "requires": { + "readable-stream": "2.0.6" + } + }, + "streamifier": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/streamifier/-/streamifier-0.1.1.tgz", + "integrity": "sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8=", + "dev": true + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "stringstream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", + "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-1.0.0.tgz", + "integrity": "sha1-hbiGLzhEtabV7IRnqTWYFzo295Q=", + "dev": true, + "requires": { + "first-chunk-stream": "1.0.0", + "is-utf8": "0.2.1" + } + }, + "strip-bom-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz", + "integrity": "sha1-+H217yYT9paKpUWr/h7HKLaoKco=", + "dev": true, + "requires": { + "first-chunk-stream": "2.0.0", + "strip-bom": "2.0.0" + }, + "dependencies": { + "first-chunk-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz", + "integrity": "sha1-G97NuOCDwGZLkZRVgVd6Q6nzHXA=", + "dev": true, + "requires": { + "readable-stream": "2.0.6" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + } + } + }, + "strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=", + "dev": true + }, + "strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "requires": { + "is-natural-number": "4.0.1" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "4.0.1" + } + }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5" + } + }, + "sudo-prompt": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-8.2.0.tgz", + "integrity": "sha512-n5Nv2lIZaWfVBg10EWC8yaJCB6xV7sEsuaISAVFIS9F4fTRjy/O35A82lkweKuSqQItDlKOGQpTHK9/udQhRRw==" + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "svgo": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz", + "integrity": "sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=", + "dev": true, + "requires": { + "coa": "1.0.4", + "colors": "1.1.2", + "csso": "2.3.2", + "js-yaml": "3.7.0", + "mkdirp": "0.5.1", + "sax": "1.2.4", + "whet.extend": "0.9.9" + }, + "dependencies": { + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "csso": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz", + "integrity": "sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=", + "dev": true, + "requires": { + "clap": "1.2.3", + "source-map": "0.5.7" + } + }, + "js-yaml": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", + "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=", + "dev": true, + "requires": { + "argparse": "1.0.10", + "esprima": "2.7.3" + } + } + } + }, + "symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "dev": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-stream": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz", + "integrity": "sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==", + "dev": true, + "requires": { + "bl": "1.2.2", + "buffer-alloc": "1.2.0", + "end-of-stream": "1.4.1", + "fs-constants": "1.0.0", + "readable-stream": "2.3.6", + "to-buffer": "1.1.1", + "xtend": "4.0.1" + }, + "dependencies": { + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "1.4.0" + } + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + } + } + }, + "text-encoding": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", + "dev": true + }, + "throttleit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", + "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=" + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "dev": true, + "requires": { + "readable-stream": "2.3.6", + "xtend": "4.0.1" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + } + } + }, + "through2-filter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", + "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", + "dev": true, + "requires": { + "through2": "2.0.3", + "xtend": "4.0.1" + } + }, + "tildify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-1.2.0.tgz", + "integrity": "sha1-3OwD9V3Km3qj5bBPIYF+tW5jWIo=", + "dev": true, + "requires": { + "os-homedir": "1.0.2" + } + }, + "time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", + "dev": true + }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, + "timers-ext": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.5.tgz", + "integrity": "sha512-tsEStd7kmACHENhsUPaxb8Jf8/+GZZxyNFQbZD07HQOyooOa6At1rQqjffgvg7n+dxscQa9cjjMdWhJtsP2sxg==", + "dev": true, + "requires": { + "es5-ext": "0.10.43", + "next-tick": "1.0.0" + } + }, + "tmp": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", + "integrity": "sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=", + "requires": { + "os-tmpdir": "1.0.2" + } + }, + "to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", + "dev": true, + "requires": { + "is-absolute": "1.0.0", + "is-negated-glob": "1.0.0" + } + }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "regex-not": "1.0.2", + "safe-regex": "1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "repeat-string": "1.6.1" + } + }, + "to-through": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", + "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=", + "dev": true, + "requires": { + "through2": "2.0.3" + } + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "requires": { + "punycode": "1.4.1" + } + }, + "tree-kill": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.0.tgz", + "integrity": "sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg==" + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5" + } + }, + "tslib": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.1.tgz", + "integrity": "sha512-avfPS28HmGLLc2o4elcc2EIq2FcH++Yo5YxpBZi9Yw93BCTGFthI4HPE4Rpep6vSYQaK8e69PelM44tPj+RaQg==", + "dev": true + }, + "tslint": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.10.0.tgz", + "integrity": "sha1-EeJrzLiK+gLdDZlWyuPUVAtfVMM=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "builtin-modules": "1.1.1", + "chalk": "2.4.1", + "commander": "2.15.1", + "diff": "3.5.0", + "glob": "7.1.2", + "js-yaml": "3.11.0", + "minimatch": "3.0.4", + "resolve": "1.7.1", + "semver": "5.5.0", + "tslib": "1.9.1", + "tsutils": "2.27.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "tslint-eslint-rules": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/tslint-eslint-rules/-/tslint-eslint-rules-5.3.1.tgz", + "integrity": "sha512-qq2H/AU/FlFbQJKXuxhtIk+ni/nQu9jHHhsFKa6hnA0/n3zl1/RWRc3TVFlL8HfWFMzkST350VeTrFpy1u4OUg==", + "dev": true, + "requires": { + "doctrine": "0.7.2", + "tslib": "1.9.0", + "tsutils": "2.8.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==", + "dev": true + }, + "tsutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.8.0.tgz", + "integrity": "sha1-AWAXNymzvxOGKN0UoVN+AIUdgUo=", + "dev": true, + "requires": { + "tslib": "1.9.0" + } + } + } + }, + "tslint-microsoft-contrib": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/tslint-microsoft-contrib/-/tslint-microsoft-contrib-5.0.3.tgz", + "integrity": "sha512-5AnfTGlfpUzpRHLmoojPBKFTTmbjnwgdaTHMdllausa4GBPya5u36i9ddrTX4PhetGZvd4JUYIpAmgHqVnsctg==", + "dev": true, + "requires": { + "tsutils": "2.27.1" + } + }, + "tsutils": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.27.1.tgz", + "integrity": "sha512-AE/7uzp32MmaHvNNFES85hhUDHFdFZp6OAiZcd6y4ZKKIg6orJTm8keYWBhIhrJQH3a4LzNKat7ZPXZt5aTf6w==", + "dev": true, + "requires": { + "tslib": "1.9.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "typemoq": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz", + "integrity": "sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw==", + "dev": true, + "requires": { + "circular-json": "0.3.3", + "lodash": "4.17.5", + "postinstall-build": "5.0.1" + } + }, + "typescript": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.1.tgz", + "integrity": "sha512-h6pM2f/GDchCFlldnriOhs1QHuwbnmj6/v7499eMHqPeW4V2G0elua2eIc2nu8v2NdHV0Gm+tzX83Hr6nUFjQA==", + "dev": true + }, + "typescript-char": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/typescript-char/-/typescript-char-0.0.0.tgz", + "integrity": "sha1-VY/tpzfHZaYQtzfu+7F3Xum8jas=" + }, + "typescript-formatter": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/typescript-formatter/-/typescript-formatter-7.2.0.tgz", + "integrity": "sha512-A16UqkHtkQOF340cf21LJXchcftyBTPqNOAmP1J8Plu2m3Q8o+2fAYwgFjLXMKP6ooSPjDoOS6z8j9q+1nEnXg==", + "dev": true, + "requires": { + "commandpost": "1.3.0", + "editorconfig": "0.15.0" + }, + "dependencies": { + "editorconfig": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.0.tgz", + "integrity": "sha512-j7JBoj/bpNzvoTQylfRZSc85MlLNKWQiq5y6gwKhmqD2h1eZ+tH4AXbkhEJD468gjDna/XMx2YtSkCxBRX9OGg==", + "dev": true, + "requires": { + "@types/commander": "2.12.2", + "@types/semver": "5.5.0", + "commander": "2.15.1", + "lru-cache": "4.1.3", + "semver": "5.5.0", + "sigmund": "1.0.1" + } + }, + "lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + } + } + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "optional": true, + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "uint64be": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uint64be/-/uint64be-1.0.1.tgz", + "integrity": "sha1-H3FUIC8qG4rzU4cd2mUb80zpPpU=" + }, + "unbzip2-stream": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.2.5.tgz", + "integrity": "sha512-izD3jxT8xkzwtXRUZjtmRwKnZoeECrfZ8ra/ketwOcusbZEp4mjULMnJOCfTDZBgGQAAY1AJ/IgxcwkavcX9Og==", + "dev": true, + "requires": { + "buffer": "3.6.0", + "through": "2.3.8" + } + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "dev": true + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", + "dev": true + }, + "unicode": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/unicode/-/unicode-10.0.0.tgz", + "integrity": "sha1-5dUcHbk7bHGguHngsMSvfm/faI4=" + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "3.1.0", + "get-value": "2.0.6", + "is-extendable": "0.1.1", + "set-value": "0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" + } + } + } + }, + "unique-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-1.0.0.tgz", + "integrity": "sha1-1ZpKdUJ0R9mqbJHnAmP40mpLEEs=", + "dev": true + }, + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "0.3.1", + "isobject": "3.0.1" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "0.1.4", + "isobject": "2.1.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "untildify": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.2.tgz", + "integrity": "sha1-fx8wIFWz/qDz6B3HjrNnZstl4/E=" + }, + "upath": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", + "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", + "dev": true + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url-parse": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.0.tgz", + "integrity": "sha512-ERuGxDiQ6Xw/agN4tuoCRbmwRuZP0cJ1lJxJubXr5Q/5cDa78+Dc4wfvtxzhzhkm5VvmW6Mf8EVj9SPGN4l8Lg==", + "dev": true, + "requires": { + "querystringify": "2.0.0", + "requires-port": "1.0.0" + } + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "dev": true, + "requires": { + "prepend-http": "2.0.0" + } + }, + "url-to-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", + "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", + "dev": true + }, + "urlgrey": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.4.tgz", + "integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=", + "dev": true + }, + "use": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", + "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "user-home": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", + "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + }, + "v8flags": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", + "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", + "dev": true, + "requires": { + "user-home": "1.1.1" + } + }, + "vali-date": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", + "integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", + "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "dev": true, + "requires": { + "spdx-correct": "3.0.0", + "spdx-expression-parse": "3.0.0" + } + }, + "validator": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz", + "integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA==", + "dev": true + }, + "value-or-function": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", + "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, + "vinyl": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", + "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", + "dev": true, + "requires": { + "clone": "1.0.4", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } + }, + "vinyl-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-file/-/vinyl-file-2.0.0.tgz", + "integrity": "sha1-p+v1/779obfRjRQPyweyI++2dRo=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0", + "strip-bom-stream": "2.0.0", + "vinyl": "1.2.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true, + "requires": { + "clone": "1.0.4", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } + } + } + }, + "vinyl-fs": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-0.3.14.tgz", + "integrity": "sha1-mmhRzhysHBzqX+hsCTHWIMLPqeY=", + "dev": true, + "requires": { + "defaults": "1.0.3", + "glob-stream": "3.1.18", + "glob-watcher": "0.0.6", + "graceful-fs": "3.0.11", + "mkdirp": "0.5.1", + "strip-bom": "1.0.0", + "through2": "0.6.5", + "vinyl": "0.4.6" + }, + "dependencies": { + "clone": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", + "dev": true + }, + "graceful-fs": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", + "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", + "dev": true, + "requires": { + "natives": "1.1.4" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "xtend": "4.0.1" + } + }, + "vinyl": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "dev": true, + "requires": { + "clone": "0.2.0", + "clone-stats": "0.0.1" + } + } + } + }, + "vinyl-source-stream": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vinyl-source-stream/-/vinyl-source-stream-1.1.2.tgz", + "integrity": "sha1-YrU6E1YQqJbpjKlr7jqH8Aio54A=", + "dev": true, + "requires": { + "through2": "2.0.3", + "vinyl": "0.4.6" + }, + "dependencies": { + "clone": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", + "dev": true + }, + "vinyl": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "dev": true, + "requires": { + "clone": "0.2.0", + "clone-stats": "0.0.1" + } + } + } + }, + "vinyl-sourcemap": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", + "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=", + "dev": true, + "requires": { + "append-buffer": "1.0.2", + "convert-source-map": "1.5.1", + "graceful-fs": "4.1.11", + "normalize-path": "2.1.1", + "now-and-later": "2.0.0", + "remove-bom-buffer": "3.0.0", + "vinyl": "2.1.0" + }, + "dependencies": { + "clone": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", + "dev": true + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + }, + "vinyl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.1.0.tgz", + "integrity": "sha1-Ah+cLPlR1rk5lDyJ617lrdT9kkw=", + "dev": true, + "requires": { + "clone": "2.1.1", + "clone-buffer": "1.0.0", + "clone-stats": "1.0.0", + "cloneable-readable": "1.1.2", + "remove-trailing-separator": "1.1.0", + "replace-ext": "1.0.0" + } + } + } + }, + "vscode": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/vscode/-/vscode-1.1.18.tgz", + "integrity": "sha512-SyDw4qFwZ+WthZX7RWp71PNiWLF7VhpM65j2oryY/6jtSORd8qH6J8vclwWZJ6Jvu0EH7JamO2RWNfBfsMR9Zw==", + "dev": true, + "requires": { + "glob": "7.1.2", + "gulp-chmod": "2.0.0", + "gulp-filter": "5.1.0", + "gulp-gunzip": "1.0.0", + "gulp-remote-src-vscode": "0.5.0", + "gulp-symdest": "1.1.0", + "gulp-untar": "0.0.7", + "gulp-vinyl-zip": "2.1.0", + "mocha": "4.1.0", + "request": "2.85.0", + "semver": "5.5.0", + "source-map-support": "0.5.6", + "url-parse": "1.4.0", + "vinyl-source-stream": "1.1.2" + }, + "dependencies": { + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "diff": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", + "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", + "dev": true + }, + "growl": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", + "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", + "dev": true + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "mocha": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", + "integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.3.1", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + } + }, + "supports-color": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "vscode-debugadapter": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.28.0.tgz", + "integrity": "sha512-GCR1326LFtfYjl7SDN1wmU2pBJ98HgUCnbWoU3s3bz0GhUWYN1xSYGg7MfuwxY6WwZk2cuqzANhy/oaKADMXaw==", + "requires": { + "vscode-debugprotocol": "1.28.0", + "vscode-uri": "1.0.1" + } + }, + "vscode-debugadapter-testsupport": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/vscode-debugadapter-testsupport/-/vscode-debugadapter-testsupport-1.29.0.tgz", + "integrity": "sha512-4P0h3gfe7MNw9FXx0k/TpKBJMA4s880Gu+puxuOHOY3txYpIGC1I2jQdPlWt0XPWK2Qcz7q/biQ221cfavqifw==", + "dev": true, + "requires": { + "vscode-debugprotocol": "1.29.0" + }, + "dependencies": { + "vscode-debugprotocol": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.29.0.tgz", + "integrity": "sha512-jrbSayWof7jyXo7VRhIcTcsjWeiPloi6vzbrucVarKvuSrZUV7Bc+ggQRSG1lzNiMmBG5AHIe/Npf6G2q4SBiw==", + "dev": true + } + } + }, + "vscode-debugprotocol": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.28.0.tgz", + "integrity": "sha512-QM4J8A13jBY9I7OPWXN0ZO1cqydnD4co2j/O81jIj6em8VkmJT4VyJQkq4HmwJe3af+u9+7IYCIEDrowgvKxTA==" + }, + "vscode-extension-telemetry": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/vscode-extension-telemetry/-/vscode-extension-telemetry-0.0.15.tgz", + "integrity": "sha512-Yf6dL9r2x2GISI1xh22XsAaydSTQG/4aBitu8sGBwGr42n2TyOsIXGtXSDgqQBNZgYD6+P1EHqrrzetn9ekWTQ==", + "requires": { + "applicationinsights": "1.0.1" + } + }, + "vscode-jsonrpc": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-3.6.2.tgz", + "integrity": "sha512-T24Jb5V48e4VgYliUXMnZ379ItbrXgOimweKaJshD84z+8q7ZOZjJan0MeDe+Ugb+uqERDVV8SBmemaGMSMugA==" + }, + "vscode-languageclient": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-4.3.0.tgz", + "integrity": "sha512-vDpsmYfYpfuyDKZ46pCJEFBOCNHepRBSmlBGA0fczEbYghYm059BiFo3SmT4MK1r8NvYrFEem4k5TYNW3wommg==", + "requires": { + "vscode-languageserver-protocol": "3.9.0" + } + }, + "vscode-languageserver": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-4.3.0.tgz", + "integrity": "sha512-4dTpnyTB6Q0HmMhxaG60rrpQthbTBlMtFX5cwJpPxcPzLZIFDWB3msR6TxGCzWpdYF11REIJihWByobpGkljdQ==", + "requires": { + "vscode-languageserver-protocol": "3.9.0", + "vscode-uri": "1.0.5" + }, + "dependencies": { + "vscode-uri": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.5.tgz", + "integrity": "sha1-O4majvccN/MFTXm9vdoxx7828g0=" + } + } + }, + "vscode-languageserver-protocol": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.9.0.tgz", + "integrity": "sha512-i1sG5iU88Mocc7egTeh6dAow/yRWpPK5PLJaxsWsKiA+dspq1Yzr/R1bNLPc+6P/ab010lXhzdUHQY0CuIUyDw==", + "requires": { + "vscode-jsonrpc": "3.6.2", + "vscode-languageserver-types": "3.9.0" + } + }, + "vscode-languageserver-types": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.9.0.tgz", + "integrity": "sha512-Qzh3VsU3t0zhKtYl1revyax+4gGHl2ejNzYXeiZYQMF3i0vX4dtPohxGDFoZYfGFQI738aXYbSUQmhLeBckDlQ==" + }, + "vscode-uri": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.1.tgz", + "integrity": "sha1-Eahr7+rDxKo+wIYjZRo8gabQu8g=" + }, + "whet.extend": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", + "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true, + "optional": true + }, + "winreg": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.4.tgz", + "integrity": "sha1-ugZWKbepJRMOFXeRCM9UCZDpjRs=" + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", + "dev": true + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": "1.2.4", + "xmlbuilder": "9.0.7" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true + } + } + }, + "yauzl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.9.1.tgz", + "integrity": "sha1-qBmB6nCleUYTOIPwKcWCGok1mn8=", + "dev": true, + "requires": { + "buffer-crc32": "0.2.13", + "fd-slicer": "1.0.1" + } + }, + "yazl": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.4.3.tgz", + "integrity": "sha1-7CblzIfVYBud+EMtvdPNLlFzoHE=", + "dev": true, + "requires": { + "buffer-crc32": "0.2.13" + } + }, + "zone.js": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.7.6.tgz", + "integrity": "sha1-+7w50+AmHQmG8boGMG6zrrDSIAk=" + } + } +} diff --git a/package.json b/package.json index a22c95dac590..c502de7d7d68 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Linting, Debugging (multi-threaded, remote), Intellisense, code formatting, refactoring, unit tests, snippets, and more.", - "version": "2018.3.0-alpha", + "version": "2018.7.1", "publisher": "ms-python", "author": { "name": "Microsoft Corporation" @@ -16,17 +16,18 @@ "bugs": { "url": "https://github.com/Microsoft/vscode-python/issues" }, + "qna": "https://stackoverflow.com/questions/tagged/visual-studio-code+python", "badges": [ + { + "url": "https://vscode-python.visualstudio.com/VSCode-Python/_apis/build/status/VSCode-Python-Rolling-CI?branchName=master", + "href": "https://vscode-python.visualstudio.com/VSCode-Python/VSCode-Python%20Team/_build/index?context=allDefinitions&path=&definitionId=9", + "description": "Continuous integration (VSTS)" + }, { "url": "https://travis-ci.org/Microsoft/vscode-python.svg?branch=master", "href": "https://travis-ci.org/Microsoft/vscode-python", "description": "Continuous integration (Travis)" }, - { - "url": "https://ci.appveyor.com/api/projects/status/s0pt8d79gqw222j7?svg=true", - "href": "https://ci.appveyor.com/project/DonJayamanne/vscode-python-v3vd6/branch/master", - "description": "Continuous integration (AppVeyor)" - }, { "url": "https://codecov.io/gh/Microsoft/vscode-python/branch/master/graph/badge.svg", "href": "https://codecov.io/gh/Microsoft/vscode-python", @@ -39,7 +40,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.18.0" + "vscode": "^1.25.0" }, "recommendations": [ "donjayamanne.jupyter" @@ -51,7 +52,7 @@ "multi-root ready" ], "categories": [ - "Languages", + "Programming Languages", "Debuggers", "Linters", "Snippets", @@ -60,6 +61,8 @@ ], "activationEvents": [ "onLanguage:python", + "onDebugResolve:python", + "onDebugResolve:pythonExperimental", "onCommand:python.execInTerminal", "onCommand:python.sortImports", "onCommand:python.runtests", @@ -81,7 +84,8 @@ "onCommand:python.goToPythonObject", "onCommand:python.setLinter", "onCommand:python.enableLinting", - "onCommand:python.createTerminal" + "onCommand:python.createTerminal", + "onCommand:python.discoverTests" ], "main": "./out/client/extension", "contributes": { @@ -91,6 +95,13 @@ "path": "./snippets/python.json" } ], + "keybindings": [ + { + "command": "python.execSelectionInTerminal", + "key": "shift+enter", + "when": "editorFocus && editorLangId == python && !findInputFocussed && !replaceInputFocussed" + } + ], "commands": [ { "command": "python.sortImports", @@ -177,6 +188,11 @@ "title": "%python.command.python.runFailedTests.title%", "category": "Python" }, + { + "command": "python.discoverTests", + "title": "%python.command.python.discoverTests.title%", + "category": "Python" + }, { "command": "python.execSelectionInTerminal", "title": "%python.command.python.execSelectionInTerminal.title%", @@ -231,7 +247,7 @@ { "command": "python.execSelectionInTerminal", "group": "Python", - "when": "editorHasSelection && editorLangId == python" + "when": "editorFocus && editorLangId == python" }, { "command": "python.execSelectionInDjangoShell", @@ -360,7 +376,7 @@ ], "debugOptions": [ "RedirectOutput", - "DjangoDebugging" + "Django" ] } }, @@ -376,7 +392,7 @@ "module": "flask", "cwd": "^\"\\${workspaceFolder}\"", "env": { - "FLASK_APP": "^\"\\${workspaceFolder}/app.py\"" + "FLASK_APP": "app.py" }, "args": [ "run", @@ -496,7 +512,7 @@ "externalTerminal" ], "description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.", - "default": "none" + "default": "integratedTerminal" }, "cwd": { "type": "string", @@ -512,7 +528,7 @@ "RedirectOutput", "DebugStdLib", "BreakOnSystemExitZero", - "DjangoDebugging", + "Django", "Sudo", "IgnoreDjangoTemplateWarnings", "Pyramid" @@ -660,7 +676,7 @@ ], "debugOptions": [ "RedirectOutput", - "DjangoDebugging" + "Django" ] }, { @@ -669,7 +685,7 @@ "request": "launch", "module": "flask", "env": { - "FLASK_APP": "${workspaceFolder}/app.py" + "FLASK_APP": "app.py" }, "args": [ "run", @@ -737,7 +753,8 @@ "enableBreakpointsFor": { "languageIds": [ "python", - "html" + "html", + "jinja" ] }, "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", @@ -771,21 +788,121 @@ "description": "%python.snippet.launch.django.description%", "body": { "name": "Django", - "type": "python", + "type": "pythonExperimental", "request": "launch", - "pythonPath": "^\"\\${config:python.pythonPath}\"", "program": "^\"\\${workspaceFolder}/manage.py\"", - "cwd": "^\"\\${workspaceFolder}\"", - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", "args": [ "runserver", "--noreload", "--nothreading" ], - "env": {}, - "envFile": "^\"\\${workspaceFolder}/.env\"", - "debugOptions": [] + "django": true + } + }, + { + "label": "Python Experimental: Flask", + "description": "%python.snippet.launch.flask.description%", + "body": { + "name": "Flask", + "type": "pythonExperimental", + "request": "launch", + "module": "flask", + "env": { + "FLASK_APP": "app.py" + }, + "args": [ + "run", + "--no-debugger", + "--no-reload" + ], + "jinja": true + } + }, + { + "label": "Python Experimental: Gevent", + "description": "%python.snippet.launch.gevent.description%", + "body": { + "name": "Gevent", + "type": "pythonExperimental", + "request": "launch", + "program": "^\"\\${file}\"", + "gevent": true + } + }, + { + "label": "Python Experimental: PySpark", + "description": "%python.snippet.launch.pyspark.description%", + "body": { + "name": "PySpark", + "type": "pythonExperimental", + "request": "launch", + "osx": { + "pythonPath": "^\"\\${env:SPARK_HOME}/bin/spark-submit\"" + }, + "windows": { + "pythonPath": "^\"\\${env:SPARK_HOME}/bin/spark-submit.cmd\"" + }, + "linux": { + "pythonPath": "^\"\\${env:SPARK_HOME}/bin/spark-submit\"" + }, + "program": "^\"\\${file}\"" + } + }, + { + "label": "Python Experimental: Watson", + "description": "%python.snippet.launch.watson.description%", + "body": { + "name": "Watson", + "type": "pythonExperimental", + "request": "launch", + "program": "^\"\\${workspaceFolder}/console.py\"", + "args": [ + "dev", + "runserver", + "--noreload=True" + ], + "jinja": true + } + }, + { + "label": "Python Experimental: Scrapy", + "description": "%python.snippet.launch.scrapy.description%", + "body": { + "name": "Scrapy", + "type": "pythonExperimental", + "request": "launch", + "module": "scrapy", + "args": [ + "crawl", + "specs", + "-o", + "bikes.json" + ] + } + }, + { + "label": "Python Experimental: Pyramid", + "description": "%python.snippet.launch.pyramid.description%", + "body": { + "name": "Pyramid", + "type": "pythonExperimental", + "request": "launch", + "args": [ + "^\"\\${workspaceFolder}/development.ini\"" + ], + "pyramid": true, + "jinja": true + } + }, + { + "label": "Python Experimental: Attach", + "description": "%python.snippet.launch.attach.description%", + "body": { + "name": "Attach (Remote Debug)", + "type": "pythonExperimental", + "request": "attach", + "port": 5678, + "host": "localhost" } } ], @@ -822,6 +939,7 @@ }, "console": { "enum": [ + "none", "integratedTerminal", "externalTerminal" ], @@ -833,17 +951,6 @@ "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).", "default": "${workspaceFolder}" }, - "debugOptions": { - "type": "array", - "description": "Advanced options, view read me for further details.", - "items": { - "type": "string", - "enum": [ - "Sudo" - ] - }, - "default": [] - }, "env": { "type": "object", "description": "Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable.", @@ -868,6 +975,116 @@ "type": "boolean", "description": "Enable logging of debugger events to a log file.", "default": false + }, + "redirectOutput": { + "type": "boolean", + "description": "Redirect output.", + "default": true + }, + "debugStdLib": { + "type": "boolean", + "description": "Debug standard library code.", + "default": false + }, + "gevent": { + "type": "boolean", + "description": "Enable debugging of gevent monkey-patched code.", + "default": false + }, + "django": { + "type": "boolean", + "description": "Django debugging.", + "default": false + }, + "jinja": { + "enum": [ + true, + false, + null + ], + "description": "Jinja template debugging (e.g. Flask).", + "default": null + }, + "sudo": { + "type": "boolean", + "description": "Running debug program under elevated permissions (on Unix).", + "default": false + }, + "pyramid": { + "type": "boolean", + "description": "Whether debugging Pyramid applications", + "default": false + } + } + }, + "attach": { + "required": [ + "port" + ], + "properties": { + "port": { + "type": "number", + "description": "Debug port to attach", + "default": 0 + }, + "host": { + "type": "string", + "description": "IP Address of the of remote server (default is localhost or use 127.0.0.1).", + "default": "localhost" + }, + "pathMappings": { + "type": "array", + "label": "Path mappings.", + "items": { + "type": "object", + "label": "Path mapping", + "required": [ + "localRoot", + "remoteRoot" + ], + "properties": { + "localRoot": { + "type": "string", + "label": "Local source root.", + "default": "${workspaceFolder}" + }, + "remoteRoot": { + "type": "string", + "label": "Remote source root.", + "default": "" + } + } + }, + "default": [] + }, + "logToFile": { + "type": "boolean", + "description": "Enable logging of debugger events to a log file.", + "default": false + }, + "redirectOutput": { + "type": "boolean", + "description": "Redirect output.", + "default": true + }, + "debugStdLib": { + "type": "boolean", + "description": "Debug standard library code.", + "default": false + }, + "django": { + "type": "boolean", + "description": "Django debugging.", + "default": false + }, + "jinja": { + "enum": [ + true, + false, + null + ], + "description": "Jinja template debugging (e.g. Flask).", + "default": null } } } @@ -880,6 +1097,13 @@ "program": "${file}", "console": "integratedTerminal" }, + { + "name": "Python Experimental: Attach", + "type": "pythonExperimental", + "request": "attach", + "port": 5678, + "host": "localhost" + }, { "name": "Python Experimental: Django", "type": "pythonExperimental", @@ -890,7 +1114,23 @@ "runserver", "--noreload", "--nothreading" - ] + ], + "django": true + }, + { + "name": "Python Experimental: Flask", + "type": "pythonExperimental", + "request": "launch", + "module": "flask", + "env": { + "FLASK_APP": "app.py" + }, + "args": [ + "run", + "--no-debugger", + "--no-reload" + ], + "jinja": true }, { "name": "Python Experimental: Current File (External Terminal)", @@ -906,56 +1146,55 @@ "type": "object", "title": "Python Configuration", "properties": { - "python.pythonPath": { - "type": "string", - "default": "python", - "description": "Path to Python, you can use a custom version of Python by modifying this setting to include the full path.", + "python.autoComplete.addBrackets": { + "type": "boolean", + "default": false, + "description": "Automatically add brackets for functions.", "scope": "resource" }, - "python.venvPath": { - "type": "string", - "default": "", - "description": "Path to folder with a list of Virtual Environments (e.g. ~/.pyenv, ~/Envs, ~/.virtualenvs).", + "python.autoComplete.extraPaths": { + "type": "array", + "default": [], + "description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.", "scope": "resource" }, - "python.venvFolders": { + "python.autoComplete.preloadModules": { "type": "array", - "default": [ - "envs", - ".pyenv", - ".direnv" - ], - "description": "Folders to look into for virtual environments.", - "scope": "resource", "items": { "type": "string" - } + }, + "default": [], + "description": "Comma delimited list of modules preloaded to speed up Auto Complete (e.g. add Numpy, Pandas, etc, items slow to load when autocompleting).", + "scope": "resource" }, - "python.envFile": { - "type": "string", - "description": "Absolute path to a file containing environment variable definitions.", - "default": "${workspaceFolder}/.env", + "python.autoComplete.showAdvancedMembers": { + "type": "boolean", + "default": true, + "description": "Controls appearance of methods with double underscores in the completion list.", "scope": "resource" }, - "python.jediPath": { - "type": "string", - "default": "", - "description": "Path to directory containing the Jedi library (this path will contain the 'Jedi' sub directory).", + "python.autoComplete.typeshedPaths": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "Specifies paths to local typeshed repository clone(s) for the Python language server.", "scope": "resource" }, - "python.jediMemoryLimit": { - "type": "number", - "default": "0", - "description": "Memory limit for the Jedi completion engine in megabytes. Zero (default) means 1024 MB", + "python.disableInstallationCheck": { + "type": "boolean", + "default": false, + "description": "Whether to check if Python is installed (also warn when using the macOS-installed Python).", "scope": "resource" }, - "python.sortImports.path": { + "python.envFile": { "type": "string", - "description": "Path to isort script, default using inner version", - "default": "", + "description": "Absolute path to a file containing environment variable definitions.", + "default": "${workspaceFolder}/.env", "scope": "resource" }, - "python.sortImports.args": { + "python.formatting.autopep8Args": { "type": "array", "description": "Arguments passed in. Each argument is a separate item in the array.", "default": [], @@ -964,102 +1203,148 @@ }, "scope": "resource" }, - "python.disablePromptForFeatures": { - "type": "array", - "default": [], - "description": "Do not display a prompt to install these features", - "items": { - "type": "string", - "default": "pylint", - "description": "Feature", - "enum": [ - "flake8", - "mypy", - "pep8", - "pylama", - "prospector", - "pydocstyle", - "pylint" - ] + "python.formatting.autopep8Path": { + "type": "string", + "default": "autopep8", + "description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.formatting.provider": { + "type": "string", + "default": "autopep8", + "description": "Provider for formatting. Possible options include 'autopep8', 'black', and 'yapf'.", + "enum": [ + "autopep8", + "black", + "yapf", + "none" + ], + "scope": "resource" + }, + "python.formatting.blackArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" }, "scope": "resource" }, - "python.disableInstallationCheck": { - "type": "boolean", - "default": false, - "description": "Whether to check if Python is installed.", + "python.formatting.blackPath": { + "type": "string", + "default": "black", + "description": "Path to Black, you can use a custom version of Black by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.formatting.yapfArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.formatting.yapfPath": { + "type": "string", + "default": "yapf", + "description": "Path to yapf, you can use a custom version of yapf by modifying this setting to include the full path.", "scope": "resource" }, "python.globalModuleInstallation": { "type": "boolean", "default": false, - "description": "Whether to install Python modules globally.", + "description": "Whether to install Python modules globally when not using an environment.", "scope": "resource" }, - "python.linting.enabled": { + "python.jediEnabled": { "type": "boolean", "default": true, - "description": "Whether to lint Python files.", + "description": "Enables Jedi as IntelliSense engine instead of Microsoft Python Analysis Engine.", "scope": "resource" }, - "python.linting.prospectorEnabled": { - "type": "boolean", - "default": false, - "description": "Whether to lint Python files using prospector.", + "python.jediMemoryLimit": { + "type": "number", + "default": 0, + "description": "Memory limit for the Jedi completion engine in megabytes. Zero (default) means 1024 MB. -1 means unlimited (disable memory limit check)", "scope": "resource" }, - "python.linting.pylintEnabled": { - "type": "boolean", - "default": true, - "description": "Whether to lint Python files using pylint.", + "python.jediPath": { + "type": "string", + "default": "", + "description": "Path to directory containing the Jedi library (this path will contain the 'Jedi' sub directory).", "scope": "resource" }, - "python.linting.pep8Enabled": { + "python.analysis.openFilesOnly": { "type": "boolean", - "default": false, - "description": "Whether to lint Python files using pep8", + "default": true, + "description": "Only show errors and warnings for open files rather than for the entire workspace.", "scope": "resource" }, - "python.linting.flake8Enabled": { - "type": "boolean", - "default": false, - "description": "Whether to lint Python files using flake8", + "python.analysis.typeshedPaths": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "Paths to look for typeshed modules.", "scope": "resource" }, - "python.linting.pydocstyleEnabled": { - "type": "boolean", - "default": false, - "description": "Whether to lint Python files using pydocstyle", + "python.analysis.errors": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "List of diagnostics messages to be shown as errors.", "scope": "resource" }, - "python.linting.mypyEnabled": { - "type": "boolean", - "default": false, - "description": "Whether to lint Python files using mypy.", + "python.analysis.warnings": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "List of diagnostics messages to be shown as warnings.", "scope": "resource" }, - "python.linting.lintOnSave": { - "type": "boolean", - "default": true, - "description": "Whether to lint Python files when saved.", + "python.analysis.information": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "List of diagnostics messages to be shown as information.", "scope": "resource" }, - "python.linting.maxNumberOfProblems": { - "type": "number", - "default": 100, - "description": "Controls the maximum number of problems produced by the server.", + "python.analysis.disabled": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "List of suppressed diagnostic messages.", "scope": "resource" }, - "python.linting.pylintUseMinimalCheckers": { + "python.linting.enabled": { "type": "boolean", "default": true, - "description": "Whether to run Pylint with minimal set of rules.", + "description": "Whether to lint Python files.", "scope": "resource" }, - "python.linting.pylintCategorySeverity.convention": { + "python.linting.flake8Args": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.flake8CategorySeverity.E": { "type": "string", - "default": "Information", - "description": "Severity of Pylint message type 'Convention/C'.", + "default": "Error", + "description": "Severity of Flake8 message type 'E'.", "enum": [ "Hint", "Error", @@ -1068,10 +1353,10 @@ ], "scope": "resource" }, - "python.linting.pylintCategorySeverity.refactor": { + "python.linting.flake8CategorySeverity.F": { "type": "string", - "default": "Hint", - "description": "Severity of Pylint message type 'Refactor/R'.", + "default": "Error", + "description": "Severity of Flake8 message type 'F'.", "enum": [ "Hint", "Error", @@ -1080,10 +1365,10 @@ ], "scope": "resource" }, - "python.linting.pylintCategorySeverity.warning": { + "python.linting.flake8CategorySeverity.W": { "type": "string", "default": "Warning", - "description": "Severity of Pylint message type 'Warning/W'.", + "description": "Severity of Flake8 message type 'W'.", "enum": [ "Hint", "Error", @@ -1092,58 +1377,58 @@ ], "scope": "resource" }, - "python.linting.pylintCategorySeverity.error": { - "type": "string", - "default": "Error", - "description": "Severity of Pylint message type 'Error/E'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ], + "python.linting.flake8Enabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using flake8", "scope": "resource" }, - "python.linting.pylintCategorySeverity.fatal": { + "python.linting.flake8Path": { "type": "string", - "default": "Error", - "description": "Severity of Pylint message type 'Fatal/F'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ], + "default": "flake8", + "description": "Path to flake8, you can use a custom version of flake8 by modifying this setting to include the full path.", "scope": "resource" }, - "python.linting.pep8CategorySeverity.W": { - "type": "string", - "default": "Warning", - "description": "Severity of Pep8 message type 'W'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" + "python.linting.ignorePatterns": { + "type": "array", + "description": "Patterns used to exclude files or folders from being linted.", + "default": [ + ".vscode/*.py", + "**/site-packages/**/*.py" ], + "items": { + "type": "string" + }, "scope": "resource" }, - "python.linting.pep8CategorySeverity.E": { - "type": "string", - "default": "Error", - "description": "Severity of Pep8 message type 'E'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" + "python.linting.lintOnSave": { + "type": "boolean", + "default": true, + "description": "Whether to lint Python files when saved.", + "scope": "resource" + }, + "python.linting.maxNumberOfProblems": { + "type": "number", + "default": 100, + "description": "Controls the maximum number of problems produced by the server.", + "scope": "resource" + }, + "python.linting.mypyArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [ + "--ignore-missing-imports", + "--follow-imports=silent" ], + "items": { + "type": "string" + }, "scope": "resource" }, - "python.linting.flake8CategorySeverity.F": { + "python.linting.mypyCategorySeverity.error": { "type": "string", "default": "Error", - "description": "Severity of Flake8 message type 'F'.", + "description": "Severity of Mypy message type 'Error'.", "enum": [ "Hint", "Error", @@ -1152,10 +1437,10 @@ ], "scope": "resource" }, - "python.linting.flake8CategorySeverity.E": { + "python.linting.mypyCategorySeverity.note": { "type": "string", - "default": "Error", - "description": "Severity of Flake8 message type 'E'.", + "default": "Information", + "description": "Severity of Mypy message type 'Note'.", "enum": [ "Hint", "Error", @@ -1164,22 +1449,31 @@ ], "scope": "resource" }, - "python.linting.flake8CategorySeverity.W": { + "python.linting.mypyEnabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using mypy.", + "scope": "resource" + }, + "python.linting.mypyPath": { "type": "string", - "default": "Warning", - "description": "Severity of Flake8 message type 'W'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ], + "default": "mypy", + "description": "Path to mypy, you can use a custom version of mypy by modifying this setting to include the full path.", "scope": "resource" }, - "python.linting.mypyCategorySeverity.error": { + "python.linting.pep8Args": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.pep8CategorySeverity.E": { "type": "string", "default": "Error", - "description": "Severity of Mypy message type 'Error'.", + "description": "Severity of Pep8 message type 'E'.", "enum": [ "Hint", "Error", @@ -1188,10 +1482,10 @@ ], "scope": "resource" }, - "python.linting.mypyCategorySeverity.note": { + "python.linting.pep8CategorySeverity.W": { "type": "string", - "default": "Information", - "description": "Severity of Mypy message type 'Note'.", + "default": "Warning", + "description": "Severity of Pep8 message type 'W'.", "enum": [ "Hint", "Error", @@ -1200,16 +1494,10 @@ ], "scope": "resource" }, - "python.linting.prospectorPath": { - "type": "string", - "default": "prospector", - "description": "Path to Prospector, you can use a custom version of prospector by modifying this setting to include the full path.", - "scope": "resource" - }, - "python.linting.pylintPath": { - "type": "string", - "default": "pylint", - "description": "Path to Pylint, you can use a custom version of pylint by modifying this setting to include the full path.", + "python.linting.pep8Enabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using pep8", "scope": "resource" }, "python.linting.pep8Path": { @@ -1218,24 +1506,6 @@ "description": "Path to pep8, you can use a custom version of pep8 by modifying this setting to include the full path.", "scope": "resource" }, - "python.linting.flake8Path": { - "type": "string", - "default": "flake8", - "description": "Path to flake8, you can use a custom version of flake8 by modifying this setting to include the full path.", - "scope": "resource" - }, - "python.linting.pydocstylePath": { - "type": "string", - "default": "pydocstyle", - "description": "Path to pydocstyle, you can use a custom version of pydocstyle by modifying this setting to include the full path.", - "scope": "resource" - }, - "python.linting.mypyPath": { - "type": "string", - "default": "mypy", - "description": "Path to mypy, you can use a custom version of mypy by modifying this setting to include the full path.", - "scope": "resource" - }, "python.linting.prospectorArgs": { "type": "array", "description": "Arguments passed in. Each argument is a separate item in the array.", @@ -1245,31 +1515,16 @@ }, "scope": "resource" }, - "python.linting.pylintArgs": { - "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [], - "items": { - "type": "string" - }, - "scope": "resource" - }, - "python.linting.pep8Args": { - "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [], - "items": { - "type": "string" - }, + "python.linting.prospectorEnabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using prospector.", "scope": "resource" }, - "python.linting.flake8Args": { - "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [], - "items": { - "type": "string" - }, + "python.linting.prospectorPath": { + "type": "string", + "default": "prospector", + "description": "Path to Prospector, you can use a custom version of prospector by modifying this setting to include the full path.", "scope": "resource" }, "python.linting.pydocstyleArgs": { @@ -1281,42 +1536,19 @@ }, "scope": "resource" }, - "python.linting.mypyArgs": { - "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [ - "--ignore-missing-imports", - "--follow-imports=silent" - ], - "items": { - "type": "string" - }, - "scope": "resource" - }, - "python.formatting.provider": { - "type": "string", - "default": "autopep8", - "description": "Provider for formatting. Possible options include 'autopep8' and 'yapf'.", - "enum": [ - "autopep8", - "yapf", - "none" - ], - "scope": "resource" - }, - "python.formatting.autopep8Path": { - "type": "string", - "default": "autopep8", - "description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path.", + "python.linting.pydocstyleEnabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using pydocstyle", "scope": "resource" }, - "python.formatting.yapfPath": { + "python.linting.pydocstylePath": { "type": "string", - "default": "yapf", - "description": "Path to yapf, you can use a custom version of yapf by modifying this setting to include the full path.", + "default": "pydocstyle", + "description": "Path to pydocstyle, you can use a custom version of pydocstyle by modifying this setting to include the full path.", "scope": "resource" }, - "python.formatting.autopep8Args": { + "python.linting.pylamaArgs": { "type": "array", "description": "Arguments passed in. Each argument is a separate item in the array.", "default": [], @@ -1325,7 +1557,19 @@ }, "scope": "resource" }, - "python.formatting.yapfArgs": { + "python.linting.pylamaEnabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using pylama.", + "scope": "resource" + }, + "python.linting.pylamaPath": { + "type": "string", + "default": "pylama", + "description": "Path to pylama, you can use a custom version of pylama by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.pylintArgs": { "type": "array", "description": "Arguments passed in. Each argument is a separate item in the array.", "default": [], @@ -1334,84 +1578,121 @@ }, "scope": "resource" }, - "python.autoComplete.preloadModules": { - "type": "array", - "items": { - "type": "string" - }, - "default": [], - "description": "Comma delimited list of modules preloaded to speed up Auto Complete (e.g. add Numpy, Pandas, etc, items slow to load when autocompleting).", + "python.linting.pylintCategorySeverity.convention": { + "type": "string", + "default": "Information", + "description": "Severity of Pylint message type 'Convention/C'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], "scope": "resource" }, - "python.autoComplete.extraPaths": { - "type": "array", - "default": [], - "description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.", + "python.linting.pylintCategorySeverity.error": { + "type": "string", + "default": "Error", + "description": "Severity of Pylint message type 'Error/E'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], "scope": "resource" }, - "python.autoComplete.addBrackets": { - "type": "boolean", - "default": false, - "description": "Automatically add brackets for functions.", + "python.linting.pylintCategorySeverity.fatal": { + "type": "string", + "default": "Error", + "description": "Severity of Pylint message type 'Fatal/F'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], "scope": "resource" }, - "python.workspaceSymbols.tagFilePath": { + "python.linting.pylintCategorySeverity.refactor": { "type": "string", - "default": "${workspaceFolder}/.vscode/tags", - "description": "Fully qualified path to tag file (exuberant ctag file), used to provide workspace symbols.", + "default": "Hint", + "description": "Severity of Pylint message type 'Refactor/R'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], "scope": "resource" }, - "python.workspaceSymbols.enabled": { - "type": "boolean", - "default": true, - "description": "Set to 'false' to disable Workspace Symbol provider using ctags.", + "python.linting.pylintCategorySeverity.warning": { + "type": "string", + "default": "Warning", + "description": "Severity of Pylint message type 'Warning/W'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], "scope": "resource" }, - "python.workspaceSymbols.rebuildOnStart": { + "python.linting.pylintEnabled": { "type": "boolean", "default": true, - "description": "Whether to re-build the tags file on start (defaults to true).", + "description": "Whether to lint Python files using pylint.", "scope": "resource" }, - "python.workspaceSymbols.rebuildOnFileSave": { + "python.linting.pylintPath": { + "type": "string", + "default": "pylint", + "description": "Path to Pylint, you can use a custom version of pylint by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.pylintUseMinimalCheckers": { "type": "boolean", "default": true, - "description": "Whether to re-build the tags file on when changes made to python files are saved.", + "description": "Whether to run Pylint with minimal set of rules.", "scope": "resource" }, - "python.workspaceSymbols.ctagsPath": { + "python.pythonPath": { "type": "string", - "default": "ctags", - "description": "Fully qualilified path to the ctags executable (else leave as ctags, assuming it is in current path).", + "default": "python", + "description": "Path to Python, you can use a custom version of Python by modifying this setting to include the full path.", "scope": "resource" }, - "python.workspaceSymbols.exclusionPatterns": { + "python.sortImports.args": { "type": "array", - "default": [ - "**/site-packages/**" - ], + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], "items": { "type": "string" }, - "description": "Pattern used to exclude files and folders from ctags See http://ctags.sourceforge.net/ctags.html.", "scope": "resource" }, - "python.unitTest.promptToConfigure": { + "python.sortImports.path": { + "type": "string", + "description": "Path to isort script, default using inner version", + "default": "", + "scope": "resource" + }, + "python.terminal.activateEnvironment": { "type": "boolean", "default": true, - "description": "Where to prompt to configure a test framework if potential tests directories are discovered.", + "description": "Activate Python Environment in Terminal created using the Extension.", "scope": "resource" }, - "python.unitTest.debugPort": { - "type": "number", - "default": 3000, - "description": "Port number used for debugging of unittests.", + "python.terminal.executeInFileDir": { + "type": "boolean", + "default": false, + "description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder.", "scope": "resource" }, - "python.unitTest.debugHost": { - "type": "number", - "default": "localhost", - "description": "IP Address of the of the local unit test server (default is localhost or use 127.0.0.1).", + "python.terminal.launchArgs": { + "type": "array", + "default": [], + "description": "Python launch arguments to use when executing a file in the terminal.", "scope": "resource" }, "python.unitTest.cwd": { @@ -1420,6 +1701,21 @@ "description": "Optional working directory for unit tests.", "scope": "resource" }, + "python.unitTest.debugPort": { + "type": "number", + "default": 3000, + "description": "Port number used for debugging of unittests.", + "scope": "resource" + }, + "python.unitTest.nosetestArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, "python.unitTest.nosetestsEnabled": { "type": "boolean", "default": false, @@ -1432,25 +1728,10 @@ "description": "Path to nosetests, you can use a custom version of nosetests by modifying this setting to include the full path.", "scope": "resource" }, - "python.unitTest.pyTestEnabled": { + "python.unitTest.promptToConfigure": { "type": "boolean", - "default": false, - "description": "Whether to enable or disable unit testing using pytest.", - "scope": "resource" - }, - "python.unitTest.pyTestPath": { - "type": "string", - "default": "pytest", - "description": "Path to pytest (pytest), you can use a custom version of pytest by modifying this setting to include the full path.", - "scope": "resource" - }, - "python.unitTest.nosetestArgs": { - "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [], - "items": { - "type": "string" - }, + "default": true, + "description": "Where to prompt to configure a test framework if potential tests directories are discovered.", "scope": "resource" }, "python.unitTest.pyTestArgs": { @@ -1462,10 +1743,16 @@ }, "scope": "resource" }, - "python.unitTest.unittestEnabled": { + "python.unitTest.pyTestEnabled": { "type": "boolean", "default": false, - "description": "Whether to enable or disable unit testing using unittest.", + "description": "Whether to enable or disable unit testing using pytest.", + "scope": "resource" + }, + "python.unitTest.pyTestPath": { + "type": "string", + "default": "pytest", + "description": "Path to pytest (pytest), you can use a custom version of pytest by modifying this setting to include the full path.", "scope": "resource" }, "python.unitTest.unittestArgs": { @@ -1483,55 +1770,82 @@ }, "scope": "resource" }, - "python.linting.ignorePatterns": { + "python.unitTest.unittestEnabled": { + "type": "boolean", + "default": false, + "description": "Whether to enable or disable unit testing using unittest.", + "scope": "resource" + }, + "python.unitTest.useExperimentalDebugger": { + "type": "boolean", + "default": false, + "description": "Use the experimental debugger when debugging unit tests.", + "scope": "resource" + }, + "python.unitTest.autoTestDiscoverOnSaveEnabled": { + "type": "boolean", + "default": true, + "description": "Whether to enable or disable auto run test discovery when saving a unit test file.", + "scope": "resource" + }, + "python.venvFolders": { "type": "array", - "description": "Patterns used to exclude files or folders from being linted.", "default": [ - ".vscode/*.py", - "**/site-packages/**/*.py" + "envs", + ".pyenv", + ".direnv" ], + "description": "Folders in your home directory to look into for virtual environments.", + "scope": "resource", "items": { "type": "string" - }, - "scope": "resource" + } }, - "python.linting.pylamaEnabled": { - "type": "boolean", - "default": false, - "description": "Whether to lint Python files using pylama.", + "python.venvPath": { + "type": "string", + "default": "", + "description": "Path to folder with a list of Virtual Environments (e.g. ~/.pyenv, ~/Envs, ~/.virtualenvs).", "scope": "resource" }, - "python.linting.pylamaPath": { + "python.workspaceSymbols.ctagsPath": { "type": "string", - "default": "pylama", - "description": "Path to pylama, you can use a custom version of pylama by modifying this setting to include the full path.", + "default": "ctags", + "description": "Fully qualilified path to the ctags executable (else leave as ctags, assuming it is in current path).", "scope": "resource" }, - "python.linting.pylamaArgs": { + "python.workspaceSymbols.enabled": { + "type": "boolean", + "default": true, + "description": "Set to 'false' to disable Workspace Symbol provider using ctags.", + "scope": "resource" + }, + "python.workspaceSymbols.exclusionPatterns": { "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [], + "default": [ + "**/site-packages/**" + ], "items": { "type": "string" }, + "description": "Pattern used to exclude files and folders from ctags See http://ctags.sourceforge.net/ctags.html.", "scope": "resource" }, - "python.terminal.executeInFileDir": { + "python.workspaceSymbols.rebuildOnFileSave": { "type": "boolean", - "default": false, - "description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder.", + "default": true, + "description": "Whether to re-build the tags file on when changes made to python files are saved.", "scope": "resource" }, - "python.terminal.activateEnvironment": { + "python.workspaceSymbols.rebuildOnStart": { "type": "boolean", "default": true, - "description": "Activate Python Environment in Terminal created using the Extension.", + "description": "Whether to re-build the tags file on start (defaults to true).", "scope": "resource" }, - "python.terminal.launchArgs": { - "type": "array", - "default": [], - "description": "Python launch arguments to use when executing a file in the terminal.", + "python.workspaceSymbols.tagFilePath": { + "type": "string", + "default": "${workspaceFolder}/.vscode/tags", + "description": "Fully qualified path to tag file (exuberant ctag file), used to provide workspace symbols.", "scope": "resource" } } @@ -1544,11 +1858,17 @@ "requirements.txt" ], "filenames": [ - "requirements.txt" + "requirements.txt", + "constraints.txt", + "requirements.in" ], "filenamePatterns": [ "*-requirements.txt", - "requirements-*.txt" + "requirements-*.txt", + "constraints-*.txt", + "*-constraints.txt", + "*-requirements.in", + "requirements-*.in" ], "configuration": "./languages/pip-requirements.json" }, @@ -1557,6 +1877,28 @@ "filenames": [ ".condarc" ] + }, + { + "id": "toml", + "filenames": [ + "Pipfile" + ] + }, + { + "id": "json", + "filenames": [ + "Pipfile.lock" + ] + }, + { + "id": "jinja", + "extensions": [ + ".jinja2", + ".j2" + ], + "aliases": [ + "Jinja" + ] } ], "grammars": [ @@ -1596,112 +1938,137 @@ ] }, "scripts": { - "vscode:prepublish": "tsc -p ./", + "vscode:prepublish": "gulp checkNativeDependencies && tsc -p ./", "compile": "tsc -watch -p ./", "postinstall": "node ./node_modules/vscode/bin/install", "test": "node ./out/test/standardTest.js && node ./out/test/multiRootTest.js", + "test:unittests": "node ./out/test/unittests.js", "testDebugger": "node ./out/test/debuggerTest.js", "testSingleWorkspace": "node ./out/test/standardTest.js", "testMultiWorkspace": "node ./out/test/multiRootTest.js", + "testAnalysisEngine": "node ./out/test/analysisEngineTest.js", + "testPerformance": "node ./out/test/performanceTest.js", "precommit": "node gulpfile.js", "lint-staged": "node gulpfile.js", "lint": "tslint src/**/*.ts -t verbose", "clean": "gulp clean", - "cover:enable": "gulp cover:enable" + "clean:ptvsd": "gulp clean:ptvsd", + "cover:enable": "gulp cover:enable", + "debugger-coverage": "gulp debugger-coverage", + "cover:inlinesource": "gulp inlinesource" }, "dependencies": { - "arch": "^2.1.0", - "diff-match-patch": "^1.0.0", - "fs-extra": "^4.0.2", - "fuzzy": "^0.1.3", - "get-port": "^3.2.0", - "iconv-lite": "^0.4.19", - "inversify": "^4.5.2", - "line-by-line": "^0.1.5", - "lodash": "^4.17.4", - "md5": "^2.2.1", - "minimatch": "^3.0.3", - "named-js-regexp": "^1.3.1", - "opn": "^5.1.0", - "pidusage": "^1.2.0", - "reflect-metadata": "^0.1.12", - "rxjs": "^5.5.2", - "semver": "^5.4.1", - "sudo-prompt": "^8.0.0", + "arch": "2.1.0", + "diff-match-patch": "1.0.0", + "dotenv": "5.0.1", + "fs-extra": "4.0.3", + "fuzzy": "0.1.3", + "get-port": "3.2.0", + "glob": "7.1.2", + "iconv-lite": "0.4.21", + "inversify": "4.11.1", + "line-by-line": "0.1.6", + "lodash": "4.17.5", + "md5": "2.2.1", + "minimatch": "3.0.4", + "named-js-regexp": "1.3.3", + "node-stream-zip": "1.6.0", + "opn": "5.3.0", + "pidusage": "1.2.0", + "reflect-metadata": "0.1.12", + "request": "2.85.0", + "request-progress": "3.0.0", + "rxjs": "5.5.9", + "semver": "5.5.0", + "sudo-prompt": "8.2.0", "tmp": "0.0.29", - "tree-kill": "^1.1.0", - "typescript-char": "^0.0.0", - "uint64be": "^1.0.1", - "unicode": "^10.0.0", - "untildify": "^3.0.2", - "vscode-debugadapter": "^1.0.1", - "vscode-debugprotocol": "^1.0.1", - "vscode-extension-telemetry": "^0.0.14", - "vscode-languageclient": "^3.1.0", - "vscode-languageserver": "^3.1.0", - "winreg": "^1.2.4", - "xml2js": "^0.4.17" + "tree-kill": "1.2.0", + "typescript-char": "0.0.0", + "uint64be": "1.0.1", + "unicode": "10.0.0", + "untildify": "3.0.2", + "vscode-debugadapter": "1.28.0", + "vscode-debugprotocol": "1.28.0", + "vscode-extension-telemetry": "0.0.15", + "vscode-languageclient": "4.3.0", + "vscode-languageserver": "4.3.0", + "vscode-languageserver-protocol": "3.9.0", + "winreg": "1.2.4", + "xml2js": "0.4.19" }, "devDependencies": { - "@types/chai": "4.0.6", + "@types/chai": "^4.1.2", + "@types/chai-arrays": "^1.0.2", "@types/chai-as-promised": "^7.1.0", "@types/del": "^3.0.0", + "@types/dotenv": "^4.0.3", + "@types/download": "^6.2.2", "@types/event-stream": "^3.3.33", - "@types/fs-extra": "^4.0.2", + "@types/fs-extra": "^5.0.1", "@types/get-port": "^3.2.0", - "@types/glob": "^5.0.34", - "@types/iconv-lite": "0.0.1", + "@types/glob": "^5.0.35", + "@types/iconv-lite": "^0.0.1", "@types/istanbul": "^0.4.29", - "@types/lodash": "^4.14.74", + "@types/lodash": "^4.14.104", "@types/md5": "^2.1.32", - "@types/mocha": "^2.2.43", - "@types/node": "^6.0.40", - "@types/semver": "^5.4.0", - "@types/shortid": "0.0.29", - "@types/sinon": "^2.3.2", + "@types/mocha": "^2.2.48", + "@types/node": "9.4.7", + "@types/request": "^2.47.0", + "@types/semver": "^5.5.0", + "@types/shortid": "^0.0.29", + "@types/sinon": "^4.3.0", + "@types/tmp": "0.0.33", "@types/untildify": "^3.0.0", - "@types/uuid": "^3.3.27", + "@types/uuid": "^3.4.3", "@types/winreg": "^1.2.30", - "@types/xml2js": "^0.4.0", + "@types/xml2js": "^0.4.2", "JSONStream": "^1.3.2", - "azure-storage": "^2.7.0", + "azure-storage": "^2.8.1", "chai": "^4.1.2", + "chai-arrays": "^2.0.0", "chai-as-promised": "^7.1.1", "codecov": "^3.0.0", - "colors": "^1.1.2", + "colors": "^1.2.1", + "cross-spawn": "^6.0.5", "debounce": "^1.1.0", - "decache": "^4.3.0", + "decache": "^4.4.0", "del": "^3.0.0", + "download": "^7.0.0", "event-stream": "^3.3.4", + "flat": "^4.0.0", "gulp": "^3.9.1", "gulp-debounced-watch": "^1.0.4", - "gulp-filter": "^5.0.1", + "gulp-filter": "^5.1.0", "gulp-gitmodified": "^1.1.1", - "gulp-json-editor": "^2.2.1", + "gulp-inline-source": "^3.2.0", + "gulp-json-editor": "^2.2.2", "gulp-sourcemaps": "^2.6.4", - "gulp-typescript": "^3.2.2", - "gulp-watch": "^4.3.11", + "gulp-typescript": "^4.0.1", + "gulp-watch": "^5.0.0", "husky": "^0.14.3", "is-running": "^2.1.0", "istanbul": "^0.4.5", - "mocha": "^2.3.3", + "mocha": "^5.0.4", + "mocha-junit-reporter": "^1.17.0", + "node-has-native-dependencies": "^1.0.2", "relative": "^3.0.2", - "remap-istanbul": "^0.9.5", + "remap-istanbul": "^0.10.1", "retyped-diff-match-patch-tsd-ambient": "^1.0.0-0", "shortid": "^2.2.8", - "sinon": "^2.3.6", - "tslint": "^5.7.0", - "tslint-eslint-rules": "^4.1.1", - "tslint-microsoft-contrib": "^5.0.1", + "sinon": "^4.4.5", + "tslint": "^5.9.1", + "tslint-eslint-rules": "^5.1.0", + "tslint-microsoft-contrib": "^5.0.3", "typemoq": "^2.1.0", - "typescript": "^2.6.2", - "typescript-formatter": "^6.0.0", + "typescript": "^2.9.1", + "typescript-formatter": "^7.1.0", + "uuid": "^3.2.1", "vscode": "^1.1.5", - "vscode-debugadapter-testsupport": "^1.25.0" + "vscode-debugadapter-testsupport": "^1.27.0" }, "__metadata": { "id": "f1f59ae4-9318-4f3c-a9b5-81b2eaa5f8a5", "publisherDisplayName": "Microsoft", "publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8" } -} \ No newline at end of file +} diff --git a/package.nls.es.json b/package.nls.es.json new file mode 100644 index 000000000000..3d2df644941f --- /dev/null +++ b/package.nls.es.json @@ -0,0 +1,52 @@ +{ + "python.command.python.sortImports.title": "Ordenar importaciones", + "python.command.python.startREPL.title": "Nuevo REPL", + "python.command.python.createTerminal.title": "Nueva terminal", + "python.command.python.buildWorkspaceSymbols.title": "Compilar símbolos del area de trabajo", + "python.command.python.runtests.title": "Ejecutar todas las pruebas unitarias", + "python.command.python.debugtests.title": "Depurar todas la pruebas unitarias", + "python.command.python.execInTerminal.title": "Ejecutar archivo Python en la terminal", + "python.command.python.setInterpreter.title": "Seleccionar intérprete", + "python.command.python.updateSparkLibrary.title": "Actualizar las librerías PySpark del area de trabajo", + "python.command.python.refactorExtractVariable.title": "Extraer variable", + "python.command.python.refactorExtractMethod.title": "Extraer método", + "python.command.python.viewTestOutput.title": "Mostrar resultados de la prueba unitaria", + "python.command.python.selectAndRunTestMethod.title": "Método de ejecución de pruebas unitarias ...", + "python.command.python.selectAndDebugTestMethod.title": "Método de depuración de pruebas unitarias ...", + "python.command.python.selectAndRunTestFile.title": "Ejecutar archivo de prueba unitaria ...", + "python.command.python.runCurrentTestFile.title": "Ejecutar archivo de prueba unitaria actual", + "python.command.python.runFailedTests.title": "Ejecutar pruebas unitarias fallidas", + "python.command.python.discoverTests.title": "Encontrar pruebas unitarias", + "python.command.python.execSelectionInTerminal.title": "Ejecutar linea/selección en la terminal", + "python.command.python.execSelectionInDjangoShell.title": "Ejecutar linea/selección en el interprete de Django", + "python.command.python.goToPythonObject.title": "Ir al objeto de Python", + "python.command.python.setLinter.title": "Selecionar Linter", + "python.command.python.enableLinting.title": "Habilitar Linting", + "python.command.python.runLinting.title": "Ejecutar Linting", + "python.snippet.launch.standard.label": "Python: Archivo actual", + "python.snippet.launch.standard.description": "Depurar una aplicación Python con salida estandar", + "python.snippet.launch.pyspark.label": "Python: PySpark", + "python.snippet.launch.pyspark.description": "Depurar una aplicación de PySpark", + "python.snippet.launch.module.label": "Python: Módulo", + "python.snippet.launch.module.description": "Depurar un módulo de Python", + "python.snippet.launch.terminal.label": "Python: Terminal (integrada)", + "python.snippet.launch.terminal.description": "Depurar una aplicación Python usando la terminal integrada", + "python.snippet.launch.externalTerminal.label": "Python: Terminal (externa)", + "python.snippet.launch.externalTerminal.description": "Depurar una aplicación Python usando una terminal externa", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.django.description": "Depurar una aplicación de Django", + "python.snippet.launch.flask.label": "Python: Flask (Versión 0.11.x o posterior)", + "python.snippet.launch.flask.description": "Depurar una aplicación de Flask", + "python.snippet.launch.flaskOld.label": "Python: Flask (Versión 0.10.x o anterior)", + "python.snippet.launch.flaskOld.description": "Depurar una aplicación de Flask de estilo antiguo", + "python.snippet.launch.gevent.label": "Python: Gevent", + "python.snippet.launch.gevent.description": "Depurar una aplicación de Gevent", + "python.snippet.launch.pyramid.label": "Python: Pyramid", + "python.snippet.launch.pyramid.description": "Depurar una aplicación de Pyramid", + "python.snippet.launch.watson.label": "Python: Watson", + "python.snippet.launch.watson.description": "Depurar una aplicación de Watson", + "python.snippet.launch.attach.label": "Python: Adjuntar", + "python.snippet.launch.attach.description": "Depuración remota usando depurador adjunto", + "python.snippet.launch.scrapy.label": "Python: Scrapy", + "python.snippet.launch.scrapy.description": "Scrapy usando la terminal integrada" +} diff --git a/package.nls.fr.json b/package.nls.fr.json new file mode 100644 index 000000000000..c764405ca1da --- /dev/null +++ b/package.nls.fr.json @@ -0,0 +1,49 @@ +{ + "python.command.python.sortImports.title": "Trier les imports", + "python.command.python.startREPL.title": "Démarrer la console interactive", + "python.command.python.createTerminal.title": "Créer un terminal", + "python.command.python.buildWorkspaceSymbols.title": "Construire les symboles de l'espace de travail", + "python.command.python.runtests.title": "Exécuter tous les tests unitaires", + "python.command.python.debugtests.title": "Déboguer tous les tests unitaires", + "python.command.python.execInTerminal.title": "Exécuter le script Python dans un terminal", + "python.command.python.setInterpreter.title": "Sélectionner l'interpreteur", + "python.command.python.updateSparkLibrary.title": "Mettre à jour les librairies de l'espace de travail PySpark", + "python.command.python.refactorExtractVariable.title": "Extraire la variable", + "python.command.python.refactorExtractMethod.title": "Extraire la méthode", + "python.command.python.viewTestOutput.title": "Afficher la sortie des tests unitaires", + "python.command.python.selectAndRunTestMethod.title": "Exécuter la méthode de test unitaire ...", + "python.command.python.selectAndDebugTestMethod.title": "Déboguer la méthode de test unitaire ...", + "python.command.python.selectAndRunTestFile.title": "Exécuter le fichier de test unitaire ...", + "python.command.python.runCurrentTestFile.title": "Exécuter le fichier de test unitaire courant", + "python.command.python.runFailedTests.title": "Exécuter les derniers test unitaires échoués", + "python.command.python.execSelectionInTerminal.title": "Exécuter la ligne/sélection dans un terminal Python", + "python.command.python.execSelectionInDjangoShell.title": "Exécuter la ligne/sélection dans un shell Django", + "python.command.python.goToPythonObject.title": "Se rendre à l'objet Python", + "python.command.python.setLinter.title": "Sélectionner le linter", + "python.command.python.enableLinting.title": "Activer le linting", + "python.command.python.runLinting.title": "Exécuter le linting", + "python.snippet.launch.standard.label": "Python : Fichier actuel", + "python.snippet.launch.standard.description": "Déboguer un programme Python avec la sortie standard", + "python.snippet.launch.pyspark.label": "Python : PySpark", + "python.snippet.launch.pyspark.description": "Déboguer PySpark", + "python.snippet.launch.module.label": "Python: Module", + "python.snippet.launch.module.description": "Déboguer un module Python", + "python.snippet.launch.terminal.label": "Python : Terminal (intégré)", + "python.snippet.launch.terminal.description": "Déboguer un programme Python avec la console intégrée", + "python.snippet.launch.externalTerminal.label": "Python : Terminal (externe)", + "python.snippet.launch.externalTerminal.description": "Déboguer un programme Python avec une console externe", + "python.snippet.launch.django.label": "Python : Django", + "python.snippet.launch.django.description": "Déboguer une application Django", + "python.snippet.launch.flask.label": "Python : Flask (0.11.x ou supérieur)", + "python.snippet.launch.flask.description": "Déboguer une application Flask", + "python.snippet.launch.flaskOld.label": "Python : Flask (0.10.x ou antérieur)", + "python.snippet.launch.flaskOld.description": "Déboguer une application Flask (0.10.x ou antérieur)", + "python.snippet.launch.pyramid.label": "Python : application Pyramid", + "python.snippet.launch.pyramid.description": "Déboguer une application Pyramid", + "python.snippet.launch.watson.label": "Python: Application Watson", + "python.snippet.launch.watson.description": "Déboguer une Application Watson", + "python.snippet.launch.attach.label": "Python: Attacher", + "python.snippet.launch.attach.description": "Attacher le débogueur pour un débogage distant", + "python.snippet.launch.scrapy.label": "Python : Scrapy", + "python.snippet.launch.scrapy.description": "Scrapy avec un terminal intégré" +} diff --git a/package.nls.it.json b/package.nls.it.json new file mode 100644 index 000000000000..6fabf1d664c8 --- /dev/null +++ b/package.nls.it.json @@ -0,0 +1,49 @@ +{ + "python.command.python.sortImports.title": "Ordina gli import", + "python.command.python.startREPL.title": "Apri nuova REPL", + "python.command.python.createTerminal.title": "Apri nuovo terminale", + "python.command.python.buildWorkspaceSymbols.title": "Compila simboli dello spazio di lavoro", + "python.command.python.runtests.title": "Esegui tutti i test", + "python.command.python.debugtests.title": "Esegui debug di tutti i test", + "python.command.python.execInTerminal.title": "Esegui file Python nel terminale", + "python.command.python.setInterpreter.title": "Seleziona interprete", + "python.command.python.updateSparkLibrary.title": "Aggiorna librerie PySpark dello spazio di lavoro", + "python.command.python.refactorExtractVariable.title": "Estrai variable", + "python.command.python.refactorExtractMethod.title": "Estrai metodo", + "python.command.python.viewTestOutput.title": "Mostra output dei test", + "python.command.python.selectAndRunTestMethod.title": "Esegui metodo di test ...", + "python.command.python.selectAndDebugTestMethod.title": "Esegui debug del metodo di test ...", + "python.command.python.selectAndRunTestFile.title": "Esegui file di test ...", + "python.command.python.runCurrentTestFile.title": "Esegui file di test attuale", + "python.command.python.runFailedTests.title": "Esegui test falliti", + "python.command.python.execSelectionInTerminal.title": "Esegui selezione/linea nel terminale di Python", + "python.command.python.execSelectionInDjangoShell.title": "Esegui selezione/linea nella shell Django", + "python.command.python.goToPythonObject.title": "Vai a oggetto Python", + "python.command.python.setLinter.title": "Selezione Linter", + "python.command.python.enableLinting.title": "Attiva Linting", + "python.command.python.runLinting.title": "Esegui Linting", + "python.snippet.launch.standard.label": "Python: File corrente", + "python.snippet.launch.standard.description": "Esegui debug di un programma Python su output predefinito", + "python.snippet.launch.pyspark.label": "Python: PySpark", + "python.snippet.launch.pyspark.description": "Esegui debug PySpark", + "python.snippet.launch.module.label": "Python: Modulo", + "python.snippet.launch.module.description": "Esegui debug modulo Python", + "python.snippet.launch.terminal.label": "Python: Terminale (integrato)", + "python.snippet.launch.terminal.description": "Esegui debug di un programma Python nel terminale integrato", + "python.snippet.launch.externalTerminal.label": "Python: Terminale (esterno)", + "python.snippet.launch.externalTerminal.description": "Esegui debug di un programma Python nel terminale esterno", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.django.description": "Esegui debug applicazione Django", + "python.snippet.launch.flask.label": "Python: Flask (0.11.x o successiva)", + "python.snippet.launch.flask.description": "Esegui debug applicazione Flask", + "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x o precedente)", + "python.snippet.launch.flaskOld.description": "Esegui debug applicazione Flask in vecchio stile", + "python.snippet.launch.pyramid.label": "Python: Applicazione Pyramid", + "python.snippet.launch.pyramid.description": "Esegui debug applicazione Pyramid", + "python.snippet.launch.watson.label": "Python: Applicazione Watson", + "python.snippet.launch.watson.description": "Esegui debug applicazione Watson", + "python.snippet.launch.attach.label": "Python: Allega", + "python.snippet.launch.attach.description": "Allega debugger per debug remoto", + "python.snippet.launch.scrapy.label": "Python: Scrapy", + "python.snippet.launch.scrapy.description": "Scrapy con terminale integrato" +} diff --git a/package.nls.json b/package.nls.json index f1fd181184ab..333a94791381 100644 --- a/package.nls.json +++ b/package.nls.json @@ -16,6 +16,7 @@ "python.command.python.selectAndRunTestFile.title": "Run Unit Test File ...", "python.command.python.runCurrentTestFile.title": "Run Current Unit Test File", "python.command.python.runFailedTests.title": "Run Failed Unit Tests", + "python.command.python.discoverTests.title": "Discover Unit Tests", "python.command.python.execSelectionInTerminal.title": "Run Selection/Line in Python Terminal", "python.command.python.execSelectionInDjangoShell.title": "Run Selection/Line in Django Shell", "python.command.python.goToPythonObject.title": "Go to Python Object", @@ -23,7 +24,7 @@ "python.command.python.enableLinting.title": "Enable Linting", "python.command.python.runLinting.title": "Run Linting", "python.snippet.launch.standard.label": "Python: Current File", - "python.snippet.launch.standard.description": "Debug a Python program with standard output", + "python.snippet.launch.standard.description": "Debug a Python Program with Standard Output", "python.snippet.launch.pyspark.label": "Python: PySpark", "python.snippet.launch.pyspark.description": "Debug PySpark", "python.snippet.launch.module.label": "Python: Module", @@ -38,12 +39,14 @@ "python.snippet.launch.flask.description": "Debug a Flask Application", "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x or earlier)", "python.snippet.launch.flaskOld.description": "Debug an older styled Flask Application", + "python.snippet.launch.gevent.label": "Python: Gevent", + "python.snippet.launch.gevent.description": "Debug a Gevent Application", "python.snippet.launch.pyramid.label": "Python: Pyramid Application", "python.snippet.launch.pyramid.description": "Debug a Pyramid Application", "python.snippet.launch.watson.label": "Python: Watson Application", "python.snippet.launch.watson.description": "Debug a Watson Application", "python.snippet.launch.attach.label": "Python: Attach", - "python.snippet.launch.attach.description": "Attach the debugger for remote debugging", + "python.snippet.launch.attach.description": "Attach the Debugger for Remote Debugging", "python.snippet.launch.scrapy.label": "Python: Scrapy", "python.snippet.launch.scrapy.description": "Scrapy with Integrated Terminal/Console" } diff --git a/package.nls.pt-br.json b/package.nls.pt-br.json new file mode 100644 index 000000000000..5b6e4dc2b539 --- /dev/null +++ b/package.nls.pt-br.json @@ -0,0 +1,52 @@ +{ + "python.command.python.sortImports.title": "Ordenar Importações", + "python.command.python.startREPL.title": "Iniciar REPL", + "python.command.python.createTerminal.title": "Criar Terminal", + "python.command.python.buildWorkspaceSymbols.title": "Construir Símbolos da Área de Trabalho", + "python.command.python.runtests.title": "Executar Todos os Testes Unitários", + "python.command.python.debugtests.title": "Depurar Todos os Testes Unitários", + "python.command.python.execInTerminal.title": "Executar Arquivo no Terminal", + "python.command.python.setInterpreter.title": "Selecionar Interpretador", + "python.command.python.updateSparkLibrary.title": "Atualizar Área de Trabalho da Biblioteca PySpark", + "python.command.python.refactorExtractVariable.title": "Extrair Variável", + "python.command.python.refactorExtractMethod.title": "Extrair Método", + "python.command.python.viewTestOutput.title": "Exibir Resultados dos Testes Unitários", + "python.command.python.selectAndRunTestMethod.title": "Executar Testes Unitários do Método ...", + "python.command.python.selectAndDebugTestMethod.title": "Depurar Testes Unitários do Método ...", + "python.command.python.selectAndRunTestFile.title": "Executar Arquivo de Testes Unitários ...", + "python.command.python.runCurrentTestFile.title": "Executar o Arquivo de Testes Unitários Atual", + "python.command.python.runFailedTests.title": "Executar Testes Unitários com Falhas", + "python.command.python.discoverTests.title": "Descobrir Testes Unitários", + "python.command.python.execSelectionInTerminal.title": "Executar Seleção/Linha no Terminal", + "python.command.python.execSelectionInDjangoShell.title": "Executar Seleção/Linha no Django Shell", + "python.command.python.goToPythonObject.title": "Ir para Objeto Python", + "python.command.python.setLinter.title": "Selecionar Linter", + "python.command.python.enableLinting.title": "Habilitar Linting", + "python.command.python.runLinting.title": "Executar Linting", + "python.snippet.launch.standard.label": "Python: Arquivo Atual", + "python.snippet.launch.standard.description": "Depurar um Programa Python com a saída padrão", + "python.snippet.launch.pyspark.label": "Python: PySpark", + "python.snippet.launch.pyspark.description": "Depurar PySpark", + "python.snippet.launch.module.label": "Python: Módulo", + "python.snippet.launch.module.description": "Depurar um Módulo Python", + "python.snippet.launch.terminal.label": "Python: Terminal (integrado)", + "python.snippet.launch.terminal.description": "Depurar um Programa Python com Terminal/Console Integrado", + "python.snippet.launch.externalTerminal.label": "Python: Terminal (externo)", + "python.snippet.launch.externalTerminal.description": "Depurar um Programa Python com Terminal/Console Externo", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.django.description": "Depurar uma Aplicação Django", + "python.snippet.launch.flask.label": "Python: Flask (0.11.x ou superior)", + "python.snippet.launch.flask.description": "Depurar uma Aplicação Flask", + "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x ou inferior)", + "python.snippet.launch.flaskOld.description": "Depurar uma Aplicação Flask no Estilo Antigo", + "python.snippet.launch.gevent.label": "Python: Gevent", + "python.snippet.launch.gevent.description": "Depurar uma Aplicação Gevent", + "python.snippet.launch.pyramid.label": "Python: Aplicação Pyramid", + "python.snippet.launch.pyramid.description": "Depurar uma Aplicação Pyramid", + "python.snippet.launch.watson.label": "Python: Aplicação Watson", + "python.snippet.launch.watson.description": "Depurar uma Aplicação Watson", + "python.snippet.launch.attach.label": "Python: Anexar", + "python.snippet.launch.attach.description": "Anexar depurador para depuração remota", + "python.snippet.launch.scrapy.label": "Python: Scrapy", + "python.snippet.launch.scrapy.description": "Scrapy com Terminal/Console Integrado" +} diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index b884240efa16..4580d1fa4b10 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -1,6 +1,7 @@ { "python.command.python.sortImports.title": "排序 import 语句", "python.command.python.startREPL.title": "启动 REPL", + "python.command.python.createTerminal.title": "创建终端", "python.command.python.buildWorkspaceSymbols.title": "构建工作区符号", "python.command.python.runtests.title": "运行所有单元测试", "python.command.python.debugtests.title": "调试所有单元测试", @@ -15,9 +16,14 @@ "python.command.python.selectAndRunTestFile.title": "运行单元测试文件...", "python.command.python.runCurrentTestFile.title": "运行当前单元测试文件", "python.command.python.runFailedTests.title": "运行失败的单元测试", + "python.command.python.discoverTests.title": "检测单元测试", "python.command.python.execSelectionInTerminal.title": "在 Python 终端中运行选定内容/行", "python.command.python.execSelectionInDjangoShell.title": "在 Django Shell 中运行选定内容/行", "python.command.python.goToPythonObject.title": "转到 Python 对象", + "python.command.python.setLinter.title": "选择 Linter 插件", + "python.command.python.enableLinting.title": "启用 Linting", + "python.command.python.runLinting.title": "运行 Linting", + "python.snippet.launch.standard.label": "Python: 当前文件", "python.snippet.launch.standard.label": "Python: Current File", "python.snippet.launch.standard.description": "使用标准输出调试 Python 应用", "python.snippet.launch.pyspark.label": "Python: PySpark", @@ -34,6 +40,8 @@ "python.snippet.launch.flask.description": "调试 Flask 应用", "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x 或之前)", "python.snippet.launch.flaskOld.description": "调试旧式 Flask 应用", + "python.snippet.launch.gevent.label": "Python: Gevent 应用", + "python.snippet.launch.gevent.description": "调试 Gevent 应用", "python.snippet.launch.pyramid.label": "Python: Pyramid 应用", "python.snippet.launch.pyramid.description": "调试 Pyramid 应用", "python.snippet.launch.watson.label": "Python: Watson 应用", diff --git a/packageExtension.cmd b/packageExtension.cmd new file mode 100644 index 000000000000..dc3026c20f3e --- /dev/null +++ b/packageExtension.cmd @@ -0,0 +1 @@ +%1\vsce package --out %2 \ No newline at end of file diff --git a/pythonFiles/PythonTools/ptvsd/__init__.py b/pythonFiles/PythonTools/ptvsd/__init__.py index 25765e165f14..c1b6fb6cf61e 100644 --- a/pythonFiles/PythonTools/ptvsd/__init__.py +++ b/pythonFiles/PythonTools/ptvsd/__init__.py @@ -1,22 +1,22 @@ # Python Tools for Visual Studio # Copyright(c) Microsoft Corporation # All rights reserved. -# +# # Licensed under the Apache License, Version 2.0 (the License); you may not use # this file except in compliance with the License. You may obtain a copy of the # License at http://www.apache.org/licenses/LICENSE-2.0 -# +# # THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS # OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY # IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, # MERCHANTABLITY OR NON-INFRINGEMENT. -# +# # See the Apache Version 2.0 License for specific language governing # permissions and limitations under the License. __author__ = "Microsoft Corporation " __version__ = "3.0.0.0" -__all__ = ['enable_attach', 'wait_for_attach', 'break_into_debugger', 'settrace', 'is_attached', 'AttachAlreadyEnabledError'] +__all__ = ['enable_attach', 'enable_attach_ui', 'wait_for_attach', 'break_into_debugger', 'set_attach_ui_options', 'set_trace', 'is_attached', 'AttachAlreadyEnabledError'] -from ptvsd.attach_server import enable_attach, wait_for_attach, break_into_debugger, settrace, is_attached, AttachAlreadyEnabledError \ No newline at end of file +from ptvsd.attach_server import enable_attach, enable_attach_ui, wait_for_attach, break_into_debugger, set_attach_ui_options, set_trace, is_attached, AttachAlreadyEnabledError diff --git a/pythonFiles/PythonTools/ptvsd/__main__.py b/pythonFiles/PythonTools/ptvsd/__main__.py index 8fc5004aa43a..2837de743528 100644 --- a/pythonFiles/PythonTools/ptvsd/__main__.py +++ b/pythonFiles/PythonTools/ptvsd/__main__.py @@ -54,4 +54,4 @@ DONT_DEBUG.append(os.path.normcase(__file__)) sys.argv = script_argv -exec_file(script_argv[0], {'__name__': '__main__'}) \ No newline at end of file +exec_file(script_argv[0], {'__name__': '__main__'}) diff --git a/pythonFiles/PythonTools/ptvsd/attach_server.py b/pythonFiles/PythonTools/ptvsd/attach_server.py index 743c89be787f..48dbda79b434 100644 --- a/pythonFiles/PythonTools/ptvsd/attach_server.py +++ b/pythonFiles/PythonTools/ptvsd/attach_server.py @@ -1,16 +1,16 @@ # Python Tools for Visual Studio # Copyright(c) Microsoft Corporation # All rights reserved. -# +# # Licensed under the Apache License, Version 2.0 (the License); you may not use # this file except in compliance with the License. You may obtain a copy of the # License at http://www.apache.org/licenses/LICENSE-2.0 -# +# # THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS # OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY # IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, # MERCHANTABLITY OR NON-INFRINGEMENT. -# +# # See the Apache Version 2.0 License for specific language governing # permissions and limitations under the License. @@ -37,6 +37,14 @@ except ImportError: ssl = None +import json +import errno +from socket import error as socket_error +try: + from urllib.request import urlopen, Request, URLError +except ImportError: + from urllib2 import urlopen, Request, URLError + import ptvsd.visualstudio_py_debugger as vspd import ptvsd.visualstudio_py_repl as vspr from ptvsd.visualstudio_py_util import to_bytes, read_bytes, read_int, read_string, write_bytes, write_int, write_string @@ -66,12 +74,12 @@ # (int64), and then the Python language version that the server is running represented by three int64s - # major, minor, micro; From there on the socket is assumed to be using the normal PTVS debugging protocol. # If attaching was not successful (which can happen if some other debugger is already attached), the server -# responds with 'RJCT' and closes the connection. +# responds with 'RJCT' and closes the connection. # # 'REPL' # Attach REPL to the process. If successful, the server responds with 'ACPT', and from there on the socket # is assumed to be using the normal PTVS REPL protocol. If not successful (which can happen if there is -# no debugger attached), the server responds with 'RJCT' and closes the connection. +# no debugger attached), the server responds with 'RJCT' and closes the connection. PTVS_VER = '2.2' DEFAULT_PORT = 5678 @@ -82,16 +90,19 @@ INFO = to_bytes('INFO') ATCH = to_bytes('ATCH') REPL = to_bytes('REPL') +DEBUGGER_UI_PORT = 9615 _attach_enabled = False _attached = threading.Event() +_attach_port = None +_ui_attach_enabled = False +_ui_attach_options = {} vspd.DONT_DEBUG.append(os.path.normcase(__file__)) class AttachAlreadyEnabledError(Exception): """`ptvsd.enable_attach` has already been called in this process.""" - def enable_attach(secret, address = ('0.0.0.0', DEFAULT_PORT), certfile = None, keyfile = None, redirect_output = True): """Enables Python Tools for Visual Studio to attach to this process remotely to debug Python code. @@ -105,7 +116,7 @@ def enable_attach(secret, address = ('0.0.0.0', DEFAULT_PORT), certfile = None, hostname by ``'@'``, e.g.: ``'secret@myhost.cloudapp.net:5678'``. If secret is ``None``, there's no validation, and any client can connect freely. - address : (str, int), optional + address : (str, int), optional Specifies the interface and port on which the debugging server should listen for TCP connections. It is in the same format as used for regular sockets of the `socket.AF_INET` family, i.e. a tuple of @@ -116,7 +127,7 @@ def enable_attach(secret, address = ('0.0.0.0', DEFAULT_PORT), certfile = None, Used to enable SSL. If not specified, or if set to ``None``, the connection between this program and the debugger will be unsecure, and can be intercepted on the wire. If specified, the meaning of this - parameter is the same as for `ssl.wrap_socket`. + parameter is the same as for `ssl.wrap_socket`. keyfile : str, optional Used together with `certfile` when SSL is enabled. Its meaning is the same as for ``ssl.wrap_socket``. @@ -131,7 +142,7 @@ def enable_attach(secret, address = ('0.0.0.0', DEFAULT_PORT), certfile = None, is attached, call `ptvsd.wait_for_attach`. The debugger can be detached and re-attached multiple times after `enable_attach` is called. - This function can only be called once during the lifetime of the process. + This function can only be called once during the lifetime of the process. On a second call, `AttachAlreadyEnabledError` is raised. In circumstances where the caller does not control how many times the function will be called (e.g. when a script with a single call is run more than once by @@ -166,6 +177,8 @@ def enable_attach(secret, address = ('0.0.0.0', DEFAULT_PORT), certfile = None, server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(address) server.listen(1) + global _attach_port + _attach_port = server.getsockname()[1] def server_thread_func(): while True: client = None @@ -298,8 +311,61 @@ def replace_trace_func(): vspd.intercept_threads(for_attach = True) -# Alias for convenience of users of pydevd -settrace = enable_attach +# `set_trace` should pause debug execution and attach the debugger UI to the debugger engine +def set_trace(): + # Enable on-demand UI attach to the debugger. + enable_attach_ui() + # Trigger the debugger ui to attach, if one exists + debugger_ui_attach() + wait_for_attach(30) + if vspd.DETACHED: + sys.stderr.write('Debugger timed out (30 seconds) waiting for attach!\n') + else: + break_into_debugger() + + +# Options could have: `debugOptions`, `localRoot` & `remoteRoot` & `id`. +def enable_attach_ui(): + global _attach_enabled, _ui_attach_options, _ui_attach_enabled + if not _attach_enabled: + enable_attach(None, ('0.0.0.0', 0)) + if not _ui_attach_enabled: + _ui_attach_enabled = debugger_ui_enable_attach() + + +def set_attach_ui_options(options): + global _ui_attach_options + _ui_attach_options = options + + +def debugger_ui_attach(): + if not vspd.DETACHED: + return + global _attach_port + attach_info = {"domain": "debug", "type": "python", "command": "attach", "port": _attach_port} + debugger_ui_request(attach_info) + + +def debugger_ui_enable_attach(): + global _attach_port, _ui_attach_options + attach_info = {"domain": "debug", "type": "python", "command": "enable-attach", + "port": _attach_port, "options": _ui_attach_options} + return debugger_ui_request(attach_info) + + +def debugger_ui_request(info): + req = Request('http://127.0.0.1:' + str(DEBUGGER_UI_PORT), + data=json.dumps(info).encode('utf8'), + headers={'Content-Type': 'application/json', 'Accept': 'application/json'}) + try: + response = urlopen(req) + except URLError: + # It's okay if there's no debugger ui server waiting for that info + return False + json_response = json.loads(response.read()) + if not json_response['success']: + raise RuntimeError('Failed attach attempt') + return True def wait_for_attach(timeout = None): diff --git a/pythonFiles/PythonTools/ptvsd/visualstudio_py_debugger.py b/pythonFiles/PythonTools/ptvsd/visualstudio_py_debugger.py index b589a8208baa..12aa0b09f2ed 100644 --- a/pythonFiles/PythonTools/ptvsd/visualstudio_py_debugger.py +++ b/pythonFiles/PythonTools/ptvsd/visualstudio_py_debugger.py @@ -1,16 +1,16 @@ # Python Tools for Visual Studio # Copyright(c) Microsoft Corporation # All rights reserved. -# +# # Licensed under the Apache License, Version 2.0 (the License); you may not use # this file except in compliance with the License. You may obtain a copy of the # License at http://www.apache.org/licenses/LICENSE-2.0 -# +# # THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS # OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY # IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, # MERCHANTABLITY OR NON-INFRINGEMENT. -# +# # See the Apache Version 2.0 License for specific language governing # permissions and limitations under the License. # With number of modifications by Don Jayamanne @@ -76,8 +76,10 @@ except ImportError: import ptvsd.visualstudio_py_repl as _vspr + try: import stackless + stackless.tasklet # work-around lazy on-demand importers except ImportError: stackless = None @@ -233,8 +235,8 @@ def find_by_id(breakpoint_id): send_lock = thread.allocate_lock() class _SendLockContextManager(object): - """context manager for send lock. Handles both acquiring/releasing the - send lock as well as detaching the debugger if the remote process + """context manager for send lock. Handles both acquiring/releasing the + send lock as well as detaching the debugger if the remote process is disconnected""" def __enter__(self): @@ -248,7 +250,7 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, tb): send_lock.release() - + # start sending debug events again cur_thread = get_thread_from_id(thread.get_ident()) if cur_thread is not None: @@ -258,8 +260,8 @@ def __exit__(self, exc_type, exc_value, tb): detach_threads() detach_process() # swallow the exception, we're no longer debugging - return True - + return True + _SendLockCtx = _SendLockContextManager() SEND_BREAK_COMPLETE = False @@ -334,7 +336,7 @@ def is_repr_round_tripping(x): try: # getfilesystemencoding is used here because it effectively corresponds to the notion of "locale encoding": # current ANSI codepage on Windows, LC_CTYPE on Linux, UTF-8 on OS X - which is exactly what we want. - TYPES_WITH_RAW_REPR[bytearray] = lambda b: b.decode(sys.getfilesystemencoding(), 'ignore') + TYPES_WITH_RAW_REPR[bytearray] = lambda b: b.decode(sys.getfilesystemencoding(), 'ignore') except: pass @@ -350,7 +352,7 @@ def is_repr_round_tripping(x): class StackOverflowException(Exception): pass else: StackOverflowException = RuntimeError - + ASBR = to_bytes('ASBR') SETL = to_bytes('SETL') THRF = to_bytes('THRF') @@ -420,7 +422,7 @@ def lookup_local(frame, name): while bits and obj is not None and type(obj) is types.ModuleType: obj = getattr(obj, bits.pop(0), None) return obj - + if sys.version_info[0] >= 3: _EXCEPTIONS_MODULE = 'builtins' else: @@ -489,7 +491,7 @@ def should_break(self, thread, ex_type, ex_value, trace): break_type = BREAK_TYPE_NONE return break_type - + def is_handled(self, thread, ex_type, ex_value, trace): if trace is None: # get out if we didn't get a traceback @@ -500,9 +502,9 @@ def is_handled(self, thread, ex_type, ex_value, trace): # don't break if this is not the top of the traceback, # unless the previous frame was not debuggable return True - + cur_frame = trace.tb_frame - + while should_send_frame(cur_frame) and cur_frame.f_code is not None and cur_frame.f_code.co_filename is not None: filename = path.normcase(cur_frame.f_code.co_filename) if is_file_in_zip(filename): @@ -511,11 +513,11 @@ def is_handled(self, thread, ex_type, ex_value, trace): if not is_same_py_file(filename, __file__): handlers = self.handler_cache.get(filename) - + if handlers is None: # req handlers for this file from the debug engine self.handler_lock.acquire() - + with _SendLockCtx: write_bytes(conn, REQH) write_string(conn, filename) @@ -547,7 +549,7 @@ def is_handled(self, thread, ex_type, ex_value, trace): cur_frame = cur_frame.f_back return False - + def add_exception(self, name, mode=BREAK_MODE_UNHANDLED): if name.startswith(_EXCEPTIONS_MODULE + '.'): name = name[len(_EXCEPTIONS_MODULE) + 1:] @@ -556,7 +558,7 @@ def add_exception(self, name, mode=BREAK_MODE_UNHANDLED): BREAK_ON = ExceptionBreakInfo() def probe_stack(depth = 10): - """helper to make sure we have enough stack space to proceed w/o corrupting + """helper to make sure we have enough stack space to proceed w/o corrupting debugger state.""" if depth == 0: return @@ -599,7 +601,7 @@ def breakpoint_path_match(vs_path, local_path): local_path_norm = path.normcase(local_path) if local_path_to_vs_path.get(local_path_norm) == vs_path_norm: return True - + # Walk the local filesystem from local_path up, matching agains win_path component by component, # and stop when we no longer see an __init__.py. This should give a reasonably close approximation # of matching the package name. @@ -615,7 +617,7 @@ def breakpoint_path_match(vs_path, local_path): # needed to, and matched all names on our way, so this is a match. if not path.exists(path.join(local_path, '__init__.py')): break - + local_path_to_vs_path[local_path_norm] = vs_path_norm return True @@ -623,26 +625,26 @@ def update_all_thread_stacks(blocking_thread = None, check_is_blocked = True): THREADS_LOCK.acquire() all_threads = list(THREADS.values()) THREADS_LOCK.release() - + for cur_thread in all_threads: if cur_thread is blocking_thread: continue - + cur_thread._block_starting_lock.acquire() if not check_is_blocked or not cur_thread._is_blocked: # release the lock, we're going to run user code to evaluate the frames - cur_thread._block_starting_lock.release() - + cur_thread._block_starting_lock.release() + frames = cur_thread.get_frame_list() - + # re-acquire the lock and make sure we're still not blocked. If so send # the frame list. cur_thread._block_starting_lock.acquire() if not check_is_blocked or not cur_thread._is_blocked: cur_thread.send_frame_list(frames) - + cur_thread._block_starting_lock.release() - + DJANGO_BREAKPOINTS = {} DJANGO_TEMPLATES = {} @@ -652,7 +654,7 @@ def __init__(self, filename): self.filename = filename self.breakpoints = {} self.rangeIsPlainText = {} - + def add_breakpoint(self, lineno, brkpt_id): self.breakpoints[lineno] = brkpt_id @@ -673,20 +675,20 @@ def is_range_plain_text(self, start, end): with open(self.filename, 'rb') as contents: contents.seek(start, 0) # 0 = start of file, optional in this case data = contents.read(end - start) - isPlainText = True + isPlainText = True if data.startswith('{{') and data.endswith('}}'): isPlainText = False if data.startswith('{%') and data.endswith('%}'): isPlainText = False self.rangeIsPlainText[key] = isPlainText - return isPlainText + return isPlainText except: return False else: return self.rangeIsPlainText.get(key) def line_number_to_offset(self, lineNumber): - line_locs = self.line_locations + line_locs = self.line_locations if line_locs is not None: low_line = line_locs[lineNumber - 1] hi_line = line_locs[lineNumber] @@ -722,7 +724,7 @@ def line_locations(self): return self._line_locations def get_line_range(self, start, end): - line_locs = self.line_locations + line_locs = self.line_locations if line_locs is not None: low_line = bisect.bisect_right(line_locs, start) hi_line = bisect.bisect_right(line_locs, end) @@ -735,10 +737,10 @@ def should_break(self, start, end): low_line, hi_line = self.get_line_range(start, end) if low_line is not None and hi_line is not None: # low_line/hi_line is 0 based, self.breakpoints is 1 based - for i in xrange(low_line+1, hi_line+2): + for i in xrange(low_line+1, hi_line+2): bkpt_id = self.breakpoints.get(i) if bkpt_id is not None: - return True, bkpt_id + return True, bkpt_id return False, 0 @@ -756,7 +758,7 @@ def get_django_frame_source(frame): IS_DJANGO19 = version[0] == 1 and version[1] == 9 IS_DJANGO19_OR_HIGHER = ((version[0] == 1 and version[1] >= 9) or version[0] > 1) except: - pass + pass if frame.f_code.co_name == 'render': self_obj = frame.f_locals.get('self', None) if self_obj is None: @@ -794,12 +796,12 @@ def __getattr__(self, name): class Thread(object): def __init__(self, id = None): if id is not None: - self.id = id + self.id = id else: self.id = thread.get_ident() - self._events = {'call' : self.handle_call, - 'line' : self.handle_line, - 'return' : self.handle_return, + self._events = {'call' : self.handle_call, + 'line' : self.handle_line, + 'return' : self.handle_return, 'exception' : self.handle_exception, 'c_call' : self.handle_c_call, 'c_return' : self.handle_c_return, @@ -865,7 +867,7 @@ def new_f(old_f, args, kwargs): tsk.tempval = new_f stackless.tasklet.setup(tsk, f, args, kwargs) return tsk - + def settrace(tsk, tb): if hasattr(tsk.frame, "f_trace"): tsk.frame.f_trace = tb @@ -876,14 +878,14 @@ def settrace(tsk, tb): stackless.tasklet.__call__ = __call__ if sys.platform == 'cli': self.frames = [] - + if sys.platform == 'cli': # workaround an IronPython bug where we're sometimes missing the back frames # http://ironpython.codeplex.com/workitem/31437 def push_frame(self, frame): self.cur_frame = frame self.frames.append(frame) - + def pop_frame(self): self.frames.pop() self.cur_frame = self.frames[-1] @@ -908,11 +910,11 @@ def _stackless_schedule_cb(self, prev, next): if not current: return current_tf = current.trace_function - + try: current.trace_function = None self.stepping = STEPPING_NONE - + # If the current frame has no trace function, we may need to get it # from the previous frame, depending on how we ended up in the # callback. @@ -939,7 +941,7 @@ def trace_func(self, frame, event, arg): if sys is None: return None elif self.is_sending: - # https://pytools.codeplex.com/workitem/1864 + # https://pytools.codeplex.com/workitem/1864 # we're currently doing I/O w/ the socket, we don't want to deliver # any breakpoints or async breaks because we'll deadlock. Continue # to return the trace function so all of our frames remain @@ -953,7 +955,7 @@ def trace_func(self, frame, event, arg): try: # if should_debug_code(frame.f_code) is not true during attach - # the current frame is None and a pop_frame will cause an exception and + # the current frame is None and a pop_frame will cause an exception and # break the debugger if self.cur_frame is None: # happens during attach, we need frame for blocking @@ -972,7 +974,7 @@ def trace_func(self, frame, event, arg): except (StackOverflowException, KeyboardInterrupt): # stack overflow, disable tracing return self.trace_func - + def handle_call(self, frame, arg): self.push_frame(frame) @@ -980,10 +982,10 @@ def handle_call(self, frame, arg): source_obj = get_django_frame_source(frame) if source_obj is not None: origin, (start, end), lineNumber = source_obj - + active_bps = DJANGO_BREAKPOINTS.get(origin.lower()) should_break = False - if active_bps is not None and origin != '': + if active_bps is not None and origin != '': should_break, bkpt_id = active_bps.should_break(start, end) isPlainText = active_bps.is_range_plain_text(start, end) if isPlainText: @@ -1023,8 +1025,8 @@ def handle_call(self, frame, arg): elif stepping <= STEPPING_OUT: self.stepping -= 1 - if (sys.platform == 'cli' and - frame.f_code.co_name == '' and + if (sys.platform == 'cli' and + frame.f_code.co_name == '' and not IPY_SEEN_MODULES.TryGetValue(frame.f_code)[0]): IPY_SEEN_MODULES.Add(frame.f_code, None) # work around IronPython bug - http://ironpython.codeplex.com/workitem/30127 @@ -1038,7 +1040,7 @@ def handle_call(self, frame, arg): self.prev_trace_func = old_trace_func(frame, 'call', arg) return self.trace_func - + def should_block_on_frame(self, frame): if not should_debug_code(frame.f_code): return False @@ -1091,7 +1093,7 @@ def handle_line(self, frame, arg): # the module to which it was bound, so only exact matches are considered hits. if bp.is_bound: continue - # Otherwise, use relaxed path check that tries to handle differences between + # Otherwise, use relaxed path check that tries to handle differences between # local and remote filesystems for remote scenarios: if not breakpoint_path_match(filename, frame.f_code.co_filename): continue @@ -1159,7 +1161,7 @@ def handle_line(self, frame, arg): self.prev_trace_func = old_trace_func(frame, 'line', arg) return self.trace_func - + def handle_return(self, frame, arg): self.pop_frame() @@ -1196,7 +1198,7 @@ def handle_return(self, frame, arg): # restore previous frames trace function if there is one if self.trace_func_stack: self.prev_trace_func = self.trace_func_stack.pop() - + def handle_exception(self, frame, arg): if self.stepping == STEPPING_ATTACH_BREAK: self.block_maybe_attach() @@ -1214,15 +1216,15 @@ def handle_exception(self, frame, arg): self.prev_trace_func = old_trace_func(frame, 'exception', arg) return self.trace_func - + def handle_c_call(self, frame, arg): # break points? pass - + def handle_c_return(self, frame, arg): # step out of ? pass - + def handle_c_exception(self, frame, arg): pass @@ -1236,7 +1238,7 @@ def block_maybe_attach(self): will_block_now = False attach_sent_break = True attach_lock.release() - + probe_stack() stepping = self.stepping self.stepping = STEPPING_NONE @@ -1252,7 +1254,7 @@ def block_cond(): return report_process_loaded(self.id) update_all_thread_stacks(self) self.block(block_cond) - + def async_break(self): def async_break_send(): with _SendLockCtx: @@ -1266,7 +1268,7 @@ def async_break_send(): write_int(conn, self.id) if sent_break_complete: - # if we have threads which have not broken yet capture their frame list and + # if we have threads which have not broken yet capture their frame list and # send it now. If they block we'll send an updated (and possibly more accurate - if # there are any thread locals) list of frames. update_all_thread_stacks(self) @@ -1278,10 +1280,10 @@ def block(self, block_lambda, keep_stopped_on_line = False): """blocks the current thread until the debugger resumes it""" assert not self._is_blocked #assert self.id == thread.get_ident(), 'wrong thread identity' + str(self.id) + ' ' + str(thread.get_ident()) # we should only ever block ourselves - + # send thread frames before we block self.enum_thread_frames_locally() - + if not keep_stopped_on_line: self.stopped_on_line = self.cur_frame.f_lineno @@ -1301,7 +1303,7 @@ def block(self, block_lambda, keep_stopped_on_line = False): self.unblock_work() self.unblock_work = None self._is_working = False - + self._block_starting_lock.acquire() assert self._is_blocked self._is_blocked = False @@ -1309,10 +1311,11 @@ def block(self, block_lambda, keep_stopped_on_line = False): def unblock(self): """unblocks the current thread allowing it to continue to run""" - assert self._is_blocked + assert self._is_blocked assert self.id != thread.get_ident() # only someone else should unblock us - - self._block_lock.release() + + if self._block_lock.locked(): + self._block_lock.release() def schedule_work(self, work): self.unblock_work = work @@ -1320,26 +1323,26 @@ def schedule_work(self, work): def run_on_thread(self, text, cur_frame, execution_id, frame_kind, repr_kind = PYTHON_EVALUATION_RESULT_REPR_KIND_NORMAL): self._block_starting_lock.acquire() - + if not self._is_blocked: report_execution_error('', execution_id) elif not self._is_working: self.schedule_work(lambda : self.run_locally(text, cur_frame, execution_id, frame_kind, repr_kind)) else: report_execution_error('', execution_id) - + self._block_starting_lock.release() def run_on_thread_no_report(self, text, cur_frame, frame_kind): self._block_starting_lock.acquire() - + if not self._is_blocked: pass elif not self._is_working: self.schedule_work(lambda : self.run_locally_no_report(text, cur_frame, frame_kind)) else: pass - + self._block_starting_lock.release() def enum_child_on_thread(self, text, cur_frame, execution_id, frame_kind): @@ -1463,7 +1466,7 @@ def enum_child_locally(self, expr, cur_frame, execution_id, frame_kind): break key_repr = safe_repr(key) - + # Some objects are enumerable but not indexable, or repr(key) is not a valid Python expression. For those, we # cannot use obj[key] to get the item by its key, and have to retrieve it by index from enumerate() instead. try: @@ -1494,7 +1497,7 @@ def enum_child_locally(self, expr, cur_frame, execution_id, frame_kind): def get_frame_list(self): frames = [] cur_frame = self.cur_frame - + while should_send_frame(cur_frame): # calculate the ending line number lineno = cur_frame.f_code.co_firstlineno @@ -1541,7 +1544,7 @@ def get_frame_list(self): f_globals = cur_frame.f_globals if f_globals: # ensure globals to work with (IPy may have None for cur_frame.f_globals for frames within stdlib) self.collect_variables(vars, f_globals, cur_frame.f_code.co_names, treated, skip_unknown = True) - + frame_info = None if source_obj is not None: @@ -1557,8 +1560,8 @@ def get_frame_list(self): frame_kind = FRAME_KIND_DJANGO frame_info = ( low_line + 1, - hi_line + 1, - low_line + 1, + hi_line + 1, + low_line + 1, cur_frame.f_code.co_name, str(origin), 0, @@ -1571,8 +1574,8 @@ def get_frame_list(self): if frame_info is None: frame_info = ( cur_frame.f_code.co_firstlineno, - lineno, - cur_frame.f_lineno, + lineno, + cur_frame.f_lineno, cur_frame.f_code.co_name, get_code_filename(cur_frame.f_code), cur_frame.f_code.co_argcount, @@ -1583,9 +1586,9 @@ def get_frame_list(self): ) frames.append(frame_info) - + cur_frame = cur_frame.f_back - + return frames def collect_variables(self, vars, objects, names, treated, skip_unknown = False): @@ -1613,24 +1616,24 @@ def send_frame_list(self, frames, thread_name = None): write_bytes(conn, THRF) write_int(conn, self.id) write_string(conn, thread_name) - + # send the frame count write_int(conn, len(frames)) for firstlineno, lineno, curlineno, name, filename, argcount, variables, frameKind, sourceFile, sourceLine in frames: - # send each frame + # send each frame write_int(conn, firstlineno) write_int(conn, lineno) write_int(conn, curlineno) - + write_string(conn, name) write_string(conn, filename) write_int(conn, argcount) - + write_int(conn, frameKind) if frameKind == FRAME_KIND_DJANGO: write_string(conn, sourceFile) write_int(conn, sourceLine) - + write_int(conn, len(variables)) for name, type_obj, safe_repr_obj, hex_repr_obj, type_name, obj_len in variables: write_string(conn, name) @@ -1737,7 +1740,7 @@ def loop(self): pass except: traceback.print_exc() - + def command_step_into(self): tid = read_int(self.conn) thread = get_thread_from_id(tid) @@ -1753,7 +1756,7 @@ def command_step_out(self): assert thread._is_blocked thread.stepping = STEPPING_OUT self.command_resume_all() - + def command_step_over(self): # set step over tid = read_int(self.conn) @@ -1793,7 +1796,7 @@ def command_set_breakpoint_condition(self): breakpoint_id = read_int(self.conn) kind = read_int(self.conn) condition = read_string(self.conn) - + bp = BreakpointInfo.find_by_id(breakpoint_id) if bp is not None: bp.condition_kind = kind @@ -1812,7 +1815,7 @@ def command_set_breakpoint_pass_count(self): def command_set_breakpoint_hit_count(self): breakpoint_id = read_int(self.conn) count = read_int(self.conn) - + bp = BreakpointInfo.find_by_id(breakpoint_id) if bp is not None: bp.hit_count = count @@ -1820,7 +1823,7 @@ def command_set_breakpoint_hit_count(self): def command_get_breakpoint_hit_count(self): req_id = read_int(self.conn) breakpoint_id = read_int(self.conn) - + bp = BreakpointInfo.find_by_id(breakpoint_id) count = 0 if bp is not None: @@ -1907,7 +1910,7 @@ def command_resume_all(self): if thread._is_blocked: thread.unblock() thread._block_starting_lock.release() - + def command_resume_thread(self): tid = read_int(self.conn) THREADS_LOCK.acquire() @@ -1927,7 +1930,7 @@ def command_auto_resume(self): THREADS_LOCK.release() stepping = thread.stepping - if ((stepping == STEPPING_OVER or stepping == STEPPING_INTO) and thread.cur_frame.f_lineno != thread.stopped_on_line): + if ((stepping == STEPPING_OVER or stepping == STEPPING_INTO) and thread.cur_frame.f_lineno != thread.stopped_on_line): report_step_finished(tid) else: self.command_resume_all() @@ -2020,11 +2023,11 @@ def command_enum_children(self): fid = read_int(self.conn) # frame id eid = read_int(self.conn) # execution id frame_kind = read_int(self.conn) # frame kind - + thread, cur_frame = self.get_thread_and_frame(tid, fid, frame_kind) if thread is not None and cur_frame is not None: thread.enum_child_on_thread(text, cur_frame, eid, frame_kind) - + def get_thread_and_frame(self, tid, fid, frame_kind): thread = get_thread_from_id(tid) cur_frame = None @@ -2049,11 +2052,11 @@ def command_detach(self): with _SendLockCtx: write_bytes(conn, DETC) - detach_process() + detach_process() for callback in DETACH_CALLBACKS: callback() - + raise DebuggerExitException() def command_last_ack(self): @@ -2099,12 +2102,12 @@ def report_exception(frame, exc_info, tid, break_type): exc_name = get_exception_name(exc_type) exc_value = exc_info[1] tb_value = exc_info[2] - + if type(exc_value) is tuple: - # exception object hasn't been created yet, create it now + # exception object hasn't been created yet, create it now # so we can get the correct msg. exc_value = exc_type(*exc_value) - + data = { 'typename': get_exception_name(exc_type), 'message': str(exc_value), @@ -2172,7 +2175,7 @@ def report_breakpoint_failed(id): write_bytes(conn, BRKF) write_int(conn, id) -def report_breakpoint_hit(id, tid): +def report_breakpoint_hit(id, tid): with _SendLockCtx: write_bytes(conn, BRKH) write_int(conn, id) @@ -2191,7 +2194,8 @@ def report_execution_error(exc_text, execution_id): def report_execution_exception(execution_id, exc_info): try: - exc_text = str(exc_info[1]) + exc_type, exc_value, traceback = exc_info + exc_text = '{}: {}'.format(get_exception_name(exc_type), exc_value) except: exc_text = 'An exception was thrown' @@ -2217,7 +2221,7 @@ def report_execution_result(execution_id, result, repr_kind = PYTHON_EVALUATION_ hex_repr = safe_hex_repr(result) else: flags = PYTHON_EVALUATION_RESULT_RAW - hex_repr = None + hex_repr = None for cls, raw_repr in TYPES_WITH_RAW_REPR.items(): if isinstance(result, cls): try: @@ -2311,7 +2315,7 @@ def attach_process(port_num, debug_id, debug_options, currentPid, report = False ## Begin modification by Don Jayamanne # Pass current Process id to pass back to debugger write_int(conn, currentPid) # success - ## End Modification by Don Jayamanne + ## End Modification by Don Jayamanne break except: import time @@ -2359,9 +2363,16 @@ def _excepthook(exc_type, exc_value, exc_tb): global debugger_thread_id debugger_thread_id = _start_new_thread(DebuggerLoop(conn).loop, ()) - for mod_name, mod_value in sys.modules.items(): + for mod_value in list(sys.modules.values()): try: - filename = getattr(mod_value, '__file__', None) + lazyModule = False + try: + lazyModule = mod_value.__class__.__name__ == 'LazyImporter' + except: + lazyModule = False + filename = None + if not lazyModule: + filename = getattr(mod_value, '__file__', None) if filename is not None: try: fullpath = path.abspath(filename) @@ -2370,7 +2381,7 @@ def _excepthook(exc_type, exc_value, exc_tb): else: MODULES.append((filename, Module(fullpath))) except: - traceback.print_exc() + traceback.print_exc() if report: THREADS_LOCK.acquire() @@ -2409,7 +2420,7 @@ def detach_process(): global DETACHED DETACHED = True if not _INTERCEPTING_FOR_ATTACH: - if isinstance(sys.stdout, _DebuggerOutput): + if isinstance(sys.stdout, _DebuggerOutput): sys.stdout = sys.stdout.old_out if isinstance(sys.stderr, _DebuggerOutput): sys.stderr = sys.stderr.old_out @@ -2436,7 +2447,7 @@ def detach_threads(): THREADS_LOCK.acquire() THREADS.clear() THREADS_LOCK.release() - + BREAKPOINTS.clear() def new_thread(tid = None, set_break = False, frame = None): @@ -2444,7 +2455,7 @@ def new_thread(tid = None, set_break = False, frame = None): if tid == debugger_thread_id: return None - cur_thread = Thread(tid) + cur_thread = Thread(tid) THREADS_LOCK.acquire() THREADS[cur_thread.id] = cur_thread THREADS_LOCK.release() @@ -2498,11 +2509,11 @@ def __init__(self, old_out, is_stdout): def flush(self): if self.old_out: self.old_out.flush() - + def writelines(self, lines): for line in lines: self.write(line) - + @property def encoding(self): return 'utf8' @@ -2516,13 +2527,13 @@ def write(self, value): write_string(conn, value) if self.old_out: self.old_out.write(value) - + def isatty(self): return True def next(self): pass - + @property def name(self): if self.is_stdout: @@ -2547,7 +2558,7 @@ def write(self, data): write_string(conn, str_data) self.buffer.write(data) - def flush(self): + def flush(self): self.buffer.flush() def truncate(self, pos = None): @@ -2561,9 +2572,9 @@ def seek(self, pos, whence = 0): def is_same_py_file(file1, file2): """compares 2 filenames accounting for .pyc files""" - if file1.endswith('.pyc') or file1.endswith('.pyo'): + if file1.endswith('.pyc') or file1.endswith('.pyo'): file1 = file1[:-1] - if file2.endswith('.pyc') or file2.endswith('.pyo'): + if file2.endswith('.pyc') or file2.endswith('.pyo'): file2 = file2[:-1] return file1 == file2 @@ -2583,7 +2594,7 @@ def print_exception(exc_type, exc_value, exc_tb): print('Traceback (most recent call last):') for out in traceback.format_list(tb): sys.stderr.write(out) - + # print the exception for out in traceback.format_exception_only(exc_type, exc_value): sys.stdout.write(out) @@ -2595,9 +2606,14 @@ def parse_debug_options(s): # Accept current Process id to pass back to debugger def debug(file, port_num, debug_id, debug_options, currentPid, run_as = 'script'): # remove us from modules so there's no trace of us - sys.modules['$visualstudio_py_debugger'] = sys.modules['visualstudio_py_debugger'] - __name__ = '$visualstudio_py_debugger' - del sys.modules['visualstudio_py_debugger'] + if sys.modules.has_key('visualstudio_py_debugger'): + sys.modules['$visualstudio_py_debugger'] = sys.modules['visualstudio_py_debugger'] + __name__ = '$visualstudio_py_debugger' + del sys.modules['visualstudio_py_debugger'] + elif sys.modules.has_key('ptvsd.visualstudio_py_debugger'): + sys.modules['$ptvsd.visualstudio_py_debugger'] = sys.modules['ptvsd.visualstudio_py_debugger'] + __name__ = '$ptvsd.visualstudio_py_debugger' + del sys.modules['ptvsd.visualstudio_py_debugger'] wait_on_normal_exit = 'WaitOnNormalExit' in debug_options @@ -2621,6 +2637,8 @@ def debug(file, port_num, debug_id, debug_options, currentPid, run_as = 'script' elif run_as == 'code': exec_code(file, '', globals_obj) else: + # fix sys.path to be the script file dir + sys.path[0] = '' exec_file(file, globals_obj) finally: sys.settrace(None) @@ -2691,7 +2709,7 @@ def _get_source_django_18_or_lower(frame): else: if IGNORE_DJANGO_TEMPLATE_WARNINGS: return None - + if IS_DJANGO18: # The debug setting was changed since Django 1.8 print("WARNING: Template path is not available. Set the 'debug' option in the OPTIONS of a DjangoTemplates " @@ -2749,4 +2767,4 @@ def _get_template_line(frame): return _offset_to_line_number(_read_file(file_name), source[1][0]) except: return None -## End modification by Don Jayamanne \ No newline at end of file +## End modification by Don Jayamanne diff --git a/pythonFiles/PythonTools/ptvsd/visualstudio_py_repl.py b/pythonFiles/PythonTools/ptvsd/visualstudio_py_repl.py index 3618533910e1..35a4d810132e 100644 --- a/pythonFiles/PythonTools/ptvsd/visualstudio_py_repl.py +++ b/pythonFiles/PythonTools/ptvsd/visualstudio_py_repl.py @@ -39,7 +39,6 @@ import select import time import struct -import imp import traceback import random import os @@ -553,6 +552,7 @@ class BasicReplBackend(ReplBackend): """Basic back end which executes all Python code in-proc""" def __init__(self, mod_name='__main__'): import threading + import imp ReplBackend.__init__(self) if mod_name is not None: if sys.platform == 'cli': diff --git a/pythonFiles/PythonTools/ptvsd/visualstudio_py_util.py b/pythonFiles/PythonTools/ptvsd/visualstudio_py_util.py index c00519eebb09..88d173b2a1c3 100644 --- a/pythonFiles/PythonTools/ptvsd/visualstudio_py_util.py +++ b/pythonFiles/PythonTools/ptvsd/visualstudio_py_util.py @@ -22,7 +22,6 @@ # hasn't been loaded already, it will assume that the thread on which it is being loaded is the # main thread. This will cause issues when the thread goes away after attach completes. -import imp import os import sys import struct @@ -68,6 +67,7 @@ def exec_code(code, file, global_variables): ``sys.path[0]`` will be changed to the value of `file` without the filename. Both values are restored when this function exits. ''' + import imp original_main = sys.modules.get('__main__') global_variables = dict(global_variables) @@ -510,14 +510,14 @@ def re_test(source, pattern): d1 = {} d1_key = 'a' * self.maxstring_inner * 2 d1[d1_key] = d1_key - re_test(d1, "{'a+\.\.\.a+': 'a+\.\.\.a+'}") + re_test(d1, r"{'a+\.\.\.a+': 'a+\.\.\.a+'}") d2 = {d1_key : d1} - re_test(d2, "{'a+\.\.\.a+': {'a+\.\.\.a+': 'a+\.\.\.a+'}}") + re_test(d2, r"{'a+\.\.\.a+': {'a+\.\.\.a+': 'a+\.\.\.a+'}}") d3 = {d1_key : d2} if len(self.maxcollection) == 2: - re_test(d3, "{'a+\.\.\.a+': {'a+\.\.\.a+': {\.\.\.}}}") + re_test(d3, r"{'a+\.\.\.a+': {'a+\.\.\.a+': {\.\.\.}}}") else: - re_test(d3, "{'a+\.\.\.a+': {'a+\.\.\.a+': {'a+\.\.\.a+': 'a+\.\.\.a+'}}}") + re_test(d3, r"{'a+\.\.\.a+': {'a+\.\.\.a+': {'a+\.\.\.a+': 'a+\.\.\.a+'}}}") # Ensure empty dicts work test({}, '{}') diff --git a/pythonFiles/PythonTools/visualstudio_ipython_repl.py b/pythonFiles/PythonTools/visualstudio_ipython_repl.py index 14f42362a8f8..0e2b8ca056ff 100644 --- a/pythonFiles/PythonTools/visualstudio_ipython_repl.py +++ b/pythonFiles/PythonTools/visualstudio_ipython_repl.py @@ -21,8 +21,8 @@ import re import sys -from visualstudio_py_repl import BasicReplBackend, ReplBackend, UnsupportedReplException, _command_line_to_args_list -from visualstudio_py_util import to_bytes +from ptvsd.visualstudio_py_repl import BasicReplBackend, ReplBackend, UnsupportedReplException, _command_line_to_args_list +from ptvsd.visualstudio_py_util import to_bytes try: import thread except: @@ -401,7 +401,7 @@ def init_debugger(self): def __visualstudio_debugger_init(): import sys sys.path.append(''' + repr(path.dirname(__file__)) + ''') - import visualstudio_py_debugger + import ptvsd.visualstudio_py_debugger as visualstudio_py_debugger new_thread = visualstudio_py_debugger.new_thread() sys.settrace(new_thread.trace_func) visualstudio_py_debugger.intercept_threads(True) @@ -413,7 +413,7 @@ def __visualstudio_debugger_init(): def attach_process(self, port, debugger_id): self.run_command(''' def __visualstudio_debugger_attach(): - import visualstudio_py_debugger + import ptvsd.visualstudio_py_debugger as visual_studio_py_debugger def do_detach(): visualstudio_py_debugger.DETACH_CALLBACKS.remove(do_detach) @@ -427,4 +427,4 @@ def do_detach(): class IPythonBackendWithoutPyLab(IPythonBackend): def get_extra_arguments(self): - return [] \ No newline at end of file + return [] diff --git a/pythonFiles/PythonTools/visualstudio_py_debugger.py b/pythonFiles/PythonTools/visualstudio_py_debugger.py index c0a603c077e5..b2db85fcae17 100644 --- a/pythonFiles/PythonTools/visualstudio_py_debugger.py +++ b/pythonFiles/PythonTools/visualstudio_py_debugger.py @@ -47,12 +47,12 @@ try: # In the local attach scenario, visualstudio_py_util is injected into globals() # by PyDebugAttach before loading this module, and cannot be imported. - _vspu = visualstudio_py_util + _vspu = ptvsd.visualstudio_py_util except: try: - import visualstudio_py_util as _vspu - except ImportError: import ptvsd.visualstudio_py_util as _vspu + except ImportError: + import visualstudio_py_util as _vspu to_bytes = _vspu.to_bytes exec_file = _vspu.exec_file @@ -69,15 +69,17 @@ try: # In the local attach scenario, visualstudio_py_repl is injected into globals() # by PyDebugAttach before loading this module, and cannot be imported. - _vspr = visualstudio_py_repl + _vspr = ptvsd.visualstudio_py_repl except: try: - import visualstudio_py_repl as _vspr - except ImportError: import ptvsd.visualstudio_py_repl as _vspr + except ImportError: + import visualstudio_py_repl as _vspr + try: import stackless + stackless.tasklet # work-around lazy on-demand importers except ImportError: stackless = None @@ -1312,7 +1314,8 @@ def unblock(self): assert self._is_blocked assert self.id != thread.get_ident() # only someone else should unblock us - self._block_lock.release() + if self._block_lock.locked(): + self._block_lock.release() def schedule_work(self, work): self.unblock_work = work @@ -2360,9 +2363,16 @@ def _excepthook(exc_type, exc_value, exc_tb): global debugger_thread_id debugger_thread_id = _start_new_thread(DebuggerLoop(conn).loop, ()) - for mod_name, mod_value in sys.modules.items(): + for mod_value in list(sys.modules.values()): try: - filename = getattr(mod_value, '__file__', None) + lazyModule = False + try: + lazyModule = mod_value.__class__.__name__ == 'LazyImporter' + except: + lazyModule = False + filename = None + if not lazyModule: + filename = getattr(mod_value, '__file__', None) if filename is not None: try: fullpath = path.abspath(filename) @@ -2371,7 +2381,7 @@ def _excepthook(exc_type, exc_value, exc_tb): else: MODULES.append((filename, Module(fullpath))) except: - traceback.print_exc() + traceback.print_exc() if report: THREADS_LOCK.acquire() @@ -2596,9 +2606,14 @@ def parse_debug_options(s): # Accept current Process id to pass back to debugger def debug(file, port_num, debug_id, debug_options, currentPid, run_as = 'script'): # remove us from modules so there's no trace of us - sys.modules['$visualstudio_py_debugger'] = sys.modules['visualstudio_py_debugger'] - __name__ = '$visualstudio_py_debugger' - del sys.modules['visualstudio_py_debugger'] + if sys.modules.has_key('visualstudio_py_debugger'): + sys.modules['$visualstudio_py_debugger'] = sys.modules['visualstudio_py_debugger'] + __name__ = '$visualstudio_py_debugger' + del sys.modules['visualstudio_py_debugger'] + elif sys.modules.has_key('ptvsd.visualstudio_py_debugger'): + sys.modules['$ptvsd.visualstudio_py_debugger'] = sys.modules['ptvsd.visualstudio_py_debugger'] + __name__ = '$ptvsd.visualstudio_py_debugger' + del sys.modules['ptvsd.visualstudio_py_debugger'] wait_on_normal_exit = 'WaitOnNormalExit' in debug_options @@ -2622,6 +2637,8 @@ def debug(file, port_num, debug_id, debug_options, currentPid, run_as = 'script' elif run_as == 'code': exec_code(file, '', globals_obj) else: + # fix sys.path to be the script file dir + sys.path[0] = '' exec_file(file, globals_obj) finally: sys.settrace(None) @@ -2750,4 +2767,4 @@ def _get_template_line(frame): return _offset_to_line_number(_read_file(file_name), source[1][0]) except: return None -## End modification by Don Jayamanne \ No newline at end of file +## End modification by Don Jayamanne diff --git a/pythonFiles/PythonTools/visualstudio_py_launcher.py b/pythonFiles/PythonTools/visualstudio_py_launcher.py index 12b2140c4f72..38f7a0bd53ac 100644 --- a/pythonFiles/PythonTools/visualstudio_py_launcher.py +++ b/pythonFiles/PythonTools/visualstudio_py_launcher.py @@ -1,16 +1,16 @@ # Python Tools for Visual Studio # Copyright(c) Microsoft Corporation # All rights reserved. -# +# # Licensed under the Apache License, Version 2.0 (the License); you may not use # this file except in compliance with the License. You may obtain a copy of the # License at http://www.apache.org/licenses/LICENSE-2.0 -# +# # THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS # OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY # IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, # MERCHANTABLITY OR NON-INFRINGEMENT. -# +# # See the Apache Version 2.0 License for specific language governing # permissions and limitations under the License. @@ -23,70 +23,72 @@ __author__ = "Microsoft Corporation " __version__ = "3.0.0.0" -import os -import os.path -import sys -import traceback -try: - import visualstudio_py_debugger as vspd -except: - traceback.print_exc() - print(''' -Internal error detected. Please copy the above traceback and report at -http://go.microsoft.com/fwlink/?LinkId=293415 - -Press Enter to close. . .''') +def launch(): + import os + import os.path + import sys + import traceback try: - raw_input() - except NameError: - input() - sys.exit(1) + import ptvsd.visualstudio_py_debugger as vspd + except: + traceback.print_exc() + print(''' + Internal error detected. Please copy the above traceback and report at + http://go.microsoft.com/fwlink/?LinkId=293415 + + Press Enter to close. . .''') + try: + raw_input() + except NameError: + input() + sys.exit(1) + + # Arguments are: + # 1. Working directory. + # 2. VS debugger port to connect to. + # 3. GUID for the debug session. + # 4. Debug options (as integer - see enum PythonDebugOptions). + # 5. '-m' or '-c' to override the default run-as mode. [optional] + # 6. Startup script name. + # 7. Script arguments. -# Arguments are: -# 1. Working directory. -# 2. VS debugger port to connect to. -# 3. GUID for the debug session. -# 4. Debug options (as integer - see enum PythonDebugOptions). -# 5. '-m' or '-c' to override the default run-as mode. [optional] -# 6. Startup script name. -# 7. Script arguments. + # change to directory we expected to start from + os.chdir(sys.argv[1]) -# change to directory we expected to start from -os.chdir(sys.argv[1]) + port_num = int(sys.argv[2]) + debug_id = sys.argv[3] + debug_options = vspd.parse_debug_options(sys.argv[4]) + del sys.argv[0:5] -port_num = int(sys.argv[2]) -debug_id = sys.argv[3] -debug_options = vspd.parse_debug_options(sys.argv[4]) -del sys.argv[0:5] + # set run_as mode appropriately + run_as = 'script' + if sys.argv and sys.argv[0] == '-m': + run_as = 'module' + del sys.argv[0] + if sys.argv and sys.argv[0] == '-c': + run_as = 'code' + del sys.argv[0] -# set run_as mode appropriately -run_as = 'script' -if sys.argv and sys.argv[0] == '-m': - run_as = 'module' - del sys.argv[0] -if sys.argv and sys.argv[0] == '-c': - run_as = 'code' - del sys.argv[0] + # preserve filename before we del sys + filename = sys.argv[0] -# preserve filename before we del sys -filename = sys.argv[0] + # exclude ourselves from being debugged + vspd.DONT_DEBUG.append(os.path.normcase(__file__)) -# fix sys.path to be the script file dir -sys.path[0] = '' + ## Begin modification by Don Jayamanne + # Get current Process id to pass back to debugger + currentPid = os.getpid() + ## End Modification by Don Jayamanne -# exclude ourselves from being debugged -vspd.DONT_DEBUG.append(os.path.normcase(__file__)) + # remove all state we imported + del sys, os -## Begin modification by Don Jayamanne -# Get current Process id to pass back to debugger -currentPid = os.getpid() -## End Modification by Don Jayamanne + # and start debugging + ## Begin modification by Don Jayamanne + # Pass current Process id to pass back to debugger + vspd.debug(filename, port_num, debug_id, debug_options, currentPid, run_as) + ## End Modification by Don Jayamanne -# remove all state we imported -del sys, os -# and start debugging -## Begin modification by Don Jayamanne -# Pass current Process id to pass back to debugger -vspd.debug(filename, port_num, debug_id, debug_options, currentPid, run_as) -## End Modification by Don Jayamanne +if __name__ == "__main__": + launch() diff --git a/pythonFiles/PythonTools/visualstudio_py_launcher_nodebug.py b/pythonFiles/PythonTools/visualstudio_py_launcher_nodebug.py index 7285d6d8558b..0ada07cba53a 100644 --- a/pythonFiles/PythonTools/visualstudio_py_launcher_nodebug.py +++ b/pythonFiles/PythonTools/visualstudio_py_launcher_nodebug.py @@ -9,7 +9,7 @@ import time import socket try: - import visualstudio_py_util as _vspu + import ptvsd.visualstudio_py_util as _vspu except: traceback.print_exc() print("""Internal error detected. Please copy the above traceback and report at diff --git a/pythonFiles/PythonTools/visualstudio_py_repl.py b/pythonFiles/PythonTools/visualstudio_py_repl.py index 74870fd696b3..dac8a2efc3f4 100644 --- a/pythonFiles/PythonTools/visualstudio_py_repl.py +++ b/pythonFiles/PythonTools/visualstudio_py_repl.py @@ -39,7 +39,6 @@ import select import time import struct -import imp import traceback import random import os @@ -50,12 +49,12 @@ try: # In the local attach scenario, visualstudio_py_util is injected into globals() # by PyDebugAttach before loading this module, and cannot be imported. - _vspu = visualstudio_py_util + _vspu = ptvsd.visualstudio_py_util except: try: - import visualstudio_py_util as _vspu - except ImportError: import ptvsd.visualstudio_py_util as _vspu + except ImportError: + import visualstudio_py_util as _vspu to_bytes = _vspu.to_bytes read_bytes = _vspu.read_bytes read_int = _vspu.read_int @@ -351,7 +350,7 @@ def _cmd_excx(self): self.execute_file_ex(filetype, filename, args) def _cmd_debug_attach(self): - import visualstudio_py_debugger + import ptvsd.visualstudio_py_debugger as visualstudio_py_debugger port = read_int(self.conn) id = read_string(self.conn) debug_options = visualstudio_py_debugger.parse_debug_options(read_string(self.conn)) @@ -385,7 +384,7 @@ def on_debugger_detach(self): def init_debugger(self): from os import path sys.path.append(path.dirname(__file__)) - import visualstudio_py_debugger + import ptvsd.visualstudio_py_debugger as visualstudio_py_debugger visualstudio_py_debugger.DONT_DEBUG.append(path.normcase(__file__)) new_thread = visualstudio_py_debugger.new_thread() sys.settrace(new_thread.trace_func) @@ -553,6 +552,7 @@ class BasicReplBackend(ReplBackend): """Basic back end which executes all Python code in-proc""" def __init__(self, mod_name='__main__'): import threading + import imp ReplBackend.__init__(self) if mod_name is not None: if sys.platform == 'cli': @@ -1005,13 +1005,13 @@ def flush(self): sys.stdout.flush() def do_detach(self): - import visualstudio_py_debugger + import ptvsd.visualstudio_py_debugger as visualstudio_py_debugger visualstudio_py_debugger.DETACH_CALLBACKS.remove(self.do_detach) self.on_debugger_detach() def attach_process(self, port, debugger_id, debug_options): def execute_attach_process_work_item(): - import visualstudio_py_debugger + import ptvsd.visualstudio_py_debugger as visualstudio_py_debugger visualstudio_py_debugger.DETACH_CALLBACKS.append(self.do_detach) visualstudio_py_debugger.attach_process(port, debugger_id, debug_options, report=True, block=True) @@ -1388,4 +1388,4 @@ def _run_repl(): _debug_write(traceback.format_exc()) _debug_write('exiting') input() - raise \ No newline at end of file + raise diff --git a/pythonFiles/PythonTools/visualstudio_py_testlauncher.py b/pythonFiles/PythonTools/visualstudio_py_testlauncher.py index 9464afddc6f1..270d87be3399 100644 --- a/pythonFiles/PythonTools/visualstudio_py_testlauncher.py +++ b/pythonFiles/PythonTools/visualstudio_py_testlauncher.py @@ -1,16 +1,16 @@ # Python Tools for Visual Studio # Copyright(c) Microsoft Corporation # All rights reserved. -# +# # Licensed under the Apache License, Version 2.0 (the License); you may not use # this file except in compliance with the License. You may obtain a copy of the # License at http://www.apache.org/licenses/LICENSE-2.0 -# +# # THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS # OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY # IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, # MERCHANTABLITY OR NON-INFRINGEMENT. -# +# # See the Apache Version 2.0 License for specific language governing # permissions and limitations under the License. @@ -43,11 +43,11 @@ def __init__(self, old_out, is_stdout): def flush(self): if self.old_out: self.old_out.flush() - + def writelines(self, lines): for line in lines: self.write(line) - + @property def encoding(self): return 'utf8' @@ -58,13 +58,13 @@ def write(self, value): self.old_out.write(value) # flush immediately, else things go wonky and out of order self.flush() - + def isatty(self): return True def next(self): pass - + @property def name(self): if self.is_stdout: @@ -84,7 +84,7 @@ def write(self, data): _channel.send_event('stdout' if self.is_stdout else 'stderr', content=data) self.buffer.write(data) - def flush(self): + def flush(self): self.buffer.flush() def truncate(self, pos = None): @@ -102,12 +102,20 @@ def __init__(self, socket, callback): self.seq = 0 self.callback = callback self.lock = thread.allocate_lock() + self._closed = False # start the testing reader thread loop self.test_thread_id = thread.start_new_thread(self.readSocket, ()) + def close(self): + self._closed = True + def readSocket(self): - data = self.socket.recv(1024) - self.callback() + try: + data = self.socket.recv(1024) + self.callback() + except OSError: + if not self._closed: + raise def receive(self): pass @@ -129,7 +137,7 @@ def startTest(self, test): super(VsTestResult, self).startTest(test) if _channel is not None: _channel.send_event( - name='start', + name='start', test = test.id() ) @@ -169,7 +177,7 @@ def sendResult(self, test, outcome, trace = None): tb = ''.join(formatted) message = str(trace[1]) _channel.send_event( - name='result', + name='result', outcome=outcome, traceback = tb, message = message, @@ -188,7 +196,7 @@ def stopTests(): class ExitCommand(Exception): pass -def signal_handler(signal, frame): +def signal_handler(signal, frame): raise ExitCommand() def main(): @@ -215,7 +223,7 @@ def main(): if opts.debug: from ptvsd.visualstudio_py_debugger import DONT_DEBUG, DEBUG_ENTRYPOINTS, get_code - + sys.path[0] = os.getcwd() if opts.result_port: try: @@ -235,7 +243,7 @@ def main(): pass elif opts.mixed_mode: - # For mixed-mode attach, there's no ptvsd and hence no wait_for_attach(), + # For mixed-mode attach, there's no ptvsd and hence no wait_for_attach(), # so we have to use Win32 API in a loop to do the same thing. from time import sleep from ctypes import windll, c_char @@ -270,48 +278,51 @@ def main(): opts.up = 'test*.py' tests = unittest.defaultTestLoader.discover(opts.us, opts.up) else: - # loadTestsFromNames doesn't work well (with duplicate file names or class names) + # loadTestsFromNames doesn't work well (with duplicate file names or class names) # Easier approach is find the test suite and use that for running loader = unittest.TestLoader() # opts.us will be passed in suites = loader.discover(opts.us, pattern=os.path.basename(opts.testFile)) suite = None - tests = None + tests = None if opts.tests is None: # Run everything in the test file tests = suites else: # Run a specific test class or test method - for suite in suites._tests: - for cls in suite._tests: + for test_suite in suites._tests: + for cls in test_suite._tests: try: for m in cls._tests: testId = m.id() if testId.startswith(opts.tests[0]): suite = cls + break if testId == opts.tests[0]: tests = m break except Exception as err: - errorMessage = traceback.format_exception() + errorMessage = traceback.format_exception() pass if tests is None: tests = suite if tests is None and suite is None: _channel.send_event( - name='error', + name='error', outcome='', traceback = '', message = 'Failed to identify the test', test = '' ) if opts.uvInt is None: - opts.uvInt = 0 + opts.uvInt = 0 if opts.uf is not None: runner = unittest.TextTestRunner(verbosity=opts.uvInt, resultclass=VsTestResult, failfast=True) else: runner = unittest.TextTestRunner(verbosity=opts.uvInt, resultclass=VsTestResult) result = runner.run(tests) + if _channel is not None: + _channel.close() sys.exit(not result.wasSuccessful()) finally: if cov is not None: diff --git a/pythonFiles/PythonTools/visualstudio_py_util.py b/pythonFiles/PythonTools/visualstudio_py_util.py index b3ed951e8718..58b798821750 100644 --- a/pythonFiles/PythonTools/visualstudio_py_util.py +++ b/pythonFiles/PythonTools/visualstudio_py_util.py @@ -22,7 +22,6 @@ # hasn't been loaded already, it will assume that the thread on which it is being loaded is the # main thread. This will cause issues when the thread goes away after attach completes. -import imp import os import sys import struct @@ -68,6 +67,7 @@ def exec_code(code, file, global_variables): ``sys.path[0]`` will be changed to the value of `file` without the filename. Both values are restored when this function exits. ''' + import imp original_main = sys.modules.get('__main__') global_variables = dict(global_variables) @@ -510,14 +510,14 @@ def re_test(source, pattern): d1 = {} d1_key = 'a' * self.maxstring_inner * 2 d1[d1_key] = d1_key - re_test(d1, "{'a+\.\.\.a+': 'a+\.\.\.a+'}") + re_test(d1, r"{'a+\.\.\.a+': 'a+\.\.\.a+'}") d2 = {d1_key : d1} - re_test(d2, "{'a+\.\.\.a+': {'a+\.\.\.a+': 'a+\.\.\.a+'}}") + re_test(d2, r"{'a+\.\.\.a+': {'a+\.\.\.a+': 'a+\.\.\.a+'}}") d3 = {d1_key : d2} if len(self.maxcollection) == 2: - re_test(d3, "{'a+\.\.\.a+': {'a+\.\.\.a+': {\.\.\.}}}") + re_test(d3, r"{'a+\.\.\.a+': {'a+\.\.\.a+': {\.\.\.}}}") else: - re_test(d3, "{'a+\.\.\.a+': {'a+\.\.\.a+': {'a+\.\.\.a+': 'a+\.\.\.a+'}}}") + re_test(d3, r"{'a+\.\.\.a+': {'a+\.\.\.a+': {'a+\.\.\.a+': 'a+\.\.\.a+'}}}") # Ensure empty dicts work test({}, '{}') diff --git a/pythonFiles/completion.py b/pythonFiles/completion.py index e530be32b367..5ab933715689 100644 --- a/pythonFiles/completion.py +++ b/pythonFiles/completion.py @@ -33,6 +33,7 @@ class JediCompletion(object): def __init__(self): self.default_sys_path = sys.path + self.environment = jedi.api.environment.Environment(sys.prefix, sys.executable) self._input = io.open(sys.stdin.fileno(), encoding='utf-8') if (os.path.sep == '/') and (platform.uname()[2].find('Microsoft') > -1): # WSL; does not support UNC paths @@ -150,7 +151,7 @@ def _get_call_signatures_with_args(self, script): except Exception: sig["docstring"] = '' sig["raw_docstring"] = '' - + sig["name"] = signature.name sig["paramindex"] = signature.index sig["bracketstart"].append(signature.index) @@ -199,7 +200,7 @@ def _serialize_completions(self, script, identifier=None, prefix=''): } _completion['description'] = '' _completion['raw_docstring'] = '' - + # we pass 'text' here only for fuzzy matcher if value: _completion['snippet'] = '%s=${1:%s}$0' % (name, value) @@ -223,7 +224,7 @@ def _serialize_completions(self, script, identifier=None, prefix=''): 'type': self._get_definition_type(completion), 'raw_type': completion.type, 'rightLabel': self._additional_info(completion) - } + } except Exception: continue @@ -231,7 +232,7 @@ def _serialize_completions(self, script, identifier=None, prefix=''): if c['text'] == _completion['text']: c['type'] = _completion['type'] c['raw_type'] = _completion['raw_type'] - + if any([c['text'].split('=')[0] == _completion['text'] for c in _completions]): # ignore function arguments we already have @@ -361,7 +362,7 @@ def _get_definitionsx(self, definitions, identifier=None, ignoreNoModulePath=Fal definition = self._top_definition(definition) definitionRange = { 'start_line': 0, - 'start_column': 0, + 'start_column': 0, 'end_line': 0, 'end_column': 0 } @@ -377,7 +378,7 @@ def _get_definitionsx(self, definitions, identifier=None, ignoreNoModulePath=Fal container = parent.name if parent.type != 'module' else '' except Exception: container = '' - + try: docstring = definition.docstring() rawdocstring = definition.docstring(raw=True) @@ -424,7 +425,7 @@ def _serialize_definitions(self, definitions, identifier=None): container = parent.name if parent.type != 'module' else '' except Exception: container = '' - + try: docstring = definition.docstring() rawdocstring = definition.docstring(raw=True) @@ -474,7 +475,7 @@ def _serialize_tooltip(self, definitions, identifier=None): 'type': self._get_definition_type(definition), 'text': definition.name, 'description': description, - 'docstring': description, + 'docstring': description, 'signature': signature } _definitions.append(_definition) @@ -551,7 +552,7 @@ def _process_request(self, request): self._normalize_request_path(request) path = self._get_top_level_module(request.get('path', '')) - if path not in sys.path: + if len(path) > 0 and path not in sys.path: sys.path.insert(0, path) lookup = request.get('lookup', 'completions') @@ -563,26 +564,13 @@ def _process_request(self, request): all_scopes=True), request['id']) - script = jedi.api.Script( + script = jedi.Script( source=request.get('source', None), line=request['line'] + 1, - column=request['column'], path=request.get('path', '')) - + column=request['column'], path=request.get('path', ''), + sys_path=sys.path, environment=self.environment) + if lookup == 'definitions': - defs = [] - try: - defs = self._get_definitionsx(script.goto_assignments(follow_imports=False), request['id']) - except: - pass - try: - if len(defs) == 0: - defs = self._get_definitionsx(script.goto_definitions(), request['id']) - except: - pass - try: - if len(defs) == 0: - defs = self._get_definitionsx(script.goto_assignments(), request['id']) - except: - pass + defs = self._get_definitionsx(script.goto_assignments(follow_imports=True), request['id']) return json.dumps({'id': request['id'], 'results': defs}) if lookup == 'tooltip': if jediPreview: @@ -624,7 +612,7 @@ def watch(self): try: rq = self._input.readline() if len(rq) == 0: - # Reached EOF - indication our parent process is gone. + # Reached EOF - indication our parent process is gone. sys.stderr.write('Received EOF from the standard input,exiting' + '\n') sys.stderr.flush() return diff --git a/pythonFiles/completionServer.py b/pythonFiles/completionServer.py index 9c380e493f73..d4defc59fe82 100644 --- a/pythonFiles/completionServer.py +++ b/pythonFiles/completionServer.py @@ -28,7 +28,7 @@ # http://pydoc.net/Python/magni/1.4.0/magni.tests.ipynb_examples/ # http://www.xavierdupre.fr/app/pyquickhelper/helpsphinx/_modules/pyquickhelper/ipythonhelper/notebook_runner.html -import visualstudio_py_util as _vspu +import ptvsd.visualstudio_py_util as _vspu to_bytes = _vspu.to_bytes read_bytes = _vspu.read_bytes diff --git a/pythonFiles/experimental/ptvsd_launcher.py b/pythonFiles/experimental/ptvsd_launcher.py deleted file mode 100644 index 98c4c5525bcf..000000000000 --- a/pythonFiles/experimental/ptvsd_launcher.py +++ /dev/null @@ -1,96 +0,0 @@ -# Python Tools for Visual Studio -# Copyright(c) Microsoft Corporation -# All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the License); you may not use -# this file except in compliance with the License. You may obtain a copy of the -# License at http://www.apache.org/licenses/LICENSE-2.0 -# -# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS -# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY -# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -# MERCHANTABLITY OR NON-INFRINGEMENT. -# -# See the Apache Version 2.0 License for specific language governing -# permissions and limitations under the License. - -""" -Starts Debugging, expected to start with normal program -to start as first argument and directory to run from as -the second argument. -""" - -__author__ = "Microsoft Corporation " -__version__ = "3.2.0.0" - -import os -import os.path -import sys -import traceback - -# Arguments are: -# 1. Working directory. -# 2. VS debugger port to connect to. -# 3. GUID for the debug session. -# 4. Debug options (as list of names - see enum PythonDebugOptions). -# 5. '-g' to use the installed ptvsd package, rather than bundled one. -# 6. '-m' or '-c' to override the default run-as mode. [optional] -# 7. Startup script name. -# 8. Script arguments. - -# change to directory we expected to start from -os.chdir(sys.argv[1]) - -port_num = int(sys.argv[2]) -debug_id = sys.argv[3] -debug_options = set([opt.strip() for opt in sys.argv[4].split(',')]) - -del sys.argv[0:5] - -# Use bundled ptvsd or not? -bundled_ptvsd = True -if sys.argv and sys.argv[0] == '-g': - bundled_ptvsd = False - del sys.argv[0] - -# set run_as mode appropriately -run_as = 'script' -if sys.argv and sys.argv[0] == '-m': - run_as = 'module' - del sys.argv[0] -if sys.argv and sys.argv[0] == '-c': - run_as = 'code' - del sys.argv[0] - -# preserve filename before we del sys -filename = sys.argv[0] - -# fix sys.path to be the script file dir -sys.path[0] = '' - -# Load the debugger package -try: - if bundled_ptvsd: - ptvs_lib_path = os.path.dirname(__file__) - sys.path.insert(0, ptvs_lib_path) - import ptvsd - import ptvsd.debugger as vspd - vspd.DONT_DEBUG.append(os.path.normcase(__file__)) -except: - traceback.print_exc() - print(''' -Internal error detected. Please copy the above traceback and report at -https://go.microsoft.com/fwlink/?LinkId=293415 - -Press Enter to close. . .''') - try: - raw_input() - except NameError: - input() - sys.exit(1) -finally: - if bundled_ptvsd: - sys.path.remove(ptvs_lib_path) - -# and start debugging -vspd.debug(filename, port_num, debug_id, debug_options, run_as) diff --git a/pythonFiles/experimental/testlauncher.py b/pythonFiles/experimental/testlauncher.py new file mode 100644 index 000000000000..e8c860ce133d --- /dev/null +++ b/pythonFiles/experimental/testlauncher.py @@ -0,0 +1,64 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import sys + + +def parse_argv(): + """Parses arguments for use with the test launcher. + Arguments are: + 1. Working directory. + 2. Test runner, `pytest` or `nose` + 3. Rest of the arguments are passed into the test runner. + """ + + return (sys.argv[1], sys.argv[2], sys.argv[3:]) + + +def exclude_current_file_from_debugger(): + # Load the debugger package + try: + import ptvsd + import ptvsd.debugger as vspd + vspd.DONT_DEBUG.append(os.path.normcase(__file__)) + except: + traceback.print_exc() + print(''' +Internal error detected. Please copy the above traceback and report at +https://github.com/Microsoft/vscode-python/issues/new + +Press Enter to close. . .''') + try: + raw_input() + except NameError: + input() + sys.exit(1) + + +def run(cwd, testRunner, args): + """Runs the test + cwd -- the current directory to be set + testRuner -- test runner to be used `pytest` or `nose` + args -- arguments passed into the test runner + """ + + sys.path[0] = os.getcwd() + os.chdir(cwd) + + try: + if testRunner == 'pytest': + import pytest + pytest.main(args) + else: + import nose + nose.run(argv=args) + sys.exit(0) + finally: + pass + + +if __name__ == '__main__': + exclude_current_file_from_debugger() + cwd, testRunner, args = parse_argv() + run(cwd, testRunner, args) diff --git a/pythonFiles/interpreterInfo.py b/pythonFiles/interpreterInfo.py new file mode 100644 index 000000000000..4822594bd046 --- /dev/null +++ b/pythonFiles/interpreterInfo.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import sys + +obj = {} +obj["versionInfo"] = sys.version_info[:4] +obj["sysPrefix"] = sys.prefix +obj["version"] = sys.version +obj["is64Bit"] = sys.maxsize > 2**32 + +print(json.dumps(obj)) diff --git a/pythonFiles/isort/__init__.py b/pythonFiles/isort/__init__.py index 3063d1ed92d8..4f1adaa05bf0 100644 --- a/pythonFiles/isort/__init__.py +++ b/pythonFiles/isort/__init__.py @@ -22,7 +22,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals -from . import settings -from .isort import SortImports +from . import settings # noqa: F401 +from .isort import SortImports # noqa: F401 -__version__ = "4.2.15" +__version__ = "4.3.4" diff --git a/pythonFiles/isort/__main__.py b/pythonFiles/isort/__main__.py index 94b1d057bb0c..186c98e85ea7 100644 --- a/pythonFiles/isort/__main__.py +++ b/pythonFiles/isort/__main__.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + from isort.main import main main() diff --git a/pythonFiles/isort/isort.py b/pythonFiles/isort/isort.py index cecd5af991a1..3ba3e7d19d9e 100644 --- a/pythonFiles/isort/isort.py +++ b/pythonFiles/isort/isort.py @@ -32,7 +32,8 @@ import os import re import sys -from collections import namedtuple +import sysconfig +from collections import OrderedDict, namedtuple from datetime import datetime from difflib import unified_diff from fnmatch import fnmatch @@ -40,7 +41,7 @@ from . import settings from .natural import nsorted -from .pie_slice import OrderedDict, OrderedSet, input, itemsview +from .pie_slice import OrderedSet, input, itemsview KNOWN_SECTION_MAPPING = { 'STDLIB': 'STANDARD_LIBRARY', @@ -87,6 +88,8 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch indent = "\t" self.config['indent'] = indent + self.config['comment_prefix'] = self.config['comment_prefix'].strip("'").strip('"') + self.place_imports = {} self.import_placements = {} self.remove_imports = [self._format_simplified(removal) for removal in self.config['remove_imports']] @@ -108,13 +111,24 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch elif not file_contents: self.file_path = file_path self.file_encoding = coding_check(file_path) - with io.open(file_path, encoding=self.file_encoding) as file_to_import_sort: + with io.open(file_path, encoding=self.file_encoding, newline='') as file_to_import_sort: file_contents = file_to_import_sort.read() if file_contents is None or ("isort:" + "skip_file") in file_contents: + self.skipped = True + self.output = None return - self.in_lines = file_contents.split("\n") + if self.config['line_ending']: + self.line_separator = self.config['line_ending'] + else: + if '\r\n' in file_contents: + self.line_separator = '\r\n' + elif '\r' in file_contents: + self.line_separator = '\r' + else: + self.line_separator = '\n' + self.in_lines = file_contents.split(self.line_separator) self.original_length = len(self.in_lines) if (self.original_length > 1 or self.in_lines[:1] not in ([], [""])) or self.config['force_adds']: for add_import in self.add_imports: @@ -147,22 +161,20 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch self._parse() if self.import_index != -1: self._add_formatted_imports() - self.length_change = len(self.out_lines) - self.original_length while self.out_lines and self.out_lines[-1].strip() == "": self.out_lines.pop(-1) self.out_lines.append("") - - self.output = "\n".join(self.out_lines) + self.output = self.line_separator.join(self.out_lines) if self.config['atomic']: try: - compile(self._strip_top_comments(self.out_lines), self.file_path, 'exec', 0, 1) + compile(self._strip_top_comments(self.out_lines, self.line_separator), self.file_path, 'exec', 0, 1) except SyntaxError: self.output = file_contents self.incorrectly_sorted = True try: - compile(self._strip_top_comments(self.in_lines), self.file_path, 'exec', 0, 1) - print("ERROR: {0} isort would have introduced syntax errors, please report to the project!". \ + compile(self._strip_top_comments(self.in_lines, self.line_separator), self.file_path, 'exec', 0, 1) + print("ERROR: {0} isort would have introduced syntax errors, please report to the project!". format(self.file_path)) except SyntaxError: print("ERROR: {0} File contains syntax errors.".format(self.file_path)) @@ -172,8 +184,8 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch check_output = self.output check_against = file_contents if self.config['ignore_whitespace']: - check_output = check_output.replace("\n", "").replace(" ", "") - check_against = check_against.replace("\n", "").replace(" ", "") + check_output = check_output.replace(self.line_separator, "").replace(" ", "") + check_against = check_against.replace(self.line_separator, "").replace(" ", "") if check_output == check_against: if self.config['verbose']: @@ -187,18 +199,20 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch elif write_to_stdout: sys.stdout.write(self.output) elif file_name and not check: + if self.output == file_contents: + return + if ask_to_apply: - if self.output == file_contents: - return self._show_diff(file_contents) answer = None while answer not in ('yes', 'y', 'no', 'n', 'quit', 'q'): - answer = input("Apply suggested changes to '{0}' [y/n/q]?".format(self.file_path)).lower() + answer = input("Apply suggested changes to '{0}' [y/n/q]? ".format(self.file_path)).lower() if answer in ('no', 'n'): return if answer in ('quit', 'q'): sys.exit(1) - with io.open(self.file_path, encoding=self.file_encoding, mode='w') as output_file: + with io.open(self.file_path, encoding=self.file_encoding, mode='w', newline='') as output_file: + print("Fixing {0}".format(self.file_path)) output_file.write(self.output) def _show_diff(self, file_contents): @@ -214,12 +228,12 @@ def _show_diff(self, file_contents): sys.stdout.write(line) @staticmethod - def _strip_top_comments(lines): + def _strip_top_comments(lines, line_separator): """Strips # comments that exist at the top of the given lines""" lines = copy.copy(lines) while lines and lines[0].startswith("#"): lines = lines[1:] - return "\n".join(lines) + return line_separator.join(lines) def place_module(self, module_name): """Tries to determine if a module is a python std import, third party import, or project code: @@ -261,13 +275,12 @@ def place_module(self, module_name): virtual_env_src = '{0}/src/'.format(virtual_env) # handle case-insensitive paths on windows - stdlib_lib_prefix = os.path.normcase(get_stdlib_path()) + stdlib_lib_prefix = os.path.normcase(sysconfig.get_paths()['stdlib']) for prefix in paths: - module_path = "/".join((prefix, module_name.replace(".", "/"))) package_path = "/".join((prefix, module_name.split(".")[0])) - is_module = (exists_case_sensitive(module_path + ".py") or - exists_case_sensitive(module_path + ".so")) + is_module = (exists_case_sensitive(package_path + ".py") or + exists_case_sensitive(package_path + ".so")) is_package = exists_case_sensitive(package_path) and os.path.isdir(package_path) if is_module or is_package: if ('site-packages' in prefix or 'dist-packages' in prefix or @@ -323,7 +336,8 @@ def _add_comments(self, comments, original_string=""): """ Returns a string with comments added """ - return comments and "{0} # {1}".format(self._strip_comments(original_string)[0], + return comments and "{0}{1} {2}".format(self._strip_comments(original_string)[0], + self.config['comment_prefix'], "; ".join(comments)) or original_string def _wrap(self, line): @@ -332,10 +346,16 @@ def _wrap(self, line): """ wrap_mode = self.config['multi_line_output'] if len(line) > self.config['line_length'] and wrap_mode != settings.WrapModes.NOQA: - for splitter in ("import", ".", "as"): + line_without_comment = line + comment = None + if '#' in line: + line_without_comment, comment = line.split('#', 1) + for splitter in ("import ", ".", "as "): exp = r"\b" + re.escape(splitter) + r"\b" - if re.search(exp, line) and not line.strip().startswith(splitter): - line_parts = re.split(exp, line) + if re.search(exp, line_without_comment) and not line_without_comment.strip().startswith(splitter): + line_parts = re.split(exp, line_without_comment) + if comment: + line_parts[-1] = '{0}#{1}'.format(line_parts[-1], comment) next_line = [] while (len(line) + 2) > (self.config['wrap_length'] or self.config['line_length']) and line_parts: next_line.append(line_parts.pop()) @@ -345,22 +365,22 @@ def _wrap(self, line): cont_line = self._wrap(self.config['indent'] + splitter.join(next_line).lstrip()) if self.config['use_parentheses']: - output = "{0}{1} (\n{2}{3}{4})".format( - line, splitter, cont_line, + output = "{0}{1}({2}{3}{4}{5})".format( + line, splitter, self.line_separator, cont_line, "," if self.config['include_trailing_comma'] else "", - "\n" if wrap_mode in ( + self.line_separator if wrap_mode in ( settings.WrapModes.VERTICAL_HANGING_INDENT, settings.WrapModes.VERTICAL_GRID_GROUPED, ) else "") - lines = output.split('\n') - if ' #' in lines[-1] and lines[-1].endswith(')'): - line, comment = lines[-1].split(' #', 1) - lines[-1] = line + ') #' + comment[:-1] - return '\n'.join(lines) - return "{0}{1} \\\n{2}".format(line, splitter, cont_line) + lines = output.split(self.line_separator) + if self.config['comment_prefix'] in lines[-1] and lines[-1].endswith(')'): + line, comment = lines[-1].split(self.config['comment_prefix'], 1) + lines[-1] = line + ')' + self.config['comment_prefix'] + comment[:-1] + return self.line_separator.join(lines) + return "{0}{1}\\{2}{3}".format(line, splitter, self.line_separator, cont_line) elif len(line) > self.config['line_length'] and wrap_mode == settings.WrapModes.NOQA: if "# NOQA" not in line: - return "{0} # NOQA".format(line) + return "{0}{1} NOQA".format(line, self.config['comment_prefix']) return line @@ -370,7 +390,10 @@ def _add_straight_imports(self, straight_modules, section, section_output): continue if module in self.as_map: - import_definition = "import {0} as {1}".format(module, self.as_map[module]) + import_definition = '' + if self.config['keep_direct_and_as_imports']: + import_definition = "import {0}\n".format(module) + import_definition += "import {0} as {1}".format(module, self.as_map[module]) else: import_definition = "import {0}".format(module) @@ -385,44 +408,50 @@ def _add_from_imports(self, from_modules, section, section_output, ignore_case): continue import_start = "from {0} import ".format(module) - from_imports = self.imports[section]['from'][module] - from_imports = nsorted(from_imports, key=lambda key: self._module_key(key, self.config, True, ignore_case)) + from_imports = list(self.imports[section]['from'][module]) + if not self.config['no_inline_sort'] or self.config['force_single_line']: + from_imports = nsorted(from_imports, key=lambda key: self._module_key(key, self.config, True, ignore_case)) if self.remove_imports: from_imports = [line for line in from_imports if not "{0}.{1}".format(module, line) in self.remove_imports] - for from_import in copy.copy(from_imports): - submodule = module + "." + from_import - import_as = self.as_map.get(submodule, False) - if import_as: - import_definition = "{0} as {1}".format(from_import, import_as) - if self.config['combine_as_imports'] and not ("*" in from_imports and - self.config['combine_star']): - from_imports[from_imports.index(from_import)] = import_definition - else: - import_statement = import_start + import_definition - force_grid_wrap = self.config['force_grid_wrap'] - comments = self.comments['straight'].get(submodule) - import_statement = self._add_comments(comments, self._wrap(import_statement)) - from_imports.remove(from_import) - section_output.append(import_statement) - + sub_modules = ['{0}.{1}'.format(module, from_import) for from_import in from_imports] + as_imports = dict((from_import, "{0} as {1}".format(from_import, self.as_map[sub_module])) for + from_import, sub_module in zip(from_imports, sub_modules) if sub_module in self.as_map) + if self.config['combine_as_imports'] and not ("*" in from_imports and self.config['combine_star']): + for from_import in copy.copy(from_imports): + if from_import in as_imports: + from_imports[from_imports.index(from_import)] = as_imports.pop(from_import) - if from_imports: + while from_imports: comments = self.comments['from'].pop(module, ()) if "*" in from_imports and self.config['combine_star']: import_statement = self._wrap(self._add_comments(comments, "{0}*".format(import_start))) + from_imports = None elif self.config['force_single_line']: import_statements = [] - for from_import in from_imports: + while from_imports: + from_import = from_imports.pop(0) + if from_import in as_imports: + from_comments = self.comments['straight'].get('{}.{}'.format(module, from_import)) + import_statements.append(self._add_comments(from_comments, + self._wrap(import_start + as_imports[from_import]))) + continue single_import_line = self._add_comments(comments, import_start + from_import) comment = self.comments['nested'].get(module, {}).pop(from_import, None) if comment: - single_import_line += "{0} {1}".format(comments and ";" or " #", comment) + single_import_line += "{0} {1}".format(comments and ";" or self.config['comment_prefix'], + comment) import_statements.append(self._wrap(single_import_line)) comments = None - import_statement = "\n".join(import_statements) + import_statement = self.line_separator.join(import_statements) else: + while from_imports and from_imports[0] in as_imports: + from_import = from_imports.pop(0) + from_comments = self.comments['straight'].get('{}.{}'.format(module, from_import)) + section_output.append(self._add_comments(from_comments, + self._wrap(import_start + as_imports[from_import]))) + star_import = False if "*" in from_imports: section_output.append(self._add_comments(comments, "{0}*".format(import_start))) @@ -431,10 +460,13 @@ def _add_from_imports(self, from_modules, section, section_output, ignore_case): comments = None for from_import in copy.copy(from_imports): + if from_import in as_imports: + continue comment = self.comments['nested'].get(module, {}).pop(from_import, None) if comment: single_import_line = self._add_comments(comments, import_start + from_import) - single_import_line += "{0} {1}".format(comments and ";" or " #", comment) + single_import_line += "{0} {1}".format(comments and ";" or self.config['comment_prefix'], + comment) above_comments = self.comments['above']['from'].pop(module, None) if above_comments: section_output.extend(above_comments) @@ -442,29 +474,32 @@ def _add_from_imports(self, from_modules, section, section_output, ignore_case): from_imports.remove(from_import) comments = None + from_import_section = [] + while from_imports and from_imports[0] not in as_imports: + from_import_section.append(from_imports.pop(0)) if star_import: - import_statement = import_start + (", ").join(from_imports) + import_statement = import_start + (", ").join(from_import_section) else: - import_statement = self._add_comments(comments, import_start + (", ").join(from_imports)) - if not from_imports: + import_statement = self._add_comments(comments, import_start + (", ").join(from_import_section)) + if not from_import_section: import_statement = "" do_multiline_reformat = False force_grid_wrap = self.config['force_grid_wrap'] - if force_grid_wrap and len(from_imports) >= force_grid_wrap: + if force_grid_wrap and len(from_import_section) >= force_grid_wrap: do_multiline_reformat = True - if len(import_statement) > self.config['line_length'] and len(from_imports) > 1: + if len(import_statement) > self.config['line_length'] and len(from_import_section) > 1: do_multiline_reformat = True # If line too long AND have imports AND we are NOT using GRID or VERTICAL wrap modes - if (len(import_statement) > self.config['line_length'] and len(from_imports) > 0 and + if (len(import_statement) > self.config['line_length'] and len(from_import_section) > 0 and self.config['multi_line_output'] not in (1, 0)): do_multiline_reformat = True if do_multiline_reformat: - import_statement = self._multi_line_reformat(import_start, from_imports, comments) + import_statement = self._multi_line_reformat(import_start, from_import_section, comments) if not do_multiline_reformat and len(import_statement) > self.config['line_length']: import_statement = self._wrap(import_statement) @@ -483,10 +518,10 @@ def _multi_line_reformat(self, import_start, from_imports, comments): import_statement = formatter(import_start, copy.copy(from_imports), dynamic_indent, indent, line_length, comments) if self.config['balanced_wrapping']: - lines = import_statement.split("\n") + lines = import_statement.split(self.line_separator) line_count = len(lines) if len(lines) > 1: - minimum_length = min([len(line) for line in lines[:-1]]) + minimum_length = min(len(line) for line in lines[:-1]) else: minimum_length = 0 new_import_statement = import_statement @@ -496,8 +531,8 @@ def _multi_line_reformat(self, import_start, from_imports, comments): line_length -= 1 new_import_statement = formatter(import_start, copy.copy(from_imports), dynamic_indent, indent, line_length, comments) - lines = new_import_statement.split("\n") - if import_statement.count('\n') == 0: + lines = new_import_statement.split(self.line_separator) + if import_statement.count(self.line_separator) == 0: return self._wrap(import_statement) return import_statement @@ -518,6 +553,7 @@ def _add_formatted_imports(self): sections = ('no_sections', ) output = [] + prev_section_has_imports = False for section in sections: straight_modules = self.imports[section]['straight'] straight_modules = nsorted(straight_modules, key=lambda key: self._module_key(key, self.config)) @@ -550,7 +586,6 @@ def by_module(line): line = line.lower() return '{0}{1}'.format(section, line) section_output = nsorted(section_output, key=by_module) - if section_output: section_name = section if section_name in self.place_imports: @@ -560,12 +595,17 @@ def by_module(line): section_title = self.config.get('import_heading_' + str(section_name).lower(), '') if section_title: section_comment = "# {0}".format(section_title) - if not section_comment in self.out_lines[0:1] and not section_comment in self.in_lines[0:1]: + if section_comment not in self.out_lines[0:1] and section_comment not in self.in_lines[0:1]: section_output.insert(0, section_comment) + if prev_section_has_imports and section_name in self.config['no_lines_before']: + while output and output[-1].strip() == '': + output.pop() output += section_output + ([''] * self.config['lines_between_sections']) - - while [character.strip() for character in output[-1:]] == [""]: + prev_section_has_imports = bool(section_output) + while output and output[-1].strip() == '': output.pop() + while output and output[0].strip() == '': + output.pop(0) output_at = 0 if self.import_index < self.original_length: @@ -582,16 +622,23 @@ def by_module(line): next_construct = "" self._in_quote = False tail = self.out_lines[imports_tail:] + for index, line in enumerate(tail): + in_quote = self._in_quote if not self._skip_line(line) and line.strip(): if line.strip().startswith("#") and len(tail) > (index + 1) and tail[index + 1].strip(): continue next_construct = line break + elif not in_quote: + parts = line.split() + if len(parts) >= 3 and parts[1] == '=' and "'" not in parts[0] and '"' not in parts[0]: + next_construct = line + break if self.config['lines_after_imports'] != -1: self.out_lines[imports_tail:0] = ["" for line in range(self.config['lines_after_imports'])] - elif next_construct.startswith("def") or next_construct.startswith("class") or \ + elif next_construct.startswith("def ") or next_construct.startswith("class ") or \ next_construct.startswith("@") or next_construct.startswith("async def"): self.out_lines[imports_tail:0] = ["", ""] else: @@ -612,7 +659,7 @@ def _output_grid(self, statement, imports, white_space, indent, line_length, com while imports: next_import = imports.pop(0) next_statement = self._add_comments(comments, statement + ", " + next_import) - if len(next_statement.split("\n")[-1]) + 1 > line_length: + if len(next_statement.split(self.line_separator)[-1]) + 1 > line_length: lines = ['{0}{1}'.format(white_space, next_import.split(" ")[0])] for part in next_import.split(" ")[1:]: new_line = '{0} {1}'.format(lines[-1], part) @@ -620,20 +667,20 @@ def _output_grid(self, statement, imports, white_space, indent, line_length, com lines.append('{0}{1}'.format(white_space, part)) else: lines[-1] = new_line - next_import = '\n'.join(lines) + next_import = self.line_separator.join(lines) statement = (self._add_comments(comments, "{0},".format(statement)) + - "\n{0}".format(next_import)) + "{0}{1}".format(self.line_separator, next_import)) comments = None else: statement += ", " + next_import return statement + ("," if self.config['include_trailing_comma'] else "") + ")" def _output_vertical(self, statement, imports, white_space, indent, line_length, comments): - first_import = self._add_comments(comments, imports.pop(0) + ",") + "\n" + white_space + first_import = self._add_comments(comments, imports.pop(0) + ",") + self.line_separator + white_space return "{0}({1}{2}{3})".format( statement, first_import, - (",\n" + white_space).join(imports), + ("," + self.line_separator + white_space).join(imports), "," if self.config['include_trailing_comma'] else "", ) @@ -642,56 +689,69 @@ def _output_hanging_indent(self, statement, imports, white_space, indent, line_l while imports: next_import = imports.pop(0) next_statement = self._add_comments(comments, statement + ", " + next_import) - if len(next_statement.split("\n")[-1]) + 3 > line_length: + if len(next_statement.split(self.line_separator)[-1]) + 3 > line_length: next_statement = (self._add_comments(comments, "{0}, \\".format(statement)) + - "\n{0}{1}".format(indent, next_import)) + "{0}{1}{2}".format(self.line_separator, indent, next_import)) comments = None statement = next_statement return statement def _output_vertical_hanging_indent(self, statement, imports, white_space, indent, line_length, comments): - return "{0}({1}\n{2}{3}{4}\n)".format( + return "{0}({1}{2}{3}{4}{5}{2})".format( statement, self._add_comments(comments), + self.line_separator, indent, - (",\n" + indent).join(imports), + ("," + self.line_separator + indent).join(imports), "," if self.config['include_trailing_comma'] else "", ) - def _output_vertical_grid_common(self, statement, imports, white_space, indent, line_length, comments): - statement += self._add_comments(comments, "(") + "\n" + indent + imports.pop(0) + def _output_vertical_grid_common(self, statement, imports, white_space, indent, line_length, comments, + need_trailing_char): + statement += self._add_comments(comments, "(") + self.line_separator + indent + imports.pop(0) while imports: next_import = imports.pop(0) next_statement = "{0}, {1}".format(statement, next_import) - if len(next_statement.split("\n")[-1]) + 1 > line_length: - next_statement = "{0},\n{1}{2}".format(statement, indent, next_import) + current_line_length = len(next_statement.split(self.line_separator)[-1]) + if imports or need_trailing_char: + # If we have more imports we need to account for a comma after this import + # We might also need to account for a closing ) we're going to add. + current_line_length += 1 + if current_line_length > line_length: + next_statement = "{0},{1}{2}{3}".format(statement, self.line_separator, indent, next_import) statement = next_statement if self.config['include_trailing_comma']: statement += ',' return statement def _output_vertical_grid(self, statement, imports, white_space, indent, line_length, comments): - return self._output_vertical_grid_common(statement, imports, white_space, indent, line_length, comments) + ")" + return self._output_vertical_grid_common(statement, imports, white_space, indent, line_length, comments, + True) + ")" def _output_vertical_grid_grouped(self, statement, imports, white_space, indent, line_length, comments): - return self._output_vertical_grid_common(statement, imports, white_space, indent, line_length, comments) + "\n)" + return self._output_vertical_grid_common(statement, imports, white_space, indent, line_length, comments, + True) + self.line_separator + ")" + + def _output_vertical_grid_grouped_no_comma(self, statement, imports, white_space, indent, line_length, comments): + return self._output_vertical_grid_common(statement, imports, white_space, indent, line_length, comments, + False) + self.line_separator + ")" def _output_noqa(self, statement, imports, white_space, indent, line_length, comments): retval = '{0}{1}'.format(statement, ', '.join(imports)) comment_str = ' '.join(comments) if comments: - if len(retval) + 4 + len(comment_str) <= line_length: - return '{0} # {1}'.format(retval, comment_str) + if len(retval) + len(self.config['comment_prefix']) + 1 + len(comment_str) <= line_length: + return '{0}{1} {2}'.format(retval, self.config['comment_prefix'], comment_str) else: if len(retval) <= line_length: return retval if comments: if "NOQA" in comments: - return '{0} # {1}'.format(retval, comment_str) + return '{0}{1} {2}'.format(retval, self.config['comment_prefix'], comment_str) else: - return '{0} # NOQA {1}'.format(retval, comment_str) + return '{0}{1} NOQA {2}'.format(retval, self.config['comment_prefix'], comment_str) else: - return '{0} # NOQA'.format(retval) + return '{0}{1} NOQA'.format(retval, self.config['comment_prefix']) @staticmethod def _strip_comments(line, comments=None): @@ -723,7 +783,7 @@ def _format_simplified(import_line): def _format_natural(import_line): import_line = import_line.strip() if not import_line.startswith("from ") and not import_line.startswith("import "): - if not "." in import_line: + if "." not in import_line: return "import {0}".format(import_line) parts = import_line.split(".") end = parts.pop(-1) @@ -783,7 +843,10 @@ def _parse(self): self._in_quote = False self._in_top_comment = False while not self._at_end(): - line = self._get_line() + raw_line = line = self._get_line() + line = line.replace("from.import ", "from . import ") + line = line.replace("\t", " ").replace('import*', 'import *') + line = line.replace(" .import ", " . import ") statement_index = self.index skip_line = self._skip_line(line) @@ -804,7 +867,7 @@ def _parse(self): import_type = self._import_type(line) if not import_type or skip_line: - self.out_lines.append(line) + self.out_lines.append(raw_line) continue for line in (line.strip() for line in line.split(";")): @@ -813,14 +876,12 @@ def _parse(self): self.out_lines.append(line) continue - line = line.replace("\t", " ").replace('import*', 'import *') if self.import_index == -1: self.import_index = self.index - 1 nested_comments = {} import_string, comments, new_comments = self._strip_comments(line) stripped_line = [part for part in self._strip_syntax(import_string).strip().split(" ") if part] - if import_type == "from" and len(stripped_line) == 2 and stripped_line[1] != "*" and new_comments: nested_comments[stripped_line[-1]] = comments[0] @@ -828,17 +889,32 @@ def _parse(self): while not line.strip().endswith(")") and not self._at_end(): line, comments, new_comments = self._strip_comments(self._get_line(), comments) stripped_line = self._strip_syntax(line).strip() - if import_type == "from" and stripped_line and not " " in stripped_line and new_comments: + if import_type == "from" and stripped_line and " " not in stripped_line and new_comments: nested_comments[stripped_line] = comments[-1] - import_string += "\n" + line + import_string += self.line_separator + line else: while line.strip().endswith("\\"): line, comments, new_comments = self._strip_comments(self._get_line(), comments) + + # Still need to check for parentheses after an escaped line + if "(" in line.split("#")[0] and not self._at_end(): + stripped_line = self._strip_syntax(line).strip() + if import_type == "from" and stripped_line and " " not in stripped_line and new_comments: + nested_comments[stripped_line] = comments[-1] + import_string += self.line_separator + line + + while not line.strip().endswith(")") and not self._at_end(): + line, comments, new_comments = self._strip_comments(self._get_line(), comments) + stripped_line = self._strip_syntax(line).strip() + if import_type == "from" and stripped_line and " " not in stripped_line and new_comments: + nested_comments[stripped_line] = comments[-1] + import_string += self.line_separator + line + stripped_line = self._strip_syntax(line).strip() - if import_type == "from" and stripped_line and not " " in stripped_line and new_comments: + if import_type == "from" and stripped_line and " " not in stripped_line and new_comments: nested_comments[stripped_line] = comments[-1] if import_string.strip().endswith(" import") or line.strip().startswith("import "): - import_string += "\n" + line + import_string += self.line_separator + line else: import_string = import_string.rstrip().rstrip("\\") + " " + line.lstrip() @@ -866,6 +942,8 @@ def _parse(self): if import_type == "from": import_from = imports.pop(0) placed_module = self.place_module(import_from) + if self.config['verbose']: + print("from-type place_module for %s returned %s" % (import_from, placed_module)) if placed_module == '': print( "WARNING: could not place module {0} of line {1} --" @@ -882,8 +960,8 @@ def _parse(self): if len(self.out_lines) > max(self.import_index, self._first_comment_index_end + 1, 1) - 1: last = self.out_lines and self.out_lines[-1].rstrip() or "" - while (last.startswith("#") and not last.endswith('"""') and not last.endswith("'''") and not - 'isort:imports-' in last): + while (last.startswith("#") and not last.endswith('"""') and not last.endswith("'''") and + 'isort:imports-' not in last): self.comments['above']['from'].setdefault(import_from, []).insert(0, self.out_lines.pop(-1)) if len(self.out_lines) > max(self.import_index - 1, self._first_comment_index_end + 1, 1) - 1: last = self.out_lines[-1].rstrip() @@ -906,7 +984,7 @@ def _parse(self): last = self.out_lines and self.out_lines[-1].rstrip() or "" while (last.startswith("#") and not last.endswith('"""') and not last.endswith("'''") and - not 'isort:imports-' in last): + 'isort:imports-' not in last): self.comments['above']['straight'].setdefault(module, []).insert(0, self.out_lines.pop(-1)) if len(self.out_lines) > 0: @@ -916,6 +994,8 @@ def _parse(self): if self.index - 1 == self.import_index: self.import_index -= len(self.comments['above']['straight'].get(module, [])) placed_module = self.place_module(module) + if self.config['verbose']: + print("else-type place_module for %s returned %s" % (module, placed_module)) if placed_module == '': print( "WARNING: could not place module {0} of line {1} --" @@ -942,18 +1022,6 @@ def coding_check(fname, default='utf-8'): return coding -def get_stdlib_path(): - """Returns the path to the standard lib for the current path installation. - - This function can be dropped and "sysconfig.get_paths()" used directly once Python 2.6 support is dropped. - """ - if sys.version_info >= (2, 7): - import sysconfig - return sysconfig.get_paths()['stdlib'] - else: - return os.path.join(sys.prefix, 'lib') - - def exists_case_sensitive(path): """ Returns if the given path exists and also matches the case on Windows. @@ -963,7 +1031,7 @@ def exists_case_sensitive(path): can only import using the case of the real file. """ result = os.path.exists(path) - if sys.platform.startswith('win') and result: + if (sys.platform.startswith('win') or sys.platform == 'darwin') and result: directory, basename = os.path.split(path) result = basename in os.listdir(directory) return result diff --git a/pythonFiles/isort/main.py b/pythonFiles/isort/main.py old mode 100755 new mode 100644 index eae7afa53409..efb3793d0a1b --- a/pythonFiles/isort/main.py +++ b/pythonFiles/isort/main.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#!/usr/bin/env python ''' Tool for sorting imports alphabetically, and automatically separated into sections. Copyright (C) 2013 Timothy Edmund Crosley @@ -23,7 +23,10 @@ import argparse import glob import os +import re import sys +from concurrent.futures import ProcessPoolExecutor +import functools import setuptools @@ -32,7 +35,6 @@ from .pie_slice import itemsview - INTRO = r""" /#######################################################################\ @@ -56,6 +58,36 @@ \########################################################################/ """.format(__version__) +shebang_re = re.compile(br'^#!.*\bpython[23w]?\b') + + +def is_python_file(path): + if path.endswith('.py'): + return True + + try: + with open(path, 'rb') as fp: + line = fp.readline(100) + except IOError: + return False + else: + return bool(shebang_re.match(line)) + + +class SortAttempt(object): + def __init__(self, incorrectly_sorted, skipped): + self.incorrectly_sorted = incorrectly_sorted + self.skipped = skipped + + +def sort_imports(file_name, **arguments): + try: + result = SortImports(file_name, **arguments) + return SortAttempt(result.incorrectly_sorted, result.skipped) + except IOError as e: + print("WARNING: Unable to parse file {0} due to {1}".format(file_name, e)) + return None + def iter_source_code(paths, config, skipped): """Iterate over all Python source files defined in paths.""" @@ -71,11 +103,12 @@ def iter_source_code(paths, config, skipped): skipped.append(dirname) dirnames.remove(dirname) for filename in filenames: - if filename.endswith('.py'): + filepath = os.path.join(dirpath, filename) + if is_python_file(filepath): if should_skip(filename, config, dirpath): skipped.append(filename) else: - yield os.path.join(dirpath, filename) + yield filepath else: yield path @@ -138,105 +171,119 @@ def run(self): def create_parser(): parser = argparse.ArgumentParser(description='Sort Python import definitions alphabetically ' 'within logical sections.') - parser.add_argument('files', nargs='*', help='One or more Python source files that need their imports sorted.') - parser.add_argument('-y', '--apply', dest='apply', action='store_true', - help='Tells isort to apply changes recursively without asking') - parser.add_argument('-l', '--lines', help='[Deprecated] The max length of an import line (used for wrapping ' - 'long imports).', - dest='line_length', type=int) - parser.add_argument('-w', '--line-width', help='The max length of an import line (used for wrapping long imports).', - dest='line_length', type=int) - parser.add_argument('-s', '--skip', help='Files that sort imports should skip over. If you want to skip multiple ' - 'files you should specify twice: --skip file1 --skip file2.', dest='skip', action='append') - parser.add_argument('-ns', '--dont-skip', help='Files that sort imports should never skip over.', - dest='not_skip', action='append') - parser.add_argument('-sg', '--skip-glob', help='Files that sort imports should skip over.', dest='skip_glob', - action='append') - parser.add_argument('-t', '--top', help='Force specific imports to the top of their appropriate section.', - dest='force_to_top', action='append') - parser.add_argument('-f', '--future', dest='known_future_library', action='append', - help='Force sortImports to recognize a module as part of the future compatibility libraries.') - parser.add_argument('-b', '--builtin', dest='known_standard_library', action='append', - help='Force sortImports to recognize a module as part of the python standard library.') - parser.add_argument('-o', '--thirdparty', dest='known_third_party', action='append', - help='Force sortImports to recognize a module as being part of a third party library.') - parser.add_argument('-p', '--project', dest='known_first_party', action='append', - help='Force sortImports to recognize a module as being part of the current python project.') - parser.add_argument('--virtual-env', dest='virtual_env', - help='Virtual environment to use for determining whether a package is third-party') - parser.add_argument('-m', '--multi-line', dest='multi_line_output', type=int, choices=[0, 1, 2, 3, 4, 5], - help='Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, ' - '5-vert-grid-grouped).') - parser.add_argument('-i', '--indent', help='String to place for indents defaults to " " (4 spaces).', - dest='indent', type=str) + inline_args_group = parser.add_mutually_exclusive_group() parser.add_argument('-a', '--add-import', dest='add_imports', action='append', help='Adds the specified import line to all files, ' 'automatically determining correct placement.') + parser.add_argument('-ac', '--atomic', dest='atomic', action='store_true', + help="Ensures the output doesn't save if the resulting file contains syntax errors.") parser.add_argument('-af', '--force-adds', dest='force_adds', action='store_true', help='Forces import adds even if the original file is empty.') - parser.add_argument('-r', '--remove-import', dest='remove_imports', action='append', - help='Removes the specified import from all files.') - parser.add_argument('-ls', '--length-sort', help='Sort imports by their string length.', - dest='length_sort', action='store_true') - parser.add_argument('-d', '--stdout', help='Force resulting output to stdout, instead of in-place.', - dest='write_to_stdout', action='store_true') + parser.add_argument('-b', '--builtin', dest='known_standard_library', action='append', + help='Force sortImports to recognize a module as part of the python standard library.') parser.add_argument('-c', '--check-only', action='store_true', dest="check", help='Checks the file for unsorted / unformatted imports and prints them to the ' 'command line without modifying the file.') - parser.add_argument('-ws', '--ignore-whitespace', action='store_true', dest="ignore_whitespace", - help='Tells isort to ignore whitespace differences when --check-only is being used.') - parser.add_argument('-sl', '--force-single-line-imports', dest='force_single_line', action='store_true', - help='Forces all from imports to appear on their own line') - parser.add_argument('-ds', '--no-sections', help='Put all imports into the same section bucket', dest='no_sections', - action='store_true') - parser.add_argument('-sd', '--section-default', dest='default_section', - help='Sets the default section for imports (by default FIRSTPARTY) options: ' + - str(DEFAULT_SECTIONS)) + parser.add_argument('-ca', '--combine-as', dest='combine_as_imports', action='store_true', + help="Combines as imports on the same line.") + parser.add_argument('-cs', '--combine-star', dest='combine_star', action='store_true', + help="Ensures that if a star import is present, nothing else is imported from that namespace.") + parser.add_argument('-d', '--stdout', help='Force resulting output to stdout, instead of in-place.', + dest='write_to_stdout', action='store_true') parser.add_argument('-df', '--diff', dest='show_diff', action='store_true', help="Prints a diff of all the changes isort would make to a file, instead of " "changing it in place") - parser.add_argument('-e', '--balanced', dest='balanced_wrapping', action='store_true', - help='Balances wrapping to produce the most consistent line length possible') - parser.add_argument('-rc', '--recursive', dest='recursive', action='store_true', - help='Recursively look for Python files of which to sort imports') - parser.add_argument('-ot', '--order-by-type', dest='order_by_type', - action='store_true', help='Order imports by type in addition to alphabetically') + parser.add_argument('-ds', '--no-sections', help='Put all imports into the same section bucket', dest='no_sections', + action='store_true') parser.add_argument('-dt', '--dont-order-by-type', dest='dont_order_by_type', action='store_true', help='Only order imports alphabetically, do not attempt type ordering') - parser.add_argument('-ac', '--atomic', dest='atomic', action='store_true', - help="Ensures the output doesn't save if the resulting file contains syntax errors.") - parser.add_argument('-cs', '--combine-star', dest='combine_star', action='store_true', - help="Ensures that if a star import is present, nothing else is imported from that namespace.") - parser.add_argument('-ca', '--combine-as', dest='combine_as_imports', action='store_true', - help="Combines as imports on the same line.") - parser.add_argument('-tc', '--trailing-comma', dest='include_trailing_comma', action='store_true', - help='Includes a trailing comma on multi line imports that include parentheses.') - parser.add_argument('-v', '--version', action='store_true', dest='show_version') - parser.add_argument('-vb', '--verbose', action='store_true', dest="verbose", - help='Shows verbose output, such as when files are skipped or when a check is successful.') - parser.add_argument('-q', '--quiet', action='store_true', dest="quiet", - help='Shows extra quiet output, only errors are outputted.') - parser.add_argument('-sp', '--settings-path', dest="settings_path", - help='Explicitly set the settings path instead of auto determining based on file location.') + parser.add_argument('-e', '--balanced', dest='balanced_wrapping', action='store_true', + help='Balances wrapping to produce the most consistent line length possible') + parser.add_argument('-f', '--future', dest='known_future_library', action='append', + help='Force sortImports to recognize a module as part of the future compatibility libraries.') + parser.add_argument('-fas', '--force-alphabetical-sort', action='store_true', dest="force_alphabetical_sort", + help='Force all imports to be sorted as a single section') + parser.add_argument('-fass', '--force-alphabetical-sort-within-sections', action='store_true', + dest="force_alphabetical_sort", help='Force all imports to be sorted alphabetically within a ' + 'section') parser.add_argument('-ff', '--from-first', dest='from_first', help="Switches the typical ordering preference, showing from imports first then straight ones.") - parser.add_argument('-wl', '--wrap-length', dest='wrap_length', - help="Specifies how long lines that are wrapped should be, if not set line_length is used.") parser.add_argument('-fgw', '--force-grid-wrap', nargs='?', const=2, type=int, dest="force_grid_wrap", help='Force number of from imports (defaults to 2) to be grid wrapped regardless of line ' 'length') - parser.add_argument('-fass', '--force-alphabetical-sort-within-sections', action='store_true', - dest="force_alphabetical_sort", help='Force all imports to be sorted alphabetically within a ' - 'section') - parser.add_argument('-fas', '--force-alphabetical-sort', action='store_true', dest="force_alphabetical_sort", - help='Force all imports to be sorted as a single section') parser.add_argument('-fss', '--force-sort-within-sections', action='store_true', dest="force_sort_within_sections", help='Force imports to be sorted by module, independent of import_type') + parser.add_argument('-i', '--indent', help='String to place for indents defaults to " " (4 spaces).', + dest='indent', type=str) + parser.add_argument('-j', '--jobs', help='Number of files to process in parallel.', + dest='jobs', type=int) + parser.add_argument('-k', '--keep-direct-and-as', dest='keep_direct_and_as_imports', action='store_true', + help="Turns off default behavior that removes direct imports when as imports exist.") + parser.add_argument('-l', '--lines', help='[Deprecated] The max length of an import line (used for wrapping ' + 'long imports).', + dest='line_length', type=int) + parser.add_argument('-lai', '--lines-after-imports', dest='lines_after_imports', type=int) parser.add_argument('-lbt', '--lines-between-types', dest='lines_between_types', type=int) + parser.add_argument('-le', '--line-ending', dest='line_ending', + help="Forces line endings to the specified value. If not set, values will be guessed per-file.") + parser.add_argument('-ls', '--length-sort', help='Sort imports by their string length.', + dest='length_sort', action='store_true') + parser.add_argument('-m', '--multi-line', dest='multi_line_output', type=int, choices=[0, 1, 2, 3, 4, 5], + help='Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, ' + '5-vert-grid-grouped, 6-vert-grid-grouped-no-comma).') + inline_args_group.add_argument('-nis', '--no-inline-sort', dest='no_inline_sort', action='store_true', + help='Leaves `from` imports with multiple imports \'as-is\' (e.g. `from foo import a, c ,b`).') + parser.add_argument('-nlb', '--no-lines-before', help='Sections which should not be split with previous by empty lines', + dest='no_lines_before', action='append') + parser.add_argument('-ns', '--dont-skip', help='Files that sort imports should never skip over.', + dest='not_skip', action='append') + parser.add_argument('-o', '--thirdparty', dest='known_third_party', action='append', + help='Force sortImports to recognize a module as being part of a third party library.') + parser.add_argument('-ot', '--order-by-type', dest='order_by_type', + action='store_true', help='Order imports by type in addition to alphabetically') + parser.add_argument('-p', '--project', dest='known_first_party', action='append', + help='Force sortImports to recognize a module as being part of the current python project.') + parser.add_argument('-q', '--quiet', action='store_true', dest="quiet", + help='Shows extra quiet output, only errors are outputted.') + parser.add_argument('-r', '--remove-import', dest='remove_imports', action='append', + help='Removes the specified import from all files.') + parser.add_argument('-rc', '--recursive', dest='recursive', action='store_true', + help='Recursively look for Python files of which to sort imports') + parser.add_argument('-s', '--skip', help='Files that sort imports should skip over. If you want to skip multiple ' + 'files you should specify twice: --skip file1 --skip file2.', dest='skip', action='append') + parser.add_argument('-sd', '--section-default', dest='default_section', + help='Sets the default section for imports (by default FIRSTPARTY) options: ' + + str(DEFAULT_SECTIONS)) + parser.add_argument('-sg', '--skip-glob', help='Files that sort imports should skip over.', dest='skip_glob', + action='append') + inline_args_group.add_argument('-sl', '--force-single-line-imports', dest='force_single_line', action='store_true', + help='Forces all from imports to appear on their own line') + parser.add_argument('-sp', '--settings-path', dest="settings_path", + help='Explicitly set the settings path instead of auto determining based on file location.') + parser.add_argument('-t', '--top', help='Force specific imports to the top of their appropriate section.', + dest='force_to_top', action='append') + parser.add_argument('-tc', '--trailing-comma', dest='include_trailing_comma', action='store_true', + help='Includes a trailing comma on multi line imports that include parentheses.') parser.add_argument('-up', '--use-parentheses', dest='use_parentheses', action='store_true', - help='Use parenthesis for line continuation on lenght limit instead of slashes.') + help='Use parenthesis for line continuation on length limit instead of slashes.') + parser.add_argument('-v', '--version', action='store_true', dest='show_version') + parser.add_argument('-vb', '--verbose', action='store_true', dest="verbose", + help='Shows verbose output, such as when files are skipped or when a check is successful.') + parser.add_argument('--virtual-env', dest='virtual_env', + help='Virtual environment to use for determining whether a package is third-party') + parser.add_argument('-vn', '--version-number', action='version', version=__version__, + help='Returns just the current version number without the logo') + parser.add_argument('-w', '--line-width', help='The max length of an import line (used for wrapping long imports).', + dest='line_length', type=int) + parser.add_argument('-wl', '--wrap-length', dest='wrap_length', + help="Specifies how long lines that are wrapped should be, if not set line_length is used.") + parser.add_argument('-ws', '--ignore-whitespace', action='store_true', dest="ignore_whitespace", + help='Tells isort to ignore whitespace differences when --check-only is being used.') + parser.add_argument('-y', '--apply', dest='apply', action='store_true', + help='Tells isort to apply changes recursively without asking') + parser.add_argument('files', nargs='*', help='One or more Python source files that need their imports sorted.') - arguments = dict((key, value) for (key, value) in itemsview(vars(parser.parse_args())) if value) + arguments = {key: value for key, value in itemsview(vars(parser.parse_args())) if value} if 'dont_order_by_type' in arguments: arguments['order_by_type'] = False return arguments @@ -251,6 +298,14 @@ def main(): if 'settings_path' in arguments: sp = arguments['settings_path'] arguments['settings_path'] = os.path.abspath(sp) if os.path.isdir(sp) else os.path.dirname(os.path.abspath(sp)) + if not os.path.isdir(arguments['settings_path']): + print("WARNING: settings_path dir does not exist: {0}".format(arguments['settings_path'])) + + if 'virtual_env' in arguments: + venv = arguments['virtual_env'] + arguments['virtual_env'] = os.path.abspath(venv) + if not os.path.isdir(arguments['virtual_env']): + print("WARNING: virtual_env dir does not exist: {0}".format(arguments['virtual_env'])) file_names = arguments.pop('files', []) if file_names == ['-']: @@ -270,16 +325,29 @@ def main(): num_skipped = 0 if config['verbose'] or config.get('show_logo', False): print(INTRO) - for file_name in file_names: - try: - sort_attempt = SortImports(file_name, **arguments) + jobs = arguments.get('jobs') + if jobs: + executor = ProcessPoolExecutor(max_workers=jobs) + + for sort_attempt in executor.map(functools.partial(sort_imports, **arguments), file_names): + if not sort_attempt: + continue incorrectly_sorted = sort_attempt.incorrectly_sorted if arguments.get('check', False) and incorrectly_sorted: wrong_sorted_files = True if sort_attempt.skipped: num_skipped += 1 - except IOError as e: - print("WARNING: Unable to parse file {0} due to {1}".format(file_name, e)) + else: + for file_name in file_names: + try: + sort_attempt = SortImports(file_name, **arguments) + incorrectly_sorted = sort_attempt.incorrectly_sorted + if arguments.get('check', False) and incorrectly_sorted: + wrong_sorted_files = True + if sort_attempt.skipped: + num_skipped += 1 + except IOError as e: + print("WARNING: Unable to parse file {0} due to {1}".format(file_name, e)) if wrong_sorted_files: exit(1) diff --git a/pythonFiles/isort/natural.py b/pythonFiles/isort/natural.py index aac8c4a36157..c02b42c37e48 100644 --- a/pythonFiles/isort/natural.py +++ b/pythonFiles/isort/natural.py @@ -8,7 +8,7 @@ Copyright (C) 2013 Timothy Edmund Crosley Implementation originally from @HappyLeapSecond stack overflow user in response to: - http://stackoverflow.com/questions/5967500/how-to-correctly-sort-a-string-with-a-number-inside + https://stackoverflow.com/questions/5967500/how-to-correctly-sort-a-string-with-a-number-inside Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation diff --git a/pythonFiles/isort/pie_slice.py b/pythonFiles/isort/pie_slice.py index bfb341a7f7ec..178cf33e87c3 100644 --- a/pythonFiles/isort/pie_slice.py +++ b/pythonFiles/isort/pie_slice.py @@ -21,11 +21,8 @@ """ from __future__ import absolute_import -import abc import collections -import functools import sys -from numbers import Integral __version__ = "1.1.0" @@ -47,7 +44,7 @@ common = ['native_dict', 'native_round', 'native_filter', 'native_map', 'native_range', 'native_str', 'native_chr', 'native_input', 'PY2', 'PY3', 'u', 'itemsview', 'valuesview', 'keysview', 'execute', 'integer_types', - 'native_next', 'native_object', 'with_metaclass', 'OrderedDict', 'lru_cache'] + 'native_next', 'native_object', 'with_metaclass', 'lru_cache'] def with_metaclass(meta, *bases): @@ -85,32 +82,12 @@ def unmodified_isinstance(*bases): """ class UnmodifiedIsInstance(type): - if sys.version_info[0] == 2 and sys.version_info[1] <= 6: - - @classmethod - def __instancecheck__(cls, instance): - if cls.__name__ in (str(base.__name__) for base in bases): - return isinstance(instance, bases) - - subclass = getattr(instance, '__class__', None) - subtype = type(instance) - instance_type = getattr(abc, '_InstanceType', None) - if not instance_type: - class test_object: - pass - instance_type = type(test_object) - if subtype is instance_type: - subtype = subclass - if subtype is subclass or subclass is None: - return cls.__subclasscheck__(subtype) - return (cls.__subclasscheck__(subclass) or cls.__subclasscheck__(subtype)) - else: - @classmethod - def __instancecheck__(cls, instance): - if cls.__name__ in (str(base.__name__) for base in bases): - return isinstance(instance, bases) + @classmethod + def __instancecheck__(cls, instance): + if cls.__name__ in (str(base.__name__) for base in bases): + return isinstance(instance, bases) - return type.__instancecheck__(cls, instance) + return type.__instancecheck__(cls, instance) return with_metaclass(UnmodifiedIsInstance, *bases) @@ -148,12 +125,11 @@ def callable(entity): __all__ = common + ['urllib'] else: - from itertools import ifilter as filter - from itertools import imap as map - from itertools import izip as zip + from itertools import ifilter as filter # noqa: F401 + from itertools import imap as map # noqa: F401 + from itertools import izip as zip # noqa: F401 from decimal import Decimal, ROUND_HALF_EVEN - import codecs str = unicode chr = unichr input = raw_input @@ -281,28 +257,10 @@ def __new__(cls, name, bases, dct): dct['__str__'] = lambda self: self.__unicode__().encode('utf-8') return type.__new__(cls, name, bases, dct) - if sys.version_info[1] <= 6: - def __instancecheck__(cls, instance): - if cls.__name__ == "object": - return isinstance(instance, native_object) - - subclass = getattr(instance, '__class__', None) - subtype = type(instance) - instance_type = getattr(abc, '_InstanceType', None) - if not instance_type: - class test_object: - pass - instance_type = type(test_object) - if subtype is instance_type: - subtype = subclass - if subtype is subclass or subclass is None: - return cls.__subclasscheck__(subtype) - return (cls.__subclasscheck__(subclass) or cls.__subclasscheck__(subtype)) - else: - def __instancecheck__(cls, instance): - if cls.__name__ == "object": - return isinstance(instance, native_object) - return type.__instancecheck__(cls, instance) + def __instancecheck__(cls, instance): + if cls.__name__ == "object": + return isinstance(instance, native_object) + return type.__instancecheck__(cls, instance) class object(with_metaclass(FixStr, object)): pass @@ -310,138 +268,6 @@ class object(with_metaclass(FixStr, object)): __all__ = common + ['round', 'dict', 'apply', 'cmp', 'coerce', 'execfile', 'raw_input', 'unpacks', 'str', 'chr', 'input', 'range', 'filter', 'map', 'zip', 'object'] -if sys.version_info[0] == 2 and sys.version_info[1] < 7: - # OrderedDict - # Copyright (c) 2009 Raymond Hettinger - # - # Permission is hereby granted, free of charge, to any person - # obtaining a copy of this software and associated documentation files - # (the "Software"), to deal in the Software without restriction, - # including without limitation the rights to use, copy, modify, merge, - # publish, distribute, sublicense, and/or sell copies of the Software, - # and to permit persons to whom the Software is furnished to do so, - # subject to the following conditions: - # - # The above copyright notice and this permission notice shall be - # included in all copies or substantial portions of the Software. - # - # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - # OTHER DEALINGS IN THE SOFTWARE. - - from UserDict import DictMixin - - class OrderedDict(dict, DictMixin): - - def __init__(self, *args, **kwds): - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - try: - self.__end - except AttributeError: - self.clear() - self.update(*args, **kwds) - - def clear(self): - self.__end = end = [] - end += [None, end, end] # sentinel node for doubly linked list - self.__map = {} # key --> [key, prev, next] - dict.clear(self) - - def __setitem__(self, key, value): - if key not in self: - end = self.__end - curr = end[1] - curr[2] = end[1] = self.__map[key] = [key, curr, end] - dict.__setitem__(self, key, value) - - def __delitem__(self, key): - dict.__delitem__(self, key) - key, prev, next = self.__map.pop(key) - prev[2] = next - next[1] = prev - - def __iter__(self): - end = self.__end - curr = end[2] - while curr is not end: - yield curr[0] - curr = curr[2] - - def __reversed__(self): - end = self.__end - curr = end[1] - while curr is not end: - yield curr[0] - curr = curr[1] - - def popitem(self, last=True): - if not self: - raise KeyError('dictionary is empty') - if last: - key = reversed(self).next() - else: - key = iter(self).next() - value = self.pop(key) - return key, value - - def __reduce__(self): - items = [[k, self[k]] for k in self] - tmp = self.__map, self.__end - del self.__map, self.__end - inst_dict = vars(self).copy() - self.__map, self.__end = tmp - if inst_dict: - return (self.__class__, (items,), inst_dict) - return self.__class__, (items,) - - def keys(self): - return list(self) - - setdefault = DictMixin.setdefault - update = DictMixin.update - pop = DictMixin.pop - values = DictMixin.values - items = DictMixin.items - iterkeys = DictMixin.iterkeys - itervalues = DictMixin.itervalues - iteritems = DictMixin.iteritems - - def __repr__(self): - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, self.items()) - - def copy(self): - return self.__class__(self) - - @classmethod - def fromkeys(cls, iterable, value=None): - d = cls() - for key in iterable: - d[key] = value - return d - - def __eq__(self, other): - if isinstance(other, OrderedDict): - if len(self) != len(other): - return False - for p, q in zip(self.items(), other.items()): - if p != q: - return False - return True - return dict.__eq__(self, other) - - def __ne__(self, other): - return not self == other -else: - from collections import OrderedDict - if sys.version_info < (3, 2): try: @@ -451,6 +277,8 @@ def __ne__(self, other): from functools import wraps + _CacheInfo = collections.namedtuple("CacheInfo", "hits misses maxsize currsize") + def lru_cache(maxsize=100): """Least-recently-used cache decorator. Taking from: https://github.com/MiCHiLU/python-functools32/blob/master/functools32/functools32.py @@ -461,7 +289,7 @@ def lru_cache(maxsize=100): View the cache statistics named tuple (hits, misses, maxsize, currsize) with f.cache_info(). Clear the cache and statistics with f.cache_clear(). Access the underlying function with f.__wrapped__. - See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used + See: https://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used """ def decorating_function(user_function, tuple=tuple, sorted=sorted, len=len, KeyError=KeyError): @@ -470,7 +298,7 @@ def decorating_function(user_function, tuple=tuple, sorted=sorted, len=len, KeyE lock = Lock() if maxsize is None: - CACHE = dict() + CACHE = {} @wraps(user_function) def wrapper(*args, **kwds): @@ -488,7 +316,7 @@ def wrapper(*args, **kwds): misses[0] += 1 return result else: - CACHE = OrderedDict() + CACHE = collections.OrderedDict() @wraps(user_function) def wrapper(*args, **kwds): @@ -528,7 +356,7 @@ def cache_clear(): return decorating_function else: - from functools import lru_cache + from functools import lru_cache # noqa: F401 class OrderedSet(collections.MutableSet): diff --git a/pythonFiles/isort/settings.py b/pythonFiles/isort/settings.py index 15cdb21094ac..dd0a71102405 100644 --- a/pythonFiles/isort/settings.py +++ b/pythonFiles/isort/settings.py @@ -25,8 +25,10 @@ from __future__ import absolute_import, division, print_function, unicode_literals import fnmatch +import io import os import posixpath +import sys from collections import namedtuple from .pie_slice import itemsview, lru_cache, native_str @@ -36,10 +38,11 @@ except ImportError: import ConfigParser as configparser -MAX_CONFIG_SEARCH_DEPTH = 25 # The number of parent directories isort will look for a config file within +MAX_CONFIG_SEARCH_DEPTH = 25 # The number of parent directories isort will look for a config file within DEFAULT_SECTIONS = ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER') -WrapModes = ('GRID', 'VERTICAL', 'HANGING_INDENT', 'VERTICAL_HANGING_INDENT', 'VERTICAL_GRID', 'VERTICAL_GRID_GROUPED', 'NOQA') +WrapModes = ('GRID', 'VERTICAL', 'HANGING_INDENT', 'VERTICAL_HANGING_INDENT', 'VERTICAL_GRID', 'VERTICAL_GRID_GROUPED', + 'VERTICAL_GRID_GROUPED_NO_COMMA', 'NOQA') WrapModes = namedtuple('WrapModes', WrapModes)(*range(len(WrapModes))) # Note that none of these lists must be complete as they are simply fallbacks for when included auto-detection fails. @@ -48,6 +51,7 @@ 'skip_glob': [], 'line_length': 79, 'wrap_length': 0, + 'line_ending': None, 'sections': DEFAULT_SECTIONS, 'no_sections': False, 'known_future_library': ['__future__'], @@ -101,6 +105,7 @@ 'multi_line_output': WrapModes.GRID, 'forced_separate': [], 'indent': ' ' * 4, + 'comment_prefix': ' #', 'length_sort': False, 'add_imports': [], 'remove_imports': [], @@ -120,6 +125,7 @@ 'lines_between_types': 0, 'combine_as_imports': False, 'combine_star': False, + 'keep_direct_and_as_imports': False, 'include_trailing_comma': False, 'from_first': False, 'verbose': False, @@ -130,7 +136,9 @@ 'force_grid_wrap': 0, 'force_sort_within_sections': False, 'show_diff': False, - 'ignore_whitespace': False} + 'ignore_whitespace': False, + 'no_lines_before': [], + 'no_inline_sort': False} @lru_cache() @@ -214,7 +222,7 @@ def _as_list(value): @lru_cache() def _get_config_data(file_path, sections): - with open(file_path, 'rU') as config_file: + with io.open(file_path, 'r') as config_file: if file_path.endswith('.editorconfig'): line = '\n' last_position = config_file.tell() @@ -225,9 +233,14 @@ def _get_config_data(file_path, sections): break last_position = config_file.tell() - config = configparser.SafeConfigParser() - config.readfp(config_file) - settings = dict() + if sys.version_info >= (3, 2): + config = configparser.ConfigParser() + config.read_file(config_file) + else: + config = configparser.SafeConfigParser() + config.readfp(config_file) + + settings = {} for section in sections: if config.has_section(section): settings.update(dict(config.items(section))) diff --git a/pythonFiles/jedi/__init__.py b/pythonFiles/jedi/__init__.py index 1a1080ad2fd4..ff2de906d9cd 100644 --- a/pythonFiles/jedi/__init__.py +++ b/pythonFiles/jedi/__init__.py @@ -36,8 +36,12 @@ good text editor, while still having very good IDE features for Python. """ -__version__ = '0.11.1' +__version__ = '0.12.0' from jedi.api import Script, Interpreter, set_debug_function, \ preload_module, names from jedi import settings +from jedi.api.environment import find_virtualenvs, find_system_environments, \ + get_default_environment, InvalidPythonEnvironment, create_environment, \ + get_system_environment +from jedi.api.exceptions import InternalError diff --git a/pythonFiles/jedi/_compatibility.py b/pythonFiles/jedi/_compatibility.py index 52a20fe2c07c..8b55fb8f4a1d 100644 --- a/pythonFiles/jedi/_compatibility.py +++ b/pythonFiles/jedi/_compatibility.py @@ -1,25 +1,25 @@ """ -To ensure compatibility from Python ``2.6`` - ``3.3``, a module has been +To ensure compatibility from Python ``2.7`` - ``3.x``, a module has been created. Clearly there is huge need to use conforming syntax. """ +import binascii +import errno import sys -import imp import os import re import pkgutil import warnings +import inspect +import subprocess try: import importlib except ImportError: pass -# Cannot use sys.version.major and minor names, because in Python 2.6 it's not -# a namedtuple. is_py3 = sys.version_info[0] >= 3 is_py33 = is_py3 and sys.version_info[1] >= 3 is_py34 = is_py3 and sys.version_info[1] >= 4 is_py35 = is_py3 and sys.version_info[1] >= 5 -is_py26 = not is_py3 and sys.version_info[1] < 7 py_version = int(str(sys.version_info[0]) + str(sys.version_info[1])) @@ -35,28 +35,24 @@ def close(self): del self.loader -def find_module_py34(string, path=None, fullname=None): - implicit_namespace_pkg = False +def find_module_py34(string, path=None, full_name=None): spec = None loader = None spec = importlib.machinery.PathFinder.find_spec(string, path) - if hasattr(spec, 'origin'): - origin = spec.origin - implicit_namespace_pkg = origin == 'namespace' - - # We try to disambiguate implicit namespace pkgs with non implicit namespace pkgs - if implicit_namespace_pkg: - fullname = string if not path else fullname - implicit_ns_info = ImplicitNSInfo(fullname, spec.submodule_search_locations._path) - return None, implicit_ns_info, False - - # we have found the tail end of the dotted path - if hasattr(spec, 'loader'): + if spec is not None: + # We try to disambiguate implicit namespace pkgs with non implicit namespace pkgs + if not spec.has_location: + full_name = string if not path else full_name + implicit_ns_info = ImplicitNSInfo(full_name, spec.submodule_search_locations._path) + return None, implicit_ns_info, False + + # we have found the tail end of the dotted path loader = spec.loader return find_module_py33(string, path, loader) -def find_module_py33(string, path=None, loader=None, fullname=None): + +def find_module_py33(string, path=None, loader=None, full_name=None): loader = loader or importlib.machinery.PathFinder.find_module(string, path) if loader is None and path is None: # Fallback to find builtins @@ -74,7 +70,7 @@ def find_module_py33(string, path=None, loader=None, fullname=None): raise ImportError("Originally " + repr(e)) if loader is None: - raise ImportError("Couldn't find a loader for {0}".format(string)) + raise ImportError("Couldn't find a loader for {}".format(string)) try: is_package = loader.is_package(string) @@ -109,7 +105,10 @@ def find_module_py33(string, path=None, loader=None, fullname=None): return module_file, module_path, is_package -def find_module_pre_py33(string, path=None, fullname=None): +def find_module_pre_py33(string, path=None, full_name=None): + # This import is here, because in other places it will raise a + # DeprecationWarning. + import imp try: module_file, module_path, description = imp.find_module(string, path) module_type = description[2] @@ -127,14 +126,7 @@ def find_module_pre_py33(string, path=None, fullname=None): if loader: is_package = loader.is_package(string) is_archive = hasattr(loader, 'archive') - try: - module_path = loader.get_filename(string) - except AttributeError: - # fallback for py26 - try: - module_path = loader._get_filename(string) - except AttributeError: - continue + module_path = loader.get_filename(string) if is_package: module_path = os.path.dirname(module_path) if is_archive: @@ -142,14 +134,14 @@ def find_module_pre_py33(string, path=None, fullname=None): file = None if not is_package or is_archive: file = DummyFile(loader, string) - return (file, module_path, is_package) + return file, module_path, is_package except ImportError: pass - raise ImportError("No module named {0}".format(string)) + raise ImportError("No module named {}".format(string)) find_module = find_module_py33 if is_py33 else find_module_pre_py33 -find_module = find_module_py34 if is_py34 else find_module +find_module = find_module_py34 if is_py34 else find_module find_module.__doc__ = """ Provides information about a module. @@ -161,12 +153,80 @@ def find_module_pre_py33(string, path=None, fullname=None): """ +def _iter_modules(paths, prefix=''): + # Copy of pkgutil.iter_modules adapted to work with namespaces + + for path in paths: + importer = pkgutil.get_importer(path) + + if not isinstance(importer, importlib.machinery.FileFinder): + # We're only modifying the case for FileFinder. All the other cases + # still need to be checked (like zip-importing). Do this by just + # calling the pkgutil version. + for mod_info in pkgutil.iter_modules([path], prefix): + yield mod_info + continue + + # START COPY OF pkutils._iter_file_finder_modules. + if importer.path is None or not os.path.isdir(importer.path): + return + + yielded = {} + + try: + filenames = os.listdir(importer.path) + except OSError: + # ignore unreadable directories like import does + filenames = [] + filenames.sort() # handle packages before same-named modules + + for fn in filenames: + modname = inspect.getmodulename(fn) + if modname == '__init__' or modname in yielded: + continue + + # jedi addition: Avoid traversing special directories + if fn.startswith('.') or fn == '__pycache__': + continue + + path = os.path.join(importer.path, fn) + ispkg = False + + if not modname and os.path.isdir(path) and '.' not in fn: + modname = fn + # A few jedi modifications: Don't check if there's an + # __init__.py + try: + os.listdir(path) + except OSError: + # ignore unreadable directories like import does + continue + ispkg = True + + if modname and '.' not in modname: + yielded[modname] = 1 + yield importer, prefix + modname, ispkg + # END COPY + +iter_modules = _iter_modules if py_version >= 34 else pkgutil.iter_modules + + class ImplicitNSInfo(object): """Stores information returned from an implicit namespace spec""" def __init__(self, name, paths): self.name = name self.paths = paths + +if is_py3: + all_suffixes = importlib.machinery.all_suffixes +else: + def all_suffixes(): + # Is deprecated and raises a warning in Python 3.6. + import imp + return [suffix for suffix, _, _ in imp.get_suffixes()] + + # unicode function try: unicode = unicode @@ -208,7 +268,7 @@ def use_metaclass(meta, *bases): """ Create a class with a metaclass. """ if not bases: bases = (object,) - return meta("HackClass", bases, {}) + return meta("Py2CompatibilityMetaClass", bases, {}) try: @@ -219,19 +279,37 @@ def use_metaclass(meta, *bases): encoding = 'ascii' -def u(string): +def u(string, errors='strict'): """Cast to unicode DAMMIT! Written because Python2 repr always implicitly casts to a string, so we have to cast back to a unicode (and we now that we always deal with valid unicode, because we check that in the beginning). """ - if is_py3: - return str(string) - - if not isinstance(string, unicode): - return unicode(str(string), 'UTF-8') + if isinstance(string, bytes): + return unicode(string, encoding='UTF-8', errors=errors) return string + +def cast_path(obj): + """ + Take a bytes or str path and cast it to unicode. + + Apparently it is perfectly fine to pass both byte and unicode objects into + the sys.path. This probably means that byte paths are normal at other + places as well. + + Since this just really complicates everything and Python 2.7 will be EOL + soon anyway, just go with always strings. + """ + return u(obj, errors='replace') + + +def force_unicode(obj): + # Intentionally don't mix those two up, because those two code paths might + # be different in the future (maybe windows?). + return cast_path(obj) + + try: import builtins # module name in python 3 except ImportError: @@ -242,11 +320,6 @@ def u(string): def literal_eval(string): - # py3.0, py3.1 and py32 don't support unicode literals. Support those, I - # don't want to write two versions of the tokenizer. - if is_py3 and sys.version_info.minor < 3: - if re.match('[uU][\'"]', string): - string = string[1:] return ast.literal_eval(string) @@ -260,6 +333,11 @@ def literal_eval(string): except NameError: FileNotFoundError = IOError +try: + NotADirectoryError = NotADirectoryError +except NameError: + NotADirectoryError = IOError + def no_unicode_pprint(dct): """ @@ -273,6 +351,13 @@ def no_unicode_pprint(dct): print(re.sub("u'", "'", s)) +def print_to_stderr(*args): + if is_py3: + eval("print(*args, file=sys.stderr)") + else: + print >> sys.stderr, args + + def utf8_repr(func): """ ``__repr__`` methods in Python 2 don't allow unicode objects to be @@ -289,3 +374,142 @@ def wrapper(self): return func else: return wrapper + + +if is_py3: + import queue +else: + import Queue as queue + + +import pickle +if sys.version_info[:2] == (3, 3): + """ + Monkeypatch the unpickler in Python 3.3. This is needed, because the + argument `encoding='bytes'` is not supported in 3.3, but badly needed to + communicate with Python 2. + """ + + class NewUnpickler(pickle._Unpickler): + dispatch = dict(pickle._Unpickler.dispatch) + + def _decode_string(self, value): + # Used to allow strings from Python 2 to be decoded either as + # bytes or Unicode strings. This should be used only with the + # STRING, BINSTRING and SHORT_BINSTRING opcodes. + if self.encoding == "bytes": + return value + else: + return value.decode(self.encoding, self.errors) + + def load_string(self): + data = self.readline()[:-1] + # Strip outermost quotes + if len(data) >= 2 and data[0] == data[-1] and data[0] in b'"\'': + data = data[1:-1] + else: + raise pickle.UnpicklingError("the STRING opcode argument must be quoted") + self.append(self._decode_string(pickle.codecs.escape_decode(data)[0])) + dispatch[pickle.STRING[0]] = load_string + + def load_binstring(self): + # Deprecated BINSTRING uses signed 32-bit length + len, = pickle.struct.unpack('>> defs[0].type + >>> defs = [str(d.type) for d in defs] # It's unicode and in Py2 has u before it. + >>> defs[0] 'module' - >>> defs[1].type + >>> defs[1] 'class' - >>> defs[2].type + >>> defs[2] 'instance' - >>> defs[3].type + >>> defs[3] 'function' """ @@ -159,7 +157,7 @@ def to_reverse(): except IndexError: pass - if name.api_type == 'module': + if name.api_type in 'module': module_contexts = name.infer() if module_contexts: module_context, = module_contexts @@ -259,7 +257,7 @@ def docstring(self, raw=False, fast=True): @property def description(self): """A textual description of the object.""" - return u(self._name.string_name) + return self._name.string_name @property def full_name(self): @@ -324,9 +322,9 @@ def get_param_names(context): param_names = param_names[1:] elif isinstance(context, (instance.AbstractInstanceContext, ClassContext)): if isinstance(context, ClassContext): - search = '__init__' + search = u'__init__' else: - search = '__call__' + search = u'__call__' names = context.get_function_slot_names(search) if not names: return [] @@ -377,8 +375,7 @@ def get_line_code(self, before=0, after=0): if self.in_builtin_module(): return '' - path = self._name.get_root_context().py__file__() - lines = parser_cache[self._evaluator.grammar._hashed][path].lines + lines = self._name.get_root_context().code_lines index = self._name.start_pos[0] - 1 start_index = max(index - before, 0) @@ -406,7 +403,7 @@ def _complete(self, like_name): and self.type == 'Function': append = '(' - if isinstance(self._name, ParamName) and self._stack is not None: + if self._name.api_type == 'param' and self._stack is not None: node_names = list(self._stack.get_node_names(self._evaluator.grammar._pgen_grammar)) if 'trailer' in node_names and 'argument' not in node_names: append += '=' @@ -525,7 +522,7 @@ def description(self): if typ == 'function': # For the description we want a short and a pythonic way. typ = 'def' - return typ + ' ' + u(self._name.string_name) + return typ + ' ' + self._name.string_name elif typ == 'param': code = search_ancestor(tree_name, 'param').get_code( include_prefix=False, @@ -533,7 +530,6 @@ def description(self): ) return typ + ' ' + code - definition = tree_name.get_definition() or tree_name # Remove the prefix, because that's not what we want for get_code # here. @@ -555,7 +551,7 @@ def desc_with_module(self): .. todo:: Add full path. This function is should return a `module.class.function` path. """ - position = '' if self.in_builtin_module else '@%s' % (self.line) + position = '' if self.in_builtin_module else '@%s' % self.line return "%s:%s%s" % (self.module_name, self.description, position) @memoize_method diff --git a/pythonFiles/jedi/api/completion.py b/pythonFiles/jedi/api/completion.py index 559a4d3f8320..c88a031e4679 100644 --- a/pythonFiles/jedi/api/completion.py +++ b/pythonFiles/jedi/api/completion.py @@ -2,6 +2,7 @@ from parso.python import tree from parso.tree import search_ancestor, Leaf +from jedi._compatibility import Parameter from jedi import debug from jedi import settings from jedi.api import classes @@ -18,24 +19,21 @@ def get_call_signature_param_names(call_signatures): for call_sig in call_signatures: for p in call_sig.params: # Allow protected access, because it's a public API. - tree_name = p._name.tree_name - # Compiled modules typically don't allow keyword arguments. - if tree_name is not None: - # Allow access on _definition here, because it's a - # public API and we don't want to make the internal - # Name object public. - tree_param = tree.search_ancestor(tree_name, 'param') - if tree_param.star_count == 0: # no *args/**kwargs - yield p._name + if p._name.get_kind() in (Parameter.POSITIONAL_OR_KEYWORD, + Parameter.KEYWORD_ONLY): + yield p._name def filter_names(evaluator, completion_names, stack, like_name): comp_dct = {} + if settings.case_insensitive_completion: + like_name = like_name.lower() for name in completion_names: - if settings.case_insensitive_completion \ - and name.string_name.lower().startswith(like_name.lower()) \ - or name.string_name.startswith(like_name): + string = name.string_name + if settings.case_insensitive_completion: + string = string.lower() + if string.startswith(like_name): new = classes.Completion( evaluator, name, @@ -208,7 +206,7 @@ def _get_context_completions(self): def _get_keyword_completion_names(self, keywords_): for k in keywords_: - yield keywords.keyword(self._evaluator, k).name + yield keywords.KeywordName(self._evaluator, k) def _global_completions(self): context = get_user_scope(self._module_context, self._position) diff --git a/pythonFiles/jedi/api/environment.py b/pythonFiles/jedi/api/environment.py new file mode 100644 index 000000000000..51b390f36ab4 --- /dev/null +++ b/pythonFiles/jedi/api/environment.py @@ -0,0 +1,393 @@ +""" +Environments are a way to activate different Python versions or Virtualenvs for +static analysis. The Python binary in that environment is going to be executed. +""" +import os +import re +import sys +import hashlib +import filecmp +from subprocess import PIPE +from collections import namedtuple +# When dropping Python 2.7 support we should consider switching to +# `shutil.which`. +from distutils.spawn import find_executable + +from jedi._compatibility import GeneralizedPopen +from jedi.cache import memoize_method, time_cache +from jedi.evaluate.compiled.subprocess import get_subprocess, \ + EvaluatorSameProcess, EvaluatorSubprocess + +import parso + +_VersionInfo = namedtuple('VersionInfo', 'major minor micro') + +_SUPPORTED_PYTHONS = ['3.6', '3.5', '3.4', '3.3', '2.7'] +_SAFE_PATHS = ['/usr/bin', '/usr/local/bin'] +_CURRENT_VERSION = '%s.%s' % (sys.version_info.major, sys.version_info.minor) + + +class InvalidPythonEnvironment(Exception): + """ + If you see this exception, the Python executable or Virtualenv you have + been trying to use is probably not a correct Python version. + """ + + +class _BaseEnvironment(object): + @memoize_method + def get_grammar(self): + version_string = '%s.%s' % (self.version_info.major, self.version_info.minor) + return parso.load_grammar(version=version_string) + + @property + def _sha256(self): + try: + return self._hash + except AttributeError: + self._hash = _calculate_sha256_for_file(self.executable) + return self._hash + + +class Environment(_BaseEnvironment): + """ + This class is supposed to be created by internal Jedi architecture. You + should not create it directly. Please use create_environment or the other + functions instead. It is then returned by that function. + """ + def __init__(self, path, executable): + self.path = os.path.abspath(path) + """ + The path to an environment, matches ``sys.prefix``. + """ + self.executable = os.path.abspath(executable) + """ + The Python executable, matches ``sys.executable``. + """ + self.version_info = self._get_version() + """ + + Like ``sys.version_info``. A tuple to show the current Environment's + Python version. + """ + + def _get_version(self): + try: + process = GeneralizedPopen([self.executable, '--version'], stdout=PIPE, stderr=PIPE) + stdout, stderr = process.communicate() + retcode = process.poll() + if retcode: + raise InvalidPythonEnvironment() + except OSError: + raise InvalidPythonEnvironment() + + # Until Python 3.4 wthe version string is part of stderr, after that + # stdout. + output = stdout + stderr + match = re.match(br'Python (\d+)\.(\d+)\.(\d+)', output) + if match is None: + raise InvalidPythonEnvironment("--version not working") + + return _VersionInfo(*[int(m) for m in match.groups()]) + + def __repr__(self): + version = '.'.join(str(i) for i in self.version_info) + return '<%s: %s in %s>' % (self.__class__.__name__, version, self.path) + + def get_evaluator_subprocess(self, evaluator): + return EvaluatorSubprocess(evaluator, self._get_subprocess()) + + def _get_subprocess(self): + return get_subprocess(self.executable) + + @memoize_method + def get_sys_path(self): + """ + The sys path for this environment. Does not include potential + modifications like ``sys.path.append``. + + :returns: list of str + """ + # It's pretty much impossible to generate the sys path without actually + # executing Python. The sys path (when starting with -S) itself depends + # on how the Python version was compiled (ENV variables). + # If you omit -S when starting Python (normal case), additionally + # site.py gets executed. + return self._get_subprocess().get_sys_path() + + +class SameEnvironment(Environment): + def __init__(self): + super(SameEnvironment, self).__init__(sys.prefix, sys.executable) + + def _get_version(self): + return _VersionInfo(*sys.version_info[:3]) + + +class InterpreterEnvironment(_BaseEnvironment): + def __init__(self): + self.version_info = _VersionInfo(*sys.version_info[:3]) + + def get_evaluator_subprocess(self, evaluator): + return EvaluatorSameProcess(evaluator) + + def get_sys_path(self): + return sys.path + + +def _get_virtual_env_from_var(): + var = os.environ.get('VIRTUAL_ENV') + if var is not None: + if var == sys.prefix: + return SameEnvironment() + + try: + return create_environment(var) + except InvalidPythonEnvironment: + pass + + +def _calculate_sha256_for_file(path): + sha256 = hashlib.sha256() + with open(path, 'rb') as f: + for block in iter(lambda: f.read(filecmp.BUFSIZE), b''): + sha256.update(block) + return sha256.hexdigest() + + +def get_default_environment(): + """ + Tries to return an active Virtualenv. If there is no VIRTUAL_ENV variable + set it will return the latest Python version installed on the system. This + makes it possible to use as many new Python features as possible when using + autocompletion and other functionality. + + :returns: :class:`Environment` + """ + virtual_env = _get_virtual_env_from_var() + if virtual_env is not None: + return virtual_env + + for environment in find_system_environments(): + return environment + + # If no Python Environment is found, use the environment we're already + # using. + return SameEnvironment() + + +@time_cache(seconds=10 * 60) # 10 Minutes +def get_cached_default_environment(): + return get_default_environment() + + +def find_virtualenvs(paths=None, **kwargs): + """ + :param paths: A list of paths in your file system to be scanned for + Virtualenvs. It will search in these paths and potentially execute the + Python binaries. Also the VIRTUAL_ENV variable will be checked if it + contains a valid Virtualenv. + :param safe: Default True. In case this is False, it will allow this + function to execute potential `python` environments. An attacker might + be able to drop an executable in a path this function is searching by + default. If the executable has not been installed by root, it will not + be executed. + + :yields: :class:`Environment` + """ + def py27_comp(paths=None, safe=True): + if paths is None: + paths = [] + + _used_paths = set() + + # Using this variable should be safe, because attackers might be able + # to drop files (via git) but not environment variables. + virtual_env = _get_virtual_env_from_var() + if virtual_env is not None: + yield virtual_env + _used_paths.add(virtual_env.path) + + for directory in paths: + if not os.path.isdir(directory): + continue + + directory = os.path.abspath(directory) + for path in os.listdir(directory): + path = os.path.join(directory, path) + if path in _used_paths: + # A path shouldn't be evaluated twice. + continue + _used_paths.add(path) + + try: + executable = _get_executable_path(path, safe=safe) + yield Environment(path, executable) + except InvalidPythonEnvironment: + pass + + return py27_comp(paths, **kwargs) + + +def find_system_environments(): + """ + Ignores virtualenvs and returns the Python versions that were installed on + your system. This might return nothing, if you're running Python e.g. from + a portable version. + + The environments are sorted from latest to oldest Python version. + + :yields: :class:`Environment` + """ + for version_string in _SUPPORTED_PYTHONS: + try: + yield get_system_environment(version_string) + except InvalidPythonEnvironment: + pass + + +# TODO: the logic to find the Python prefix is much more complicated than that. +# See Modules/getpath.c for UNIX and PC/getpathp.c for Windows in CPython's +# source code. A solution would be to deduce it by running the Python +# interpreter and printing the value of sys.prefix. +def _get_python_prefix(executable): + if os.name != 'nt': + return os.path.dirname(os.path.dirname(executable)) + landmark = os.path.join('Lib', 'os.py') + prefix = os.path.dirname(executable) + while prefix: + if os.path.join(prefix, landmark): + return prefix + prefix = os.path.dirname(prefix) + raise InvalidPythonEnvironment( + "Cannot find prefix of executable %s." % executable) + + +# TODO: this function should probably return a list of environments since +# multiple Python installations can be found on a system for the same version. +def get_system_environment(version): + """ + Return the first Python environment found for a string of the form 'X.Y' + where X and Y are the major and minor versions of Python. + + :raises: :exc:`.InvalidPythonEnvironment` + :returns: :class:`Environment` + """ + exe = find_executable('python' + version) + if exe: + if exe == sys.executable: + return SameEnvironment() + return Environment(_get_python_prefix(exe), exe) + + if os.name == 'nt': + for prefix, exe in _get_executables_from_windows_registry(version): + return Environment(prefix, exe) + raise InvalidPythonEnvironment("Cannot find executable python%s." % version) + + +def create_environment(path, safe=True): + """ + Make it possible to create an environment by hand. + + :raises: :exc:`.InvalidPythonEnvironment` + :returns: :class:`Environment` + """ + return Environment(path, _get_executable_path(path, safe=safe)) + + +def _get_executable_path(path, safe=True): + """ + Returns None if it's not actually a virtual env. + """ + + if os.name == 'nt': + python = os.path.join(path, 'Scripts', 'python.exe') + else: + python = os.path.join(path, 'bin', 'python') + if not os.path.exists(python): + raise InvalidPythonEnvironment("%s seems to be missing." % python) + + if safe and not _is_safe(python): + raise InvalidPythonEnvironment("The python binary is potentially unsafe.") + return python + + +def _get_executables_from_windows_registry(version): + # The winreg module is named _winreg on Python 2. + try: + import winreg + except ImportError: + import _winreg as winreg + + # TODO: support Python Anaconda. + sub_keys = [ + r'SOFTWARE\Python\PythonCore\{version}\InstallPath', + r'SOFTWARE\Wow6432Node\Python\PythonCore\{version}\InstallPath', + r'SOFTWARE\Python\PythonCore\{version}-32\InstallPath', + r'SOFTWARE\Wow6432Node\Python\PythonCore\{version}-32\InstallPath' + ] + for root_key in [winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE]: + for sub_key in sub_keys: + sub_key = sub_key.format(version=version) + try: + with winreg.OpenKey(root_key, sub_key) as key: + prefix = winreg.QueryValueEx(key, '')[0] + exe = os.path.join(prefix, 'python.exe') + if os.path.isfile(exe): + yield prefix, exe + except WindowsError: + pass + + +def _is_safe(executable_path): + # Resolve sym links. A venv typically is a symlink to a known Python + # binary. Only virtualenvs copy symlinks around. + real_path = os.path.realpath(executable_path) + + if _is_unix_safe_simple(real_path): + return True + + # Just check the list of known Python versions. If it's not in there, + # it's likely an attacker or some Python that was not properly + # installed in the system. + for environment in find_system_environments(): + if environment.executable == real_path: + return True + + # If the versions don't match, just compare the binary files. If we + # don't do that, only venvs will be working and not virtualenvs. + # venvs are symlinks while virtualenvs are actual copies of the + # Python files. + # This still means that if the system Python is updated and the + # virtualenv's Python is not (which is probably never going to get + # upgraded), it will not work with Jedi. IMO that's fine, because + # people should just be using venv. ~ dave + if environment._sha256 == _calculate_sha256_for_file(real_path): + return True + return False + + +def _is_unix_safe_simple(real_path): + if _is_unix_admin(): + # In case we are root, just be conservative and + # only execute known paths. + return any(real_path.startswith(p) for p in _SAFE_PATHS) + + uid = os.stat(real_path).st_uid + # The interpreter needs to be owned by root. This means that it wasn't + # written by a user and therefore attacking Jedi is not as simple. + # The attack could look like the following: + # 1. A user clones a repository. + # 2. The repository has an innocent looking folder called foobar. jedi + # searches for the folder and executes foobar/bin/python --version if + # there's also a foobar/bin/activate. + # 3. The bin/python is obviously not a python script but a bash script or + # whatever the attacker wants. + return uid == 0 + + +def _is_unix_admin(): + try: + return os.getuid() == 0 + except AttributeError: + return False # Windows diff --git a/pythonFiles/jedi/api/exceptions.py b/pythonFiles/jedi/api/exceptions.py new file mode 100644 index 000000000000..99cebdb7ddb5 --- /dev/null +++ b/pythonFiles/jedi/api/exceptions.py @@ -0,0 +1,10 @@ +class _JediError(Exception): + pass + + +class InternalError(_JediError): + pass + + +class WrongVersion(_JediError): + pass diff --git a/pythonFiles/jedi/api/helpers.py b/pythonFiles/jedi/api/helpers.py index 2c4d8e0d10fc..221fc4dfe0d4 100644 --- a/pythonFiles/jedi/api/helpers.py +++ b/pythonFiles/jedi/api/helpers.py @@ -7,12 +7,13 @@ from parso.python.parser import Parser from parso.python import tree -from parso import split_lines from jedi._compatibility import u from jedi.evaluate.syntax_tree import eval_atom from jedi.evaluate.helpers import evaluate_call_of_leaf -from jedi.cache import time_cache +from jedi.evaluate.compiled import get_string_context_set +from jedi.evaluate.base_context import ContextSet +from jedi.cache import call_signature_time_cache CompletionParts = namedtuple('CompletionParts', ['path', 'has_dot', 'name']) @@ -44,7 +45,7 @@ def _get_code(code_lines, start_pos, end_pos): lines[-1] = lines[-1][:end_pos[1]] # Remove first line indentation. lines[0] = lines[0][start_pos[1]:] - return '\n'.join(lines) + return ''.join(lines) class OnErrorLeaf(Exception): @@ -53,28 +54,11 @@ def error_leaf(self): return self.args[0] -def _is_on_comment(leaf, position): - comment_lines = split_lines(leaf.prefix) - difference = leaf.start_pos[0] - position[0] - prefix_start_pos = leaf.get_start_pos_of_prefix() - if difference == 0: - indent = leaf.start_pos[1] - elif position[0] == prefix_start_pos[0]: - indent = prefix_start_pos[1] - else: - indent = 0 - line = comment_lines[-difference - 1][:position[1] - indent] - return '#' in line - - def _get_code_for_stack(code_lines, module_node, position): leaf = module_node.get_leaf_for_position(position, include_prefixes=True) # It might happen that we're on whitespace or on a comment. This means # that we would not get the right leaf. if leaf.start_pos >= position: - if _is_on_comment(leaf, position): - return u('') - # If we're not on a comment simply get the previous leaf and proceed. leaf = leaf.get_previous_leaf() if leaf is None: @@ -125,6 +109,9 @@ def tokenize_without_endmarker(code): for token_ in tokens: if token_.string == safeword: raise EndMarkerReached() + elif token_.prefix.endswith(safeword): + # This happens with comments. + raise EndMarkerReached() else: yield token_ @@ -134,7 +121,7 @@ def tokenize_without_endmarker(code): # completion. # Use Z as a prefix because it's not part of a number suffix. safeword = 'ZZZ_USER_WANTS_TO_COMPLETE_HERE_WITH_JEDI' - code = code + safeword + code = code + ' ' + safeword p = Parser(grammar._pgen_grammar, error_recovery=True) try: @@ -208,6 +195,8 @@ def evaluate_goto_definition(evaluator, context, leaf): return evaluate_call_of_leaf(context, leaf) elif isinstance(leaf, tree.Literal): return eval_atom(context, leaf) + elif leaf.type in ('fstring_string', 'fstring_start', 'fstring_end'): + return get_string_context_set(evaluator) return [] @@ -294,14 +283,14 @@ def get_call_signature_details(module, position): return None -@time_cache("call_signatures_validity") +@call_signature_time_cache("call_signatures_validity") def cache_call_signatures(evaluator, context, bracket_leaf, code_lines, user_pos): """This function calculates the cache key.""" - index = user_pos[0] - 1 + line_index = user_pos[0] - 1 - before_cursor = code_lines[index][:user_pos[1]] - other_lines = code_lines[bracket_leaf.start_pos[0]:index] - whole = '\n'.join(other_lines + [before_cursor]) + before_cursor = code_lines[line_index][:user_pos[1]] + other_lines = code_lines[bracket_leaf.start_pos[0]:line_index] + whole = ''.join(other_lines + [before_cursor]) before_bracket = re.match(r'.*\(', whole, re.DOTALL) module_path = context.get_root_context().py__file__() diff --git a/pythonFiles/jedi/api/interpreter.py b/pythonFiles/jedi/api/interpreter.py index 202f345e94b9..c9b7bd69bbe0 100644 --- a/pythonFiles/jedi/api/interpreter.py +++ b/pythonFiles/jedi/api/interpreter.py @@ -5,24 +5,34 @@ from jedi.evaluate.context import ModuleContext from jedi.evaluate import compiled from jedi.evaluate.compiled import mixed +from jedi.evaluate.compiled.access import create_access_path from jedi.evaluate.base_context import Context +def _create(evaluator, obj): + return compiled.create_from_access_path( + evaluator, create_access_path(evaluator, obj) + ) + + class NamespaceObject(object): def __init__(self, dct): self.__dict__ = dct class MixedModuleContext(Context): - resets_positions = True type = 'mixed_module' - def __init__(self, evaluator, tree_module, namespaces, path): + def __init__(self, evaluator, tree_module, namespaces, path, code_lines): self.evaluator = evaluator self._namespaces = namespaces self._namespace_objects = [NamespaceObject(n) for n in namespaces] - self._module_context = ModuleContext(evaluator, tree_module, path=path) + self._module_context = ModuleContext( + evaluator, tree_module, + path=path, + code_lines=code_lines + ) self.tree_node = tree_module def get_node(self): @@ -33,7 +43,7 @@ def get_filters(self, *args, **kwargs): yield filter for namespace_obj in self._namespace_objects: - compiled_object = compiled.create(self.evaluator, namespace_obj) + compiled_object = _create(self.evaluator, namespace_obj) mixed_object = mixed.MixedObject( self.evaluator, parent_context=self, @@ -43,5 +53,9 @@ def get_filters(self, *args, **kwargs): for filter in mixed_object.get_filters(*args, **kwargs): yield filter + @property + def code_lines(self): + return self._module_context.code_lines + def __getattr__(self, name): return getattr(self._module_context, name) diff --git a/pythonFiles/jedi/api/keywords.py b/pythonFiles/jedi/api/keywords.py index a1bc4e7f8556..2991a0f81a56 100644 --- a/pythonFiles/jedi/api/keywords.py +++ b/pythonFiles/jedi/api/keywords.py @@ -1,10 +1,7 @@ import pydoc -import keyword -from jedi._compatibility import is_py3, is_py35 from jedi.evaluate.utils import ignored from jedi.evaluate.filters import AbstractNameDefinition -from parso.python.tree import Leaf try: from pydoc_data import topics as pydoc_topics @@ -17,87 +14,30 @@ # pydoc_data module in its file python3x.zip. pydoc_topics = None -if is_py3: - if is_py35: - # in python 3.5 async and await are not proper keywords, but for - # completion pursposes should as as though they are - keys = keyword.kwlist + ["async", "await"] - else: - keys = keyword.kwlist -else: - keys = keyword.kwlist + ['None', 'False', 'True'] - - -def has_inappropriate_leaf_keyword(pos, module): - relevant_errors = filter( - lambda error: error.first_pos[0] == pos[0], - module.error_statement_stacks) - - for error in relevant_errors: - if error.next_token in keys: - return True - - return False - - -def completion_names(evaluator, stmt, pos, module): - keyword_list = all_keywords(evaluator) - - if not isinstance(stmt, Leaf) or has_inappropriate_leaf_keyword(pos, module): - keyword_list = filter( - lambda keyword: not keyword.only_valid_as_leaf, - keyword_list - ) - return [keyword.name for keyword in keyword_list] - - -def all_keywords(evaluator, pos=(0, 0)): - return set([Keyword(evaluator, k, pos) for k in keys]) - - -def keyword(evaluator, string, pos=(0, 0)): - if string in keys: - return Keyword(evaluator, string, pos) - else: - return None - def get_operator(evaluator, string, pos): return Keyword(evaluator, string, pos) -keywords_only_valid_as_leaf = ( - 'continue', - 'break', -) - - class KeywordName(AbstractNameDefinition): - api_type = 'keyword' + api_type = u'keyword' def __init__(self, evaluator, name): self.evaluator = evaluator self.string_name = name - self.parent_context = evaluator.BUILTINS - - def eval(self): - return set() + self.parent_context = evaluator.builtins_module def infer(self): return [Keyword(self.evaluator, self.string_name, (0, 0))] class Keyword(object): - api_type = 'keyword' + api_type = u'keyword' def __init__(self, evaluator, name, pos): self.name = KeywordName(evaluator, name) self.start_pos = pos - self.parent = evaluator.BUILTINS - - @property - def only_valid_as_leaf(self): - return self.name.value in keywords_only_valid_as_leaf + self.parent = evaluator.builtins_module @property def names(self): diff --git a/pythonFiles/jedi/api/project.py b/pythonFiles/jedi/api/project.py new file mode 100644 index 000000000000..ca6992b5db7f --- /dev/null +++ b/pythonFiles/jedi/api/project.py @@ -0,0 +1,200 @@ +import os +import json + +from jedi._compatibility import FileNotFoundError, NotADirectoryError +from jedi.api.environment import SameEnvironment, \ + get_cached_default_environment +from jedi.api.exceptions import WrongVersion +from jedi._compatibility import force_unicode +from jedi.evaluate.sys_path import discover_buildout_paths +from jedi.evaluate.cache import evaluator_as_method_param_cache +from jedi.common.utils import traverse_parents + +_CONFIG_FOLDER = '.jedi' +_CONTAINS_POTENTIAL_PROJECT = 'setup.py', '.git', '.hg', 'requirements.txt', 'MANIFEST.in' + +_SERIALIZER_VERSION = 1 + + +def _remove_duplicates_from_path(path): + used = set() + for p in path: + if p in used: + continue + used.add(p) + yield p + + +def _force_unicode_list(lst): + return list(map(force_unicode, lst)) + + +class Project(object): + # TODO serialize environment + _serializer_ignore_attributes = ('_environment',) + _environment = None + + @staticmethod + def _get_json_path(base_path): + return os.path.join(base_path, _CONFIG_FOLDER, 'project.json') + + @classmethod + def load(cls, path): + """ + :param path: The path of the directory you want to use as a project. + """ + with open(cls._get_json_path(path)) as f: + version, data = json.load(f) + + if version == 1: + self = cls.__new__() + self.__dict__.update(data) + return self + else: + raise WrongVersion( + "The Jedi version of this project seems newer than what we can handle." + ) + + def __init__(self, path, **kwargs): + """ + :param path: The base path for this project. + :param sys_path: list of str. You can override the sys path if you + want. By default the ``sys.path.`` is generated from the + environment (virtualenvs, etc). + :param smart_sys_path: If this is enabled (default), adds paths from + local directories. Otherwise you will have to rely on your packages + being properly configured on the ``sys.path``. + """ + def py2_comp(path, environment=None, sys_path=None, + smart_sys_path=True, _django=False): + self._path = path + if isinstance(environment, SameEnvironment): + self._environment = environment + + self._sys_path = sys_path + self._smart_sys_path = smart_sys_path + self._django = _django + + py2_comp(path, **kwargs) + + def _get_base_sys_path(self, environment=None): + if self._sys_path is not None: + return self._sys_path + + # The sys path has not been set explicitly. + if environment is None: + environment = self.get_environment() + + sys_path = environment.get_sys_path() + try: + sys_path.remove('') + except ValueError: + pass + return sys_path + + @evaluator_as_method_param_cache() + def _get_sys_path(self, evaluator, environment=None): + """ + Keep this method private for all users of jedi. However internally this + one is used like a public method. + """ + suffixed = [] + prefixed = [] + + sys_path = list(self._get_base_sys_path(environment)) + if self._smart_sys_path: + prefixed.append(self._path) + + if evaluator.script_path is not None: + suffixed += discover_buildout_paths(evaluator, evaluator.script_path) + + traversed = [] + for parent in traverse_parents(evaluator.script_path): + traversed.append(parent) + if parent == self._path: + # Don't go futher than the project path. + break + + # AFAIK some libraries have imports like `foo.foo.bar`, which + # leads to the conclusion to by default prefer longer paths + # rather than shorter ones by default. + suffixed += reversed(traversed) + + if self._django: + prefixed.append(self._path) + + path = prefixed + sys_path + suffixed + return list(_force_unicode_list(_remove_duplicates_from_path(path))) + + def save(self): + data = dict(self.__dict__) + for attribute in self._serializer_ignore_attributes: + data.pop(attribute, None) + + with open(self._get_json_path(self._path), 'wb') as f: + return json.dump((_SERIALIZER_VERSION, data), f) + + def get_environment(self): + if self._environment is None: + return get_cached_default_environment() + + return self._environment + + def __repr__(self): + return '<%s: %s>' % (self.__class__.__name__, self._path) + + +def _is_potential_project(path): + for name in _CONTAINS_POTENTIAL_PROJECT: + if os.path.exists(os.path.join(path, name)): + return True + return False + + +def _is_django_path(directory): + """ Detects the path of the very well known Django library (if used) """ + try: + with open(os.path.join(directory, 'manage.py'), 'rb') as f: + return b"DJANGO_SETTINGS_MODULE" in f.read() + except (FileNotFoundError, NotADirectoryError): + return False + + return False + + +def get_default_project(path=None): + if path is None: + path = os.getcwd() + + check = os.path.realpath(path) + probable_path = None + first_no_init_file = None + for dir in traverse_parents(check, include_current=True): + try: + return Project.load(dir) + except (FileNotFoundError, NotADirectoryError): + pass + + if first_no_init_file is None: + if os.path.exists(os.path.join(dir, '__init__.py')): + # In the case that a __init__.py exists, it's in 99% just a + # Python package and the project sits at least one level above. + continue + else: + first_no_init_file = dir + + if _is_django_path(dir): + return Project(dir, _django=True) + + if probable_path is None and _is_potential_project(dir): + probable_path = dir + + if probable_path is not None: + # TODO search for setup.py etc + return Project(probable_path) + + if first_no_init_file is not None: + return Project(first_no_init_file) + + curdir = path if os.path.isdir(path) else os.path.dirname(path) + return Project(curdir) diff --git a/pythonFiles/jedi/api/replstartup.py b/pythonFiles/jedi/api/replstartup.py index 5bfcc8ce889e..4c44a626b775 100644 --- a/pythonFiles/jedi/api/replstartup.py +++ b/pythonFiles/jedi/api/replstartup.py @@ -11,8 +11,8 @@ [GCC 4.6.1] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import os - >>> os.path.join().split().in # doctest: +SKIP - os.path.join().split().index os.path.join().split().insert + >>> os.path.join('a', 'b').split().in # doctest: +SKIP + ..dex ..sert """ import jedi.utils diff --git a/pythonFiles/jedi/cache.py b/pythonFiles/jedi/cache.py index 01138e75a99b..6c0c2a830942 100644 --- a/pythonFiles/jedi/cache.py +++ b/pythonFiles/jedi/cache.py @@ -12,6 +12,7 @@ these variables are being cleaned after every API usage. """ import time +from functools import wraps from jedi import settings from parso.cache import parser_cache @@ -74,7 +75,7 @@ def clear_time_caches(delete_all=False): del tc[key] -def time_cache(time_add_setting): +def call_signature_time_cache(time_add_setting): """ This decorator works as follows: Call it with a setting and after that use the function with a callable that returns the key. @@ -106,8 +107,31 @@ def wrapper(*args, **kwargs): return _temp +def time_cache(seconds): + def decorator(func): + cache = {} + + @wraps(func) + def wrapper(*args, **kwargs): + key = (args, frozenset(kwargs.items())) + try: + created, result = cache[key] + if time.time() < created + seconds: + return result + except KeyError: + pass + result = func(*args, **kwargs) + cache[key] = time.time(), result + return result + + wrapper.clear_cache = lambda: cache.clear() + return wrapper + return decorator + + def memoize_method(method): """A normal memoize function.""" + @wraps(method) def wrapper(self, *args, **kwargs): cache_dict = self.__dict__.setdefault('_memoize_method_dct', {}) dct = cache_dict.setdefault(method, {}) diff --git a/pythonFiles/jedi/common/utils.py b/pythonFiles/jedi/common/utils.py new file mode 100644 index 000000000000..72726a4696e6 --- /dev/null +++ b/pythonFiles/jedi/common/utils.py @@ -0,0 +1,12 @@ +import os + + +def traverse_parents(path, include_current=False): + if not include_current: + path = os.path.dirname(path) + + previous = None + while previous != path: + yield path + previous = path + path = os.path.dirname(path) diff --git a/pythonFiles/jedi/debug.py b/pythonFiles/jedi/debug.py index 8caf1accb17f..a4fd86465bac 100644 --- a/pythonFiles/jedi/debug.py +++ b/pythonFiles/jedi/debug.py @@ -35,7 +35,7 @@ def _lazy_colorama_init(): # need this. initialise.atexit_done = True try: - init() + init(strip=False) except Exception: # Colorama fails with initializing under vim and is buggy in # version 0.3.6. diff --git a/pythonFiles/jedi/evaluate/__init__.py b/pythonFiles/jedi/evaluate/__init__.py index 20461071abdb..3ba52b89f408 100644 --- a/pythonFiles/jedi/evaluate/__init__.py +++ b/pythonFiles/jedi/evaluate/__init__.py @@ -17,7 +17,8 @@ ``eval_expr_stmt``. There's separate logic for autocompletion in the API, the evaluator is all about evaluating an expression. -TODO this paragraph is not what jedi does anymore. +TODO this paragraph is not what jedi does anymore, it's similar, but not the +same. Now you need to understand what follows after ``eval_expr_stmt``. Let's make an example:: @@ -62,10 +63,9 @@ that are not used are just being ignored. """ -import sys - from parso.python import tree import parso +from parso import python_bytes_to_unicode from jedi import debug from jedi import parser_utils @@ -86,31 +86,42 @@ class Evaluator(object): - def __init__(self, grammar, project): - self.grammar = grammar + def __init__(self, project, environment=None, script_path=None): + if environment is None: + environment = project.get_environment() + self.environment = environment + self.script_path = script_path + self.compiled_subprocess = environment.get_evaluator_subprocess(self) + self.grammar = environment.get_grammar() + self.latest_grammar = parso.load_grammar(version='3.6') self.memoize_cache = {} # for memoize decorators - # To memorize modules -> equals `sys.modules`. - self.modules = {} # like `sys.modules`. + self.module_cache = imports.ModuleCache() # does the job of `sys.modules`. self.compiled_cache = {} # see `evaluate.compiled.create()` self.inferred_element_counts = {} self.mixed_cache = {} # see `evaluate.compiled.mixed._create()` self.analysis = [] self.dynamic_params_depth = 0 self.is_analysis = False - self.python_version = sys.version_info[:2] self.project = project - project.add_evaluator(self) + self.access_cache = {} self.reset_recursion_limitations() + self.allow_different_encoding = True - # Constants - self.BUILTINS = compiled.get_special_object(self, 'BUILTINS') + @property + @evaluator_function_cache() + def builtins_module(self): + return compiled.get_special_object(self, u'BUILTINS') def reset_recursion_limitations(self): self.recursion_detector = recursion.RecursionDetector() self.execution_recursion_detector = recursion.ExecutionRecursionDetector(self) + def get_sys_path(self): + """Convenience function""" + return self.project._get_sys_path(self, environment=self.environment) + def eval_element(self, context, element): if isinstance(context, CompForContext): return eval_node(context, element) @@ -124,7 +135,11 @@ def eval_element(self, context, element): if_stmt = None break predefined_if_name_dict = context.predefined_names.get(if_stmt) - if predefined_if_name_dict is None and if_stmt and if_stmt.type == 'if_stmt': + # TODO there's a lot of issues with this one. We actually should do + # this in a different way. Caching should only be active in certain + # cases and this all sucks. + if predefined_if_name_dict is None and if_stmt \ + and if_stmt.type == 'if_stmt' and self.is_analysis: if_stmt_test = if_stmt.children[1] name_dicts = [{}] # If we already did a check, we don't want to do it again -> If @@ -357,3 +372,15 @@ def from_scope_node(scope_node, child_is_funcdef=None, is_nested=True, node_is_o node = node.parent scope_node = parent_scope(node) return from_scope_node(scope_node, is_nested=True, node_is_object=node_is_object) + + def parse_and_get_code(self, code=None, path=None, **kwargs): + if self.allow_different_encoding: + if code is None: + with open(path, 'rb') as f: + code = f.read() + code = python_bytes_to_unicode(code, errors='replace') + + return self.grammar.parse(code=code, path=path, **kwargs), code + + def parse(self, *args, **kwargs): + return self.parse_and_get_code(*args, **kwargs)[0] diff --git a/pythonFiles/jedi/evaluate/analysis.py b/pythonFiles/jedi/evaluate/analysis.py index c825e5fef9e9..ded4e9f20880 100644 --- a/pythonFiles/jedi/evaluate/analysis.py +++ b/pythonFiles/jedi/evaluate/analysis.py @@ -1,9 +1,12 @@ """ Module for statical analysis. """ -from jedi import debug from parso.python import tree + +from jedi._compatibility import force_unicode +from jedi import debug from jedi.evaluate.compiled import CompiledObject +from jedi.evaluate.helpers import is_string CODES = { @@ -114,9 +117,10 @@ def add_attribute_error(name_context, lookup_context, name): # instead of an error, if that happens. typ = Error if isinstance(lookup_context, AbstractInstanceContext): - slot_names = lookup_context.get_function_slot_names('__getattr__') + \ - lookup_context.get_function_slot_names('__getattribute__') + slot_names = lookup_context.get_function_slot_names(u'__getattr__') + \ + lookup_context.get_function_slot_names(u'__getattribute__') for n in slot_names: + # TODO do we even get here? if isinstance(name, CompiledInstanceName) and \ n.parent_context.obj == object: typ = Warning @@ -139,7 +143,7 @@ def _check_for_exception_catch(node_context, jedi_name, exception, payload=None) """ def check_match(cls, exception): try: - return isinstance(cls, CompiledObject) and issubclass(exception, cls.obj) + return isinstance(cls, CompiledObject) and cls.is_super_class(exception) except TypeError: return False @@ -160,7 +164,7 @@ def check_try_for_except(obj, exception): except_classes = node_context.eval_node(node) for cls in except_classes: from jedi.evaluate.context import iterable - if isinstance(cls, iterable.AbstractIterable) and \ + if isinstance(cls, iterable.Sequence) and \ cls.array_type == 'tuple': # multiple exceptions for lazy_context in cls.py__iter__(): @@ -189,8 +193,8 @@ def check_hasattr(node, suite): # Check name key, lazy_context = args[1] names = list(lazy_context.infer()) - assert len(names) == 1 and isinstance(names[0], CompiledObject) - assert names[0].obj == payload[1].value + assert len(names) == 1 and is_string(names[0]) + assert force_unicode(names[0].get_safe_value()) == payload[1].value # Check objects key, lazy_context = args[0] diff --git a/pythonFiles/jedi/evaluate/arguments.py b/pythonFiles/jedi/evaluate/arguments.py index 32b9238c6f4d..beab4c8c9541 100644 --- a/pythonFiles/jedi/evaluate/arguments.py +++ b/pythonFiles/jedi/evaluate/arguments.py @@ -10,6 +10,7 @@ from jedi.evaluate.context import iterable from jedi.evaluate.param import get_params, ExecutedParam + def try_iter_content(types, depth=0): """Helper method for static analysis.""" if depth > 10: @@ -29,6 +30,8 @@ def try_iter_content(types, depth=0): class AbstractArguments(object): context = None + argument_node = None + trailer = None def eval_argument_clinic(self, parameters): """Uses a list with argument clinic information (see PEP 436).""" @@ -95,29 +98,30 @@ def __init__(self, evaluator, context, argument_node, trailer=None): self.trailer = trailer # Can be None, e.g. in a class definition. def _split(self): - if isinstance(self.argument_node, (tuple, list)): - for el in self.argument_node: - yield 0, el - else: - if not (self.argument_node.type == 'arglist' or ( - # in python 3.5 **arg is an argument, not arglist - (self.argument_node.type == 'argument') and - self.argument_node.children[0] in ('*', '**'))): - yield 0, self.argument_node - return - - iterator = iter(self.argument_node.children) - for child in iterator: - if child == ',': - continue - elif child in ('*', '**'): - yield len(child.value), next(iterator) - elif child.type == 'argument' and \ - child.children[0] in ('*', '**'): - assert len(child.children) == 2 - yield len(child.children[0].value), child.children[1] - else: - yield 0, child + if self.argument_node is None: + return + + # Allow testlist here as well for Python2's class inheritance + # definitions. + if not (self.argument_node.type in ('arglist', 'testlist') or ( + # in python 3.5 **arg is an argument, not arglist + (self.argument_node.type == 'argument') and + self.argument_node.children[0] in ('*', '**'))): + yield 0, self.argument_node + return + + iterator = iter(self.argument_node.children) + for child in iterator: + if child == ',': + continue + elif child in ('*', '**'): + yield len(child.value), next(iterator) + elif child.type == 'argument' and \ + child.children[0] in ('*', '**'): + assert len(child.children) == 2 + yield len(child.children[0].value), child.children[1] + else: + yield 0, child def unpack(self, funcdef=None): named_args = [] @@ -126,7 +130,6 @@ def unpack(self, funcdef=None): arrays = self.context.eval_node(el) iterators = [_iterate_star_args(self.context, a, el, funcdef) for a in arrays] - iterators = list(iterators) for values in list(zip_longest(*iterators)): # TODO zip_longest yields None, that means this would raise # an exception? @@ -134,7 +137,7 @@ def unpack(self, funcdef=None): [v for v in values if v is not None] ) elif star_count == 2: - arrays = self._evaluator.eval_element(self.context, el) + arrays = self.context.eval_node(el) for dct in arrays: for key, values in _star_star_dict(self.context, dct, el, funcdef): yield key, values @@ -197,7 +200,11 @@ def get_calling_nodes(self): arguments = param.var_args break - return [arguments.argument_node or arguments.trailer] + if arguments.argument_node is not None: + return [arguments.argument_node] + if arguments.trailer is not None: + return [arguments.trailer] + return [] class ValuesArguments(AbstractArguments): @@ -235,7 +242,7 @@ def _star_star_dict(context, array, input_node, funcdef): # For now ignore this case. In the future add proper iterators and just # make one call without crazy isinstance checks. return {} - elif isinstance(array, iterable.AbstractIterable) and array.array_type == 'dict': + elif isinstance(array, iterable.Sequence) and array.array_type == 'dict': return array.exact_key_items() else: if funcdef is not None: diff --git a/pythonFiles/jedi/evaluate/base_context.py b/pythonFiles/jedi/evaluate/base_context.py index 693a99aae7aa..2c6fe6cd2c88 100644 --- a/pythonFiles/jedi/evaluate/base_context.py +++ b/pythonFiles/jedi/evaluate/base_context.py @@ -1,3 +1,11 @@ +""" +Contexts are the "values" that Python would return. However Contexts are at the +same time also the "contexts" that a user is currently sitting in. + +A ContextSet is typically used to specify the return of a function or any other +static analysis operation. In jedi there are always multiple returns and not +just one. +""" from parso.python.tree import ExprStmt, CompFor from jedi import debug @@ -63,10 +71,13 @@ def execute_evaluated(self, *value_list): arguments = ValuesArguments([ContextSet(value) for value in value_list]) return self.execute(arguments) - def iterate(self, contextualized_node=None): - debug.dbg('iterate') + def iterate(self, contextualized_node=None, is_async=False): + debug.dbg('iterate %s', self) try: - iter_method = self.py__iter__ + if is_async: + iter_method = self.py__aiter__ + else: + iter_method = self.py__iter__ except AttributeError: if contextualized_node is not None: from jedi.evaluate import analysis @@ -81,17 +92,22 @@ def iterate(self, contextualized_node=None): def get_item(self, index_contexts, contextualized_node): from jedi.evaluate.compiled import CompiledObject - from jedi.evaluate.context.iterable import Slice, AbstractIterable + from jedi.evaluate.context.iterable import Slice, Sequence result = ContextSet() for index in index_contexts: - if isinstance(index, (CompiledObject, Slice)): + if isinstance(index, Slice): index = index.obj + if isinstance(index, CompiledObject): + try: + index = index.get_safe_value() + except ValueError: + pass - if type(index) not in (float, int, str, unicode, slice, type(Ellipsis)): + if type(index) not in (float, int, str, unicode, slice, bytes): # If the index is not clearly defined, we have to get all the # possiblities. - if isinstance(self, AbstractIterable) and self.array_type == 'dict': + if isinstance(self, Sequence) and self.array_type == 'dict': result |= self.dict_values() else: result |= iterate_contexts(ContextSet(self)) @@ -139,10 +155,6 @@ def py__getattribute__(self, name_or_str, name_context=None, position=None, return f.filter_name(filters) return f.find(filters, attribute_lookup=not search_global) - return self.evaluator.find_types( - self, name_or_str, name_context, position, search_global, is_goto, - analysis_errors) - def create_context(self, node, node_is_context=False, node_is_object=False): return self.evaluator.create_context(self, node, node_is_context, node_is_object) @@ -169,14 +181,14 @@ def py__doc__(self, include_call_signature=False): return None -def iterate_contexts(contexts, contextualized_node=None): +def iterate_contexts(contexts, contextualized_node=None, is_async=False): """ Calls `iterate`, on all contexts but ignores the ordering and just returns all contexts that the iterate functions yield. """ return ContextSet.from_sets( lazy_context.infer() - for lazy_context in contexts.iterate(contextualized_node) + for lazy_context in contexts.iterate(contextualized_node, is_async=is_async) ) @@ -241,9 +253,9 @@ class ContextSet(BaseContextSet): def py__class__(self): return ContextSet.from_iterable(c.py__class__() for c in self._set) - def iterate(self, contextualized_node=None): + def iterate(self, contextualized_node=None, is_async=False): from jedi.evaluate.lazy_context import get_merged_lazy_context - type_iters = [c.iterate(contextualized_node) for c in self._set] + type_iters = [c.iterate(contextualized_node, is_async=is_async) for c in self._set] for lazy_contexts in zip_longest(*type_iters): yield get_merged_lazy_context( [l for l in lazy_contexts if l is not None] diff --git a/pythonFiles/jedi/evaluate/cache.py b/pythonFiles/jedi/evaluate/cache.py index b7c7cd7e979f..c619e698a3c8 100644 --- a/pythonFiles/jedi/evaluate/cache.py +++ b/pythonFiles/jedi/evaluate/cache.py @@ -59,7 +59,7 @@ def decorator(func): return decorator -def _memoize_meta_class(): +def evaluator_as_method_param_cache(): def decorator(call): return _memoize_default(second_arg_is_evaluator=True)(call) @@ -72,6 +72,6 @@ class CachedMetaClass(type): class initializations. Either you do it this way or with decorators, but with decorators you lose class access (isinstance, etc). """ - @_memoize_meta_class() + @evaluator_as_method_param_cache() def __call__(self, *args, **kwargs): return super(CachedMetaClass, self).__call__(*args, **kwargs) diff --git a/pythonFiles/jedi/evaluate/compiled/__init__.py b/pythonFiles/jedi/evaluate/compiled/__init__.py index f9f2e0781e13..357d26cc87fc 100644 --- a/pythonFiles/jedi/evaluate/compiled/__init__.py +++ b/pythonFiles/jedi/evaluate/compiled/__init__.py @@ -1,638 +1,39 @@ -""" -Imitate the parser representation. -""" -import inspect -import re -import sys -import os -import types -from functools import partial +from jedi._compatibility import unicode +from jedi.evaluate.compiled.context import CompiledObject, CompiledName, \ + CompiledObjectFilter, CompiledContextName, create_from_access_path, \ + create_from_name -from jedi._compatibility import builtins as _builtins, unicode, py_version -from jedi import debug -from jedi.cache import underscore_memoization, memoize_method -from jedi.evaluate.filters import AbstractFilter, AbstractNameDefinition, \ - ContextNameMixin -from jedi.evaluate.base_context import Context, ContextSet -from jedi.evaluate.lazy_context import LazyKnownContext -from jedi.evaluate.compiled.getattr_static import getattr_static -from . import fake - -_sep = os.path.sep -if os.path.altsep is not None: - _sep += os.path.altsep -_path_re = re.compile('(?:\.[^{0}]+|[{0}]__init__\.py)$'.format(re.escape(_sep))) -del _sep - -# Those types don't exist in typing. -MethodDescriptorType = type(str.replace) -WrapperDescriptorType = type(set.__iter__) -# `object.__subclasshook__` is an already executed descriptor. -object_class_dict = type.__dict__["__dict__"].__get__(object) -ClassMethodDescriptorType = type(object_class_dict['__subclasshook__']) - -ALLOWED_DESCRIPTOR_ACCESS = ( - types.FunctionType, - types.GetSetDescriptorType, - types.MemberDescriptorType, - MethodDescriptorType, - WrapperDescriptorType, - ClassMethodDescriptorType, - staticmethod, - classmethod, -) - -class CheckAttribute(object): - """Raises an AttributeError if the attribute X isn't available.""" - def __init__(self, func): - self.func = func - # Remove the py in front of e.g. py__call__. - self.check_name = func.__name__[2:] - - def __get__(self, instance, owner): - # This might raise an AttributeError. That's wanted. - if self.check_name == '__iter__': - # Python iterators are a bit strange, because there's no need for - # the __iter__ function as long as __getitem__ is defined (it will - # just start with __getitem__(0). This is especially true for - # Python 2 strings, where `str.__iter__` is not even defined. - try: - iter(instance.obj) - except TypeError: - raise AttributeError - else: - getattr(instance.obj, self.check_name) - return partial(self.func, instance) - - -class CompiledObject(Context): - path = None # modules have this attribute - set it to None. - used_names = lambda self: {} # To be consistent with modules. - - def __init__(self, evaluator, obj, parent_context=None, faked_class=None): - super(CompiledObject, self).__init__(evaluator, parent_context) - self.obj = obj - # This attribute will not be set for most classes, except for fakes. - self.tree_node = faked_class - - def get_root_node(self): - # To make things a bit easier with filters we add this method here. - return self.get_root_context() - - @CheckAttribute - def py__call__(self, params): - if inspect.isclass(self.obj): - from jedi.evaluate.context import CompiledInstance - return ContextSet(CompiledInstance(self.evaluator, self.parent_context, self, params)) - else: - return ContextSet.from_iterable(self._execute_function(params)) - - @CheckAttribute - def py__class__(self): - return create(self.evaluator, self.obj.__class__) - - @CheckAttribute - def py__mro__(self): - return (self,) + tuple(create(self.evaluator, cls) for cls in self.obj.__mro__[1:]) - - @CheckAttribute - def py__bases__(self): - return tuple(create(self.evaluator, cls) for cls in self.obj.__bases__) - - def py__bool__(self): - return bool(self.obj) - - def py__file__(self): - try: - return self.obj.__file__ - except AttributeError: - return None - - def is_class(self): - return inspect.isclass(self.obj) - - def py__doc__(self, include_call_signature=False): - return inspect.getdoc(self.obj) or '' - - def get_param_names(self): - obj = self.obj - try: - if py_version < 33: - raise ValueError("inspect.signature was introduced in 3.3") - if py_version == 34: - # In 3.4 inspect.signature are wrong for str and int. This has - # been fixed in 3.5. The signature of object is returned, - # because no signature was found for str. Here we imitate 3.5 - # logic and just ignore the signature if the magic methods - # don't match object. - # 3.3 doesn't even have the logic and returns nothing for str - # and classes that inherit from object. - user_def = inspect._signature_get_user_defined_method - if (inspect.isclass(obj) - and not user_def(type(obj), '__init__') - and not user_def(type(obj), '__new__') - and (obj.__init__ != object.__init__ - or obj.__new__ != object.__new__)): - raise ValueError - - signature = inspect.signature(obj) - except ValueError: # Has no signature - params_str, ret = self._parse_function_doc() - tokens = params_str.split(',') - if inspect.ismethoddescriptor(obj): - tokens.insert(0, 'self') - for p in tokens: - parts = p.strip().split('=') - yield UnresolvableParamName(self, parts[0]) - else: - for signature_param in signature.parameters.values(): - yield SignatureParamName(self, signature_param) - - def __repr__(self): - return '<%s: %s>' % (self.__class__.__name__, repr(self.obj)) - - @underscore_memoization - def _parse_function_doc(self): - doc = self.py__doc__() - if doc is None: - return '', '' - - return _parse_function_doc(doc) - - @property - def api_type(self): - obj = self.obj - if inspect.isclass(obj): - return 'class' - elif inspect.ismodule(obj): - return 'module' - elif inspect.isbuiltin(obj) or inspect.ismethod(obj) \ - or inspect.ismethoddescriptor(obj) or inspect.isfunction(obj): - return 'function' - # Everything else... - return 'instance' - - @property - def type(self): - """Imitate the tree.Node.type values.""" - cls = self._get_class() - if inspect.isclass(cls): - return 'classdef' - elif inspect.ismodule(cls): - return 'file_input' - elif inspect.isbuiltin(cls) or inspect.ismethod(cls) or \ - inspect.ismethoddescriptor(cls): - return 'funcdef' - - @underscore_memoization - def _cls(self): - """ - We used to limit the lookups for instantiated objects like list(), but - this is not the case anymore. Python itself - """ - # Ensures that a CompiledObject is returned that is not an instance (like list) - return self - - def _get_class(self): - if not fake.is_class_instance(self.obj) or \ - inspect.ismethoddescriptor(self.obj): # slots - return self.obj - - try: - return self.obj.__class__ - except AttributeError: - # happens with numpy.core.umath._UFUNC_API (you get it - # automatically by doing `import numpy`. - return type - - def get_filters(self, search_global=False, is_instance=False, - until_position=None, origin_scope=None): - yield self._ensure_one_filter(is_instance) - - @memoize_method - def _ensure_one_filter(self, is_instance): - """ - search_global shouldn't change the fact that there's one dict, this way - there's only one `object`. - """ - return CompiledObjectFilter(self.evaluator, self, is_instance) - - @CheckAttribute - def py__getitem__(self, index): - if type(self.obj) not in (str, list, tuple, unicode, bytes, bytearray, dict): - # Get rid of side effects, we won't call custom `__getitem__`s. - return ContextSet() - - return ContextSet(create(self.evaluator, self.obj[index])) - - @CheckAttribute - def py__iter__(self): - if type(self.obj) not in (str, list, tuple, unicode, bytes, bytearray, dict): - # Get rid of side effects, we won't call custom `__getitem__`s. - return - - for i, part in enumerate(self.obj): - if i > 20: - # Should not go crazy with large iterators - break - yield LazyKnownContext(create(self.evaluator, part)) - - def py__name__(self): - try: - return self._get_class().__name__ - except AttributeError: - return None - - @property - def name(self): - try: - name = self._get_class().__name__ - except AttributeError: - name = repr(self.obj) - return CompiledContextName(self, name) - - def _execute_function(self, params): - from jedi.evaluate import docstrings - if self.type != 'funcdef': - return - for name in self._parse_function_doc()[1].split(): - try: - bltn_obj = getattr(_builtins, name) - except AttributeError: - continue - else: - if bltn_obj is None: - # We want to evaluate everything except None. - # TODO do we? - continue - bltn_obj = create(self.evaluator, bltn_obj) - for result in bltn_obj.execute(params): - yield result - for type_ in docstrings.infer_return_types(self): - yield type_ - - def get_self_attributes(self): - return [] # Instance compatibility - - def get_imports(self): - return [] # Builtins don't have imports - - def dict_values(self): - return ContextSet.from_iterable( - create(self.evaluator, v) for v in self.obj.values() - ) - - -class CompiledName(AbstractNameDefinition): - def __init__(self, evaluator, parent_context, name): - self._evaluator = evaluator - self.parent_context = parent_context - self.string_name = name - - def __repr__(self): - try: - name = self.parent_context.name # __name__ is not defined all the time - except AttributeError: - name = None - return '<%s: (%s).%s>' % (self.__class__.__name__, name, self.string_name) - - @property - def api_type(self): - return next(iter(self.infer())).api_type - - @underscore_memoization - def infer(self): - module = self.parent_context.get_root_context() - return ContextSet(_create_from_name( - self._evaluator, module, self.parent_context, self.string_name - )) - - -class SignatureParamName(AbstractNameDefinition): - api_type = 'param' - - def __init__(self, compiled_obj, signature_param): - self.parent_context = compiled_obj.parent_context - self._signature_param = signature_param - - @property - def string_name(self): - return self._signature_param.name - - def infer(self): - p = self._signature_param - evaluator = self.parent_context.evaluator - contexts = ContextSet() - if p.default is not p.empty: - contexts = ContextSet(create(evaluator, p.default)) - if p.annotation is not p.empty: - annotation = create(evaluator, p.annotation) - contexts |= annotation.execute_evaluated() - return contexts - - -class UnresolvableParamName(AbstractNameDefinition): - api_type = 'param' - - def __init__(self, compiled_obj, name): - self.parent_context = compiled_obj.parent_context - self.string_name = name - - def infer(self): - return ContextSet() - - -class CompiledContextName(ContextNameMixin, AbstractNameDefinition): - def __init__(self, context, name): - self.string_name = name - self._context = context - self.parent_context = context.parent_context - - -class EmptyCompiledName(AbstractNameDefinition): - """ - Accessing some names will raise an exception. To avoid not having any - completions, just give Jedi the option to return this object. It infers to - nothing. - """ - def __init__(self, evaluator, name): - self.parent_context = evaluator.BUILTINS - self.string_name = name - - def infer(self): - return ContextSet() - - -class CompiledObjectFilter(AbstractFilter): - name_class = CompiledName - - def __init__(self, evaluator, compiled_object, is_instance=False): - self._evaluator = evaluator - self._compiled_object = compiled_object - self._is_instance = is_instance - - @memoize_method - def get(self, name): - name = str(name) - obj = self._compiled_object.obj - try: - attr, is_get_descriptor = getattr_static(obj, name) - except AttributeError: - return [] - else: - if is_get_descriptor \ - and not type(attr) in ALLOWED_DESCRIPTOR_ACCESS: - # In case of descriptors that have get methods we cannot return - # it's value, because that would mean code execution. - return [EmptyCompiledName(self._evaluator, name)] - if self._is_instance and name not in dir(obj): - return [] - return [self._create_name(name)] - - def values(self): - obj = self._compiled_object.obj - - names = [] - for name in dir(obj): - names += self.get(name) - - is_instance = self._is_instance or fake.is_class_instance(obj) - # ``dir`` doesn't include the type names. - if not inspect.ismodule(obj) and (obj is not type) and not is_instance: - for filter in create(self._evaluator, type).get_filters(): - names += filter.values() - return names - - def _create_name(self, name): - return self.name_class(self._evaluator, self._compiled_object, name) - - -def dotted_from_fs_path(fs_path, sys_path): - """ - Changes `/usr/lib/python3.4/email/utils.py` to `email.utils`. I.e. - compares the path with sys.path and then returns the dotted_path. If the - path is not in the sys.path, just returns None. - """ - if os.path.basename(fs_path).startswith('__init__.'): - # We are calculating the path. __init__ files are not interesting. - fs_path = os.path.dirname(fs_path) - - # prefer - # - UNIX - # /path/to/pythonX.Y/lib-dynload - # /path/to/pythonX.Y/site-packages - # - Windows - # C:\path\to\DLLs - # C:\path\to\Lib\site-packages - # over - # - UNIX - # /path/to/pythonX.Y - # - Windows - # C:\path\to\Lib - path = '' - for s in sys_path: - if (fs_path.startswith(s) and len(path) < len(s)): - path = s - - # - Window - # X:\path\to\lib-dynload/datetime.pyd => datetime - module_path = fs_path[len(path):].lstrip(os.path.sep).lstrip('/') - # - Window - # Replace like X:\path\to\something/foo/bar.py - return _path_re.sub('', module_path).replace(os.path.sep, '.').replace('/', '.') - - -def load_module(evaluator, path=None, name=None): - sys_path = list(evaluator.project.sys_path) - if path is not None: - dotted_path = dotted_from_fs_path(path, sys_path=sys_path) - else: - dotted_path = name - - temp, sys.path = sys.path, sys_path - try: - __import__(dotted_path) - except RuntimeError: - if 'PySide' in dotted_path or 'PyQt' in dotted_path: - # RuntimeError: the PyQt4.QtCore and PyQt5.QtCore modules both wrap - # the QObject class. - # See https://github.com/davidhalter/jedi/pull/483 - return None - raise - except ImportError: - # If a module is "corrupt" or not really a Python module or whatever. - debug.warning('Module %s not importable in path %s.', dotted_path, path) - return None - finally: - sys.path = temp - - # Just access the cache after import, because of #59 as well as the very - # complicated import structure of Python. - module = sys.modules[dotted_path] - - return create(evaluator, module) - - -docstr_defaults = { - 'floating point number': 'float', - 'character': 'str', - 'integer': 'int', - 'dictionary': 'dict', - 'string': 'str', -} +def builtin_from_name(evaluator, string): + builtins = evaluator.builtins_module + return create_from_name(evaluator, builtins, string) -def _parse_function_doc(doc): +def create_simple_object(evaluator, obj): """ - Takes a function and returns the params and return value as a tuple. - This is nothing more than a docstring parser. - - TODO docstrings like utime(path, (atime, mtime)) and a(b [, b]) -> None - TODO docstrings like 'tuple of integers' + Only allows creations of objects that are easily picklable across Python + versions. """ - # parse round parentheses: def func(a, (b,c)) - try: - count = 0 - start = doc.index('(') - for i, s in enumerate(doc[start:]): - if s == '(': - count += 1 - elif s == ')': - count -= 1 - if count == 0: - end = start + i - break - param_str = doc[start + 1:end] - except (ValueError, UnboundLocalError): - # ValueError for doc.index - # UnboundLocalError for undefined end in last line - debug.dbg('no brackets found - no param') - end = 0 - param_str = '' - else: - # remove square brackets, that show an optional param ( = None) - def change_options(m): - args = m.group(1).split(',') - for i, a in enumerate(args): - if a and '=' not in a: - args[i] += '=None' - return ','.join(args) - - while True: - param_str, changes = re.subn(r' ?\[([^\[\]]+)\]', - change_options, param_str) - if changes == 0: - break - param_str = param_str.replace('-', '_') # see: isinstance.__doc__ - - # parse return value - r = re.search('-[>-]* ', doc[end:end + 7]) - if r is None: - ret = '' - else: - index = end + r.end() - # get result type, which can contain newlines - pattern = re.compile(r'(,\n|[^\n-])+') - ret_str = pattern.match(doc, index).group(0).strip() - # New object -> object() - ret_str = re.sub(r'[nN]ew (.*)', r'\1()', ret_str) - - ret = docstr_defaults.get(ret_str, ret_str) - - return param_str, ret - - -def _create_from_name(evaluator, module, compiled_object, name): - obj = compiled_object.obj - faked = None - try: - faked = fake.get_faked(evaluator, module, obj, parent_context=compiled_object, name=name) - if faked.type == 'funcdef': - from jedi.evaluate.context.function import FunctionContext - return FunctionContext(evaluator, compiled_object, faked) - except fake.FakeDoesNotExist: - pass - - try: - obj = getattr(obj, name) - except AttributeError: - # Happens e.g. in properties of - # PyQt4.QtGui.QStyleOptionComboBox.currentText - # -> just set it to None - obj = None - return create(evaluator, obj, parent_context=compiled_object, faked=faked) - - -def builtin_from_name(evaluator, string): - bltn_obj = getattr(_builtins, string) - return create(evaluator, bltn_obj) - - -def _a_generator(foo): - """Used to have an object to return for generators.""" - yield 42 - yield foo - - -_SPECIAL_OBJECTS = { - 'FUNCTION_CLASS': type(load_module), - 'METHOD_CLASS': type(CompiledObject.is_class), - 'MODULE_CLASS': type(os), - 'GENERATOR_OBJECT': _a_generator(1.0), - 'BUILTINS': _builtins, -} + assert isinstance(obj, (int, float, str, bytes, unicode, slice, complex)) + return create_from_access_path( + evaluator, + evaluator.compiled_subprocess.create_simple_object(obj) + ) def get_special_object(evaluator, identifier): - obj = _SPECIAL_OBJECTS[identifier] - return create(evaluator, obj, parent_context=create(evaluator, _builtins)) + return create_from_access_path( + evaluator, + evaluator.compiled_subprocess.get_special_object(identifier) + ) -def compiled_objects_cache(attribute_name): - def decorator(func): - """ - This decorator caches just the ids, oopposed to caching the object itself. - Caching the id has the advantage that an object doesn't need to be - hashable. - """ - def wrapper(evaluator, obj, parent_context=None, module=None, faked=None): - cache = getattr(evaluator, attribute_name) - # Do a very cheap form of caching here. - key = id(obj), id(parent_context) - try: - return cache[key][0] - except KeyError: - # TODO this whole decorator is way too ugly - result = func(evaluator, obj, parent_context, module, faked) - # Need to cache all of them, otherwise the id could be overwritten. - cache[key] = result, obj, parent_context, module, faked - return result - return wrapper +def get_string_context_set(evaluator): + return builtin_from_name(evaluator, u'str').execute_evaluated() - return decorator - -@compiled_objects_cache('compiled_cache') -def create(evaluator, obj, parent_context=None, module=None, faked=None): - """ - A very weird interface class to this module. The more options provided the - more acurate loading compiled objects is. - """ - if inspect.ismodule(obj): - if parent_context is not None: - # Modules don't have parents, be careful with caching: recurse. - return create(evaluator, obj) - else: - if parent_context is None and obj is not _builtins: - return create(evaluator, obj, create(evaluator, _builtins)) - - try: - faked = fake.get_faked(evaluator, module, obj, parent_context=parent_context) - if faked.type == 'funcdef': - from jedi.evaluate.context.function import FunctionContext - return FunctionContext(evaluator, parent_context, faked) - except fake.FakeDoesNotExist: - pass - - return CompiledObject(evaluator, obj, parent_context, faked) +def load_module(evaluator, **kwargs): + access_path = evaluator.compiled_subprocess.load_module(**kwargs) + if access_path is None: + return None + return create_from_access_path(evaluator, access_path) diff --git a/pythonFiles/jedi/evaluate/compiled/access.py b/pythonFiles/jedi/evaluate/compiled/access.py new file mode 100644 index 000000000000..97a8dd0b5c3d --- /dev/null +++ b/pythonFiles/jedi/evaluate/compiled/access.py @@ -0,0 +1,490 @@ +import inspect +import types +import sys +from textwrap import dedent +import operator as op +from collections import namedtuple + +from jedi import debug +from jedi._compatibility import unicode, is_py3, is_py34, builtins, \ + py_version, force_unicode, print_to_stderr +from jedi.evaluate.compiled.getattr_static import getattr_static +from jedi.evaluate.utils import dotted_from_fs_path + + +MethodDescriptorType = type(str.replace) +# These are not considered classes and access is granted even though they have +# a __class__ attribute. +NOT_CLASS_TYPES = ( + types.BuiltinFunctionType, + types.CodeType, + types.FrameType, + types.FunctionType, + types.GeneratorType, + types.GetSetDescriptorType, + types.LambdaType, + types.MemberDescriptorType, + types.MethodType, + types.ModuleType, + types.TracebackType, + MethodDescriptorType +) + +if is_py3: + NOT_CLASS_TYPES += ( + types.MappingProxyType, + types.SimpleNamespace + ) + if is_py34: + NOT_CLASS_TYPES += (types.DynamicClassAttribute,) + + +# Those types don't exist in typing. +MethodDescriptorType = type(str.replace) +WrapperDescriptorType = type(set.__iter__) +# `object.__subclasshook__` is an already executed descriptor. +object_class_dict = type.__dict__["__dict__"].__get__(object) +ClassMethodDescriptorType = type(object_class_dict['__subclasshook__']) + +def _a_generator(foo): + """Used to have an object to return for generators.""" + yield 42 + yield foo + + +_sentinel = object() + +# Maps Python syntax to the operator module. +COMPARISON_OPERATORS = { + '==': op.eq, + '!=': op.ne, + 'is': op.is_, + 'is not': op.is_not, + '<': op.lt, + '<=': op.le, + '>': op.gt, + '>=': op.ge, +} + +_OPERATORS = { + '+': op.add, + '-': op.sub, +} +_OPERATORS.update(COMPARISON_OPERATORS) + +ALLOWED_DESCRIPTOR_ACCESS = ( + types.FunctionType, + types.GetSetDescriptorType, + types.MemberDescriptorType, + MethodDescriptorType, + WrapperDescriptorType, + ClassMethodDescriptorType, + staticmethod, + classmethod, +) + + +def safe_getattr(obj, name, default=_sentinel): + try: + attr, is_get_descriptor = getattr_static(obj, name) + except AttributeError: + if default is _sentinel: + raise + return default + else: + if type(attr) in ALLOWED_DESCRIPTOR_ACCESS: + # In case of descriptors that have get methods we cannot return + # it's value, because that would mean code execution. + return getattr(obj, name) + return attr + + +SignatureParam = namedtuple( + 'SignatureParam', + 'name has_default default has_annotation annotation kind_name' +) + + +def compiled_objects_cache(attribute_name): + def decorator(func): + """ + This decorator caches just the ids, oopposed to caching the object itself. + Caching the id has the advantage that an object doesn't need to be + hashable. + """ + def wrapper(evaluator, obj, parent_context=None): + cache = getattr(evaluator, attribute_name) + # Do a very cheap form of caching here. + key = id(obj) + try: + cache[key] + return cache[key][0] + except KeyError: + # TODO wuaaaarrghhhhhhhh + if attribute_name == 'mixed_cache': + result = func(evaluator, obj, parent_context) + else: + result = func(evaluator, obj) + # Need to cache all of them, otherwise the id could be overwritten. + cache[key] = result, obj, parent_context + return result + return wrapper + + return decorator + + +def create_access(evaluator, obj): + return evaluator.compiled_subprocess.get_or_create_access_handle(obj) + + +def load_module(evaluator, path=None, name=None, sys_path=None): + if sys_path is None: + sys_path = list(evaluator.get_sys_path()) + if path is not None: + dotted_path = dotted_from_fs_path(path, sys_path=sys_path) + else: + dotted_path = name + + temp, sys.path = sys.path, sys_path + try: + __import__(dotted_path) + except ImportError: + # If a module is "corrupt" or not really a Python module or whatever. + debug.warning('Module %s not importable in path %s.', dotted_path, path) + return None + except Exception: + # Since __import__ pretty much makes code execution possible, just + # catch any error here and print it. + import traceback + print_to_stderr("Cannot import:\n%s" % traceback.format_exc()) + return None + finally: + sys.path = temp + + # Just access the cache after import, because of #59 as well as the very + # complicated import structure of Python. + module = sys.modules[dotted_path] + return create_access_path(evaluator, module) + + +class AccessPath(object): + def __init__(self, accesses): + self.accesses = accesses + + # Writing both of these methods here looks a bit ridiculous. However with + # the differences of Python 2/3 it's actually necessary, because we will + # otherwise have a accesses attribute that is bytes instead of unicode. + def __getstate__(self): + return self.accesses + + def __setstate__(self, value): + self.accesses = value + + +def create_access_path(evaluator, obj): + access = create_access(evaluator, obj) + return AccessPath(access.get_access_path_tuples()) + + +def _force_unicode_decorator(func): + return lambda *args, **kwargs: force_unicode(func(*args, **kwargs)) + + +class DirectObjectAccess(object): + def __init__(self, evaluator, obj): + self._evaluator = evaluator + self._obj = obj + + def __repr__(self): + return '%s(%s)' % (self.__class__.__name__, self.get_repr()) + + def _create_access(self, obj): + return create_access(self._evaluator, obj) + + def _create_access_path(self, obj): + return create_access_path(self._evaluator, obj) + + def py__bool__(self): + return bool(self._obj) + + def py__file__(self): + try: + return self._obj.__file__ + except AttributeError: + return None + + def py__doc__(self, include_call_signature=False): + return force_unicode(inspect.getdoc(self._obj)) or u'' + + def py__name__(self): + if not _is_class_instance(self._obj) or \ + inspect.ismethoddescriptor(self._obj): # slots + cls = self._obj + else: + try: + cls = self._obj.__class__ + except AttributeError: + # happens with numpy.core.umath._UFUNC_API (you get it + # automatically by doing `import numpy`. + return None + + try: + return force_unicode(cls.__name__) + except AttributeError: + return None + + def py__mro__accesses(self): + return tuple(self._create_access_path(cls) for cls in self._obj.__mro__[1:]) + + def py__getitem__(self, index): + if type(self._obj) not in (str, list, tuple, unicode, bytes, bytearray, dict): + # Get rid of side effects, we won't call custom `__getitem__`s. + return None + + return self._create_access_path(self._obj[index]) + + def py__iter__list(self): + if type(self._obj) not in (str, list, tuple, unicode, bytes, bytearray, dict): + # Get rid of side effects, we won't call custom `__getitem__`s. + return [] + + lst = [] + for i, part in enumerate(self._obj): + if i > 20: + # Should not go crazy with large iterators + break + lst.append(self._create_access_path(part)) + return lst + + def py__class__(self): + return self._create_access_path(self._obj.__class__) + + def py__bases__(self): + return [self._create_access_path(base) for base in self._obj.__bases__] + + @_force_unicode_decorator + def get_repr(self): + builtins = 'builtins', '__builtin__' + + if inspect.ismodule(self._obj): + return repr(self._obj) + # Try to avoid execution of the property. + if safe_getattr(self._obj, '__module__', default='') in builtins: + return repr(self._obj) + + type_ = type(self._obj) + if type_ == type: + return type.__repr__(self._obj) + + if safe_getattr(type_, '__module__', default='') in builtins: + # Allow direct execution of repr for builtins. + return repr(self._obj) + return object.__repr__(self._obj) + + def is_class(self): + return inspect.isclass(self._obj) + + def ismethoddescriptor(self): + return inspect.ismethoddescriptor(self._obj) + + def dir(self): + return list(map(force_unicode, dir(self._obj))) + + def has_iter(self): + try: + iter(self._obj) + return True + except TypeError: + return False + + def is_allowed_getattr(self, name): + # TODO this API is ugly. + try: + attr, is_get_descriptor = getattr_static(self._obj, name) + except AttributeError: + return False, False + else: + if is_get_descriptor and type(attr) not in ALLOWED_DESCRIPTOR_ACCESS: + # In case of descriptors that have get methods we cannot return + # it's value, because that would mean code execution. + return True, True + return True, False + + def getattr(self, name, default=_sentinel): + try: + return self._create_access(getattr(self._obj, name)) + except AttributeError: + # Happens e.g. in properties of + # PyQt4.QtGui.QStyleOptionComboBox.currentText + # -> just set it to None + if default is _sentinel: + raise + return self._create_access(default) + + def get_safe_value(self): + if type(self._obj) in (bool, bytes, float, int, str, unicode, slice): + return self._obj + raise ValueError("Object is type %s and not simple" % type(self._obj)) + + def get_api_type(self): + obj = self._obj + if self.is_class(): + return u'class' + elif inspect.ismodule(obj): + return u'module' + elif inspect.isbuiltin(obj) or inspect.ismethod(obj) \ + or inspect.ismethoddescriptor(obj) or inspect.isfunction(obj): + return u'function' + # Everything else... + return u'instance' + + def get_access_path_tuples(self): + accesses = [create_access(self._evaluator, o) for o in self._get_objects_path()] + return [(access.py__name__(), access) for access in accesses] + + def _get_objects_path(self): + def get(): + obj = self._obj + yield obj + try: + obj = obj.__objclass__ + except AttributeError: + pass + else: + yield obj + + try: + # Returns a dotted string path. + imp_plz = obj.__module__ + except AttributeError: + # Unfortunately in some cases like `int` there's no __module__ + if not inspect.ismodule(obj): + yield builtins + else: + if imp_plz is None: + # Happens for example in `(_ for _ in []).send.__module__`. + yield builtins + else: + try: + # TODO use sys.modules, __module__ can be faked. + yield sys.modules[imp_plz] + except KeyError: + # __module__ can be something arbitrary that doesn't exist. + yield builtins + + return list(reversed(list(get()))) + + def execute_operation(self, other_access_handle, operator): + other_access = other_access_handle.access + op = _OPERATORS[operator] + return self._create_access_path(op(self._obj, other_access._obj)) + + def needs_type_completions(self): + return inspect.isclass(self._obj) and self._obj != type + + def get_signature_params(self): + obj = self._obj + if py_version < 33: + raise ValueError("inspect.signature was introduced in 3.3") + if py_version == 34: + # In 3.4 inspect.signature are wrong for str and int. This has + # been fixed in 3.5. The signature of object is returned, + # because no signature was found for str. Here we imitate 3.5 + # logic and just ignore the signature if the magic methods + # don't match object. + # 3.3 doesn't even have the logic and returns nothing for str + # and classes that inherit from object. + user_def = inspect._signature_get_user_defined_method + if (inspect.isclass(obj) + and not user_def(type(obj), '__init__') + and not user_def(type(obj), '__new__') + and (obj.__init__ != object.__init__ + or obj.__new__ != object.__new__)): + raise ValueError + + try: + signature = inspect.signature(obj) + except (RuntimeError, TypeError): + # Reading the code of the function in Python 3.6 implies there are + # at least these errors that might occur if something is wrong with + # the signature. In that case we just want a simple escape for now. + raise ValueError + return [ + SignatureParam( + name=p.name, + has_default=p.default is not p.empty, + default=self._create_access_path(p.default), + has_annotation=p.annotation is not p.empty, + annotation=self._create_access_path(p.annotation), + kind_name=str(p.kind) + ) for p in signature.parameters.values() + ] + + def negate(self): + return self._create_access_path(-self._obj) + + def dict_values(self): + return [self._create_access_path(v) for v in self._obj.values()] + + def is_super_class(self, exception): + return issubclass(exception, self._obj) + + def get_dir_infos(self): + """ + Used to return a couple of infos that are needed when accessing the sub + objects of an objects + """ + # TODO is_allowed_getattr might raise an AttributeError + tuples = dict( + (force_unicode(name), self.is_allowed_getattr(name)) + for name in self.dir() + ) + return self.needs_type_completions(), tuples + + +def _is_class_instance(obj): + """Like inspect.* methods.""" + try: + cls = obj.__class__ + except AttributeError: + return False + else: + return cls != type and not issubclass(cls, NOT_CLASS_TYPES) + + +if py_version >= 35: + exec(compile(dedent(""" + async def _coroutine(): pass + _coroutine = _coroutine() + CoroutineType = type(_coroutine) + _coroutine.close() # Prevent ResourceWarning + """), 'blub', 'exec')) + _coroutine_wrapper = _coroutine.__await__() +else: + _coroutine = None + _coroutine_wrapper = None + +if py_version >= 36: + exec(compile(dedent(""" + async def _async_generator(): + yield + _async_generator = _async_generator() + AsyncGeneratorType = type(_async_generator) + """), 'blub', 'exec')) +else: + _async_generator = None + +class _SPECIAL_OBJECTS(object): + FUNCTION_CLASS = types.FunctionType + METHOD_CLASS = type(DirectObjectAccess.py__bool__) + MODULE_CLASS = types.ModuleType + GENERATOR_OBJECT = _a_generator(1.0) + BUILTINS = builtins + COROUTINE = _coroutine + COROUTINE_WRAPPER = _coroutine_wrapper + ASYNC_GENERATOR = _async_generator + + +def get_special_object(evaluator, identifier): + obj = getattr(_SPECIAL_OBJECTS, identifier) + return create_access_path(evaluator, obj) diff --git a/pythonFiles/jedi/evaluate/compiled/context.py b/pythonFiles/jedi/evaluate/compiled/context.py new file mode 100644 index 000000000000..f81509d73f72 --- /dev/null +++ b/pythonFiles/jedi/evaluate/compiled/context.py @@ -0,0 +1,474 @@ +""" +Imitate the parser representation. +""" +import re +from functools import partial + +from jedi import debug +from jedi._compatibility import force_unicode, Parameter +from jedi.cache import underscore_memoization, memoize_method +from jedi.evaluate.filters import AbstractFilter, AbstractNameDefinition, \ + ContextNameMixin +from jedi.evaluate.base_context import Context, ContextSet +from jedi.evaluate.lazy_context import LazyKnownContext +from jedi.evaluate.compiled.access import _sentinel +from jedi.evaluate.cache import evaluator_function_cache +from . import fake + + +class CheckAttribute(object): + """Raises an AttributeError if the attribute X isn't available.""" + def __init__(self, func): + self.func = func + # Remove the py in front of e.g. py__call__. + self.check_name = force_unicode(func.__name__[2:]) + + def __get__(self, instance, owner): + if instance is None: + return self + + # This might raise an AttributeError. That's wanted. + if self.check_name == '__iter__': + # Python iterators are a bit strange, because there's no need for + # the __iter__ function as long as __getitem__ is defined (it will + # just start with __getitem__(0). This is especially true for + # Python 2 strings, where `str.__iter__` is not even defined. + if not instance.access_handle.has_iter(): + raise AttributeError + else: + instance.access_handle.getattr(self.check_name) + return partial(self.func, instance) + + +class CompiledObject(Context): + def __init__(self, evaluator, access_handle, parent_context=None, faked_class=None): + super(CompiledObject, self).__init__(evaluator, parent_context) + self.access_handle = access_handle + # This attribute will not be set for most classes, except for fakes. + self.tree_node = faked_class + + @CheckAttribute + def py__call__(self, params): + if self.tree_node is not None and self.tree_node.type == 'funcdef': + from jedi.evaluate.context.function import FunctionContext + return FunctionContext( + self.evaluator, + parent_context=self.parent_context, + funcdef=self.tree_node + ).py__call__(params) + if self.access_handle.is_class(): + from jedi.evaluate.context import CompiledInstance + return ContextSet(CompiledInstance(self.evaluator, self.parent_context, self, params)) + else: + return ContextSet.from_iterable(self._execute_function(params)) + + @CheckAttribute + def py__class__(self): + return create_from_access_path(self.evaluator, self.access_handle.py__class__()) + + @CheckAttribute + def py__mro__(self): + return (self,) + tuple( + create_from_access_path(self.evaluator, access) + for access in self.access_handle.py__mro__accesses() + ) + + @CheckAttribute + def py__bases__(self): + return tuple( + create_from_access_path(self.evaluator, access) + for access in self.access_handle.py__bases__() + ) + + def py__bool__(self): + return self.access_handle.py__bool__() + + def py__file__(self): + return self.access_handle.py__file__() + + def is_class(self): + return self.access_handle.is_class() + + def py__doc__(self, include_call_signature=False): + return self.access_handle.py__doc__() + + def get_param_names(self): + try: + signature_params = self.access_handle.get_signature_params() + except ValueError: # Has no signature + params_str, ret = self._parse_function_doc() + tokens = params_str.split(',') + if self.access_handle.ismethoddescriptor(): + tokens.insert(0, 'self') + for p in tokens: + parts = p.strip().split('=') + yield UnresolvableParamName(self, parts[0]) + else: + for signature_param in signature_params: + yield SignatureParamName(self, signature_param) + + def __repr__(self): + return '<%s: %s>' % (self.__class__.__name__, self.access_handle.get_repr()) + + @underscore_memoization + def _parse_function_doc(self): + doc = self.py__doc__() + if doc is None: + return '', '' + + return _parse_function_doc(doc) + + @property + def api_type(self): + return self.access_handle.get_api_type() + + @underscore_memoization + def _cls(self): + """ + We used to limit the lookups for instantiated objects like list(), but + this is not the case anymore. Python itself + """ + # Ensures that a CompiledObject is returned that is not an instance (like list) + return self + + def get_filters(self, search_global=False, is_instance=False, + until_position=None, origin_scope=None): + yield self._ensure_one_filter(is_instance) + + @memoize_method + def _ensure_one_filter(self, is_instance): + """ + search_global shouldn't change the fact that there's one dict, this way + there's only one `object`. + """ + return CompiledObjectFilter(self.evaluator, self, is_instance) + + @CheckAttribute + def py__getitem__(self, index): + access = self.access_handle.py__getitem__(index) + if access is None: + return ContextSet() + + return ContextSet(create_from_access_path(self.evaluator, access)) + + @CheckAttribute + def py__iter__(self): + for access in self.access_handle.py__iter__list(): + yield LazyKnownContext(create_from_access_path(self.evaluator, access)) + + def py__name__(self): + return self.access_handle.py__name__() + + @property + def name(self): + name = self.py__name__() + if name is None: + name = self.access_handle.get_repr() + return CompiledContextName(self, name) + + def _execute_function(self, params): + from jedi.evaluate import docstrings + from jedi.evaluate.compiled import builtin_from_name + if self.api_type != 'function': + return + + for name in self._parse_function_doc()[1].split(): + try: + # TODO wtf is this? this is exactly the same as the thing + # below. It uses getattr as well. + self.evaluator.builtins_module.access_handle.getattr(name) + except AttributeError: + continue + else: + bltn_obj = builtin_from_name(self.evaluator, name) + for result in bltn_obj.execute(params): + yield result + for type_ in docstrings.infer_return_types(self): + yield type_ + + def dict_values(self): + return ContextSet.from_iterable( + create_from_access_path(self.evaluator, access) + for access in self.access_handle.dict_values() + ) + + def get_safe_value(self, default=_sentinel): + try: + return self.access_handle.get_safe_value() + except ValueError: + if default == _sentinel: + raise + return default + + def execute_operation(self, other, operator): + return create_from_access_path( + self.evaluator, + self.access_handle.execute_operation(other.access_handle, operator) + ) + + def negate(self): + return create_from_access_path(self.evaluator, self.access_handle.negate()) + + def is_super_class(self, exception): + return self.access_handle.is_super_class(exception) + + +class CompiledName(AbstractNameDefinition): + def __init__(self, evaluator, parent_context, name): + self._evaluator = evaluator + self.parent_context = parent_context + self.string_name = name + + def __repr__(self): + try: + name = self.parent_context.name # __name__ is not defined all the time + except AttributeError: + name = None + return '<%s: (%s).%s>' % (self.__class__.__name__, name, self.string_name) + + @property + def api_type(self): + return next(iter(self.infer())).api_type + + @underscore_memoization + def infer(self): + return ContextSet(create_from_name( + self._evaluator, self.parent_context, self.string_name + )) + + +class SignatureParamName(AbstractNameDefinition): + api_type = u'param' + + def __init__(self, compiled_obj, signature_param): + self.parent_context = compiled_obj.parent_context + self._signature_param = signature_param + + @property + def string_name(self): + return self._signature_param.name + + def get_kind(self): + return getattr(Parameter, self._signature_param.kind_name) + + def is_keyword_param(self): + return self._signature_param + + def infer(self): + p = self._signature_param + evaluator = self.parent_context.evaluator + contexts = ContextSet() + if p.has_default: + contexts = ContextSet(create_from_access_path(evaluator, p.default)) + if p.has_annotation: + annotation = create_from_access_path(evaluator, p.annotation) + contexts |= annotation.execute_evaluated() + return contexts + + +class UnresolvableParamName(AbstractNameDefinition): + api_type = u'param' + + def __init__(self, compiled_obj, name): + self.parent_context = compiled_obj.parent_context + self.string_name = name + + def get_kind(self): + return Parameter.POSITIONAL_ONLY + + def infer(self): + return ContextSet() + + +class CompiledContextName(ContextNameMixin, AbstractNameDefinition): + def __init__(self, context, name): + self.string_name = name + self._context = context + self.parent_context = context.parent_context + + +class EmptyCompiledName(AbstractNameDefinition): + """ + Accessing some names will raise an exception. To avoid not having any + completions, just give Jedi the option to return this object. It infers to + nothing. + """ + def __init__(self, evaluator, name): + self.parent_context = evaluator.builtins_module + self.string_name = name + + def infer(self): + return ContextSet() + + +class CompiledObjectFilter(AbstractFilter): + name_class = CompiledName + + def __init__(self, evaluator, compiled_object, is_instance=False): + self._evaluator = evaluator + self._compiled_object = compiled_object + self._is_instance = is_instance + + def get(self, name): + return self._get( + name, + lambda: self._compiled_object.access_handle.is_allowed_getattr(name), + lambda: self._compiled_object.access_handle.dir(), + check_has_attribute=True + ) + + def _get(self, name, allowed_getattr_callback, dir_callback, check_has_attribute=False): + """ + To remove quite a few access calls we introduced the callback here. + """ + has_attribute, is_descriptor = allowed_getattr_callback() + if check_has_attribute and not has_attribute: + return [] + + # Always use unicode objects in Python 2 from here. + name = force_unicode(name) + + if is_descriptor or not has_attribute: + return [self._get_cached_name(name, is_empty=True)] + + if self._is_instance and name not in dir_callback(): + return [] + return [self._get_cached_name(name)] + + @memoize_method + def _get_cached_name(self, name, is_empty=False): + if is_empty: + return EmptyCompiledName(self._evaluator, name) + else: + return self._create_name(name) + + def values(self): + from jedi.evaluate.compiled import builtin_from_name + names = [] + needs_type_completions, dir_infos = self._compiled_object.access_handle.get_dir_infos() + for name in dir_infos: + names += self._get( + name, + lambda: dir_infos[name], + lambda: dir_infos.keys(), + ) + + # ``dir`` doesn't include the type names. + if not self._is_instance and needs_type_completions: + for filter in builtin_from_name(self._evaluator, u'type').get_filters(): + names += filter.values() + return names + + def _create_name(self, name): + return self.name_class(self._evaluator, self._compiled_object, name) + + +docstr_defaults = { + 'floating point number': u'float', + 'character': u'str', + 'integer': u'int', + 'dictionary': u'dict', + 'string': u'str', +} + + +def _parse_function_doc(doc): + """ + Takes a function and returns the params and return value as a tuple. + This is nothing more than a docstring parser. + + TODO docstrings like utime(path, (atime, mtime)) and a(b [, b]) -> None + TODO docstrings like 'tuple of integers' + """ + doc = force_unicode(doc) + # parse round parentheses: def func(a, (b,c)) + try: + count = 0 + start = doc.index('(') + for i, s in enumerate(doc[start:]): + if s == '(': + count += 1 + elif s == ')': + count -= 1 + if count == 0: + end = start + i + break + param_str = doc[start + 1:end] + except (ValueError, UnboundLocalError): + # ValueError for doc.index + # UnboundLocalError for undefined end in last line + debug.dbg('no brackets found - no param') + end = 0 + param_str = u'' + else: + # remove square brackets, that show an optional param ( = None) + def change_options(m): + args = m.group(1).split(',') + for i, a in enumerate(args): + if a and '=' not in a: + args[i] += '=None' + return ','.join(args) + + while True: + param_str, changes = re.subn(r' ?\[([^\[\]]+)\]', + change_options, param_str) + if changes == 0: + break + param_str = param_str.replace('-', '_') # see: isinstance.__doc__ + + # parse return value + r = re.search(u'-[>-]* ', doc[end:end + 7]) + if r is None: + ret = u'' + else: + index = end + r.end() + # get result type, which can contain newlines + pattern = re.compile(r'(,\n|[^\n-])+') + ret_str = pattern.match(doc, index).group(0).strip() + # New object -> object() + ret_str = re.sub(r'[nN]ew (.*)', r'\1()', ret_str) + + ret = docstr_defaults.get(ret_str, ret_str) + + return param_str, ret + + +def create_from_name(evaluator, compiled_object, name): + faked = None + try: + faked = fake.get_faked_with_parent_context(compiled_object, name) + except fake.FakeDoesNotExist: + pass + + access = compiled_object.access_handle.getattr(name, default=None) + return create_cached_compiled_object( + evaluator, access, parent_context=compiled_object, faked=faked + ) + + +def _normalize_create_args(func): + """The cache doesn't care about keyword vs. normal args.""" + def wrapper(evaluator, obj, parent_context=None, faked=None): + return func(evaluator, obj, parent_context, faked) + return wrapper + + +def create_from_access_path(evaluator, access_path): + parent_context = None + for name, access in access_path.accesses: + try: + if parent_context is None: + faked = fake.get_faked_module(evaluator, access_path.accesses[0][0]) + else: + faked = fake.get_faked_with_parent_context(parent_context, name) + except fake.FakeDoesNotExist: + faked = None + + parent_context = create_cached_compiled_object(evaluator, access, parent_context, faked) + return parent_context + + +@_normalize_create_args +@evaluator_function_cache() +def create_cached_compiled_object(evaluator, access_handle, parent_context, faked): + return CompiledObject(evaluator, access_handle, parent_context, faked) diff --git a/pythonFiles/jedi/evaluate/compiled/fake.py b/pythonFiles/jedi/evaluate/compiled/fake.py index 60dbefe4acac..ac43ea286b48 100644 --- a/pythonFiles/jedi/evaluate/compiled/fake.py +++ b/pythonFiles/jedi/evaluate/compiled/fake.py @@ -5,73 +5,59 @@ """ import os -import inspect -import types from itertools import chain -from parso.python import tree +from jedi._compatibility import unicode -from jedi._compatibility import is_py3, builtins, unicode, is_py34 +fake_modules = {} -modules = {} +def _get_path_dict(): + path = os.path.dirname(os.path.abspath(__file__)) + base_path = os.path.join(path, 'fake') + dct = {} + for file_name in os.listdir(base_path): + if file_name.endswith('.pym'): + dct[file_name[:-4]] = os.path.join(base_path, file_name) + return dct -MethodDescriptorType = type(str.replace) -# These are not considered classes and access is granted even though they have -# a __class__ attribute. -NOT_CLASS_TYPES = ( - types.BuiltinFunctionType, - types.CodeType, - types.FrameType, - types.FunctionType, - types.GeneratorType, - types.GetSetDescriptorType, - types.LambdaType, - types.MemberDescriptorType, - types.MethodType, - types.ModuleType, - types.TracebackType, - MethodDescriptorType -) -if is_py3: - NOT_CLASS_TYPES += ( - types.MappingProxyType, - types.SimpleNamespace - ) - if is_py34: - NOT_CLASS_TYPES += (types.DynamicClassAttribute,) +_path_dict = _get_path_dict() class FakeDoesNotExist(Exception): pass -def _load_faked_module(grammar, module): - module_name = module.__name__ - if module_name == '__builtin__' and not is_py3: - module_name = 'builtins' +def _load_faked_module(evaluator, module_name): + try: + return fake_modules[module_name] + except KeyError: + pass + + check_module_name = module_name + if module_name == '__builtin__' and evaluator.environment.version_info.major == 2: + check_module_name = 'builtins' try: - return modules[module_name] + path = _path_dict[check_module_name] except KeyError: - path = os.path.dirname(os.path.abspath(__file__)) - try: - with open(os.path.join(path, 'fake', module_name) + '.pym') as f: - source = f.read() - except IOError: - modules[module_name] = None - return - modules[module_name] = m = grammar.parse(unicode(source)) - - if module_name == 'builtins' and not is_py3: - # There are two implementations of `open` for either python 2/3. - # -> Rename the python2 version (`look at fake/builtins.pym`). - open_func = _search_scope(m, 'open') - open_func.children[1].value = 'open_python3' - open_func = _search_scope(m, 'open_python2') - open_func.children[1].value = 'open' - return m + fake_modules[module_name] = None + return + + with open(path) as f: + source = f.read() + + fake_modules[module_name] = m = evaluator.latest_grammar.parse(unicode(source)) + + if check_module_name != module_name: + # There are two implementations of `open` for either python 2/3. + # -> Rename the python2 version (`look at fake/builtins.pym`). + open_func = _search_scope(m, 'open') + open_func.children[1].value = 'open_python3' + open_func = _search_scope(m, 'open_python2') + open_func.children[1].value = 'open' + return m def _search_scope(scope, obj_name): @@ -80,134 +66,17 @@ def _search_scope(scope, obj_name): return s -def get_module(obj): - if inspect.ismodule(obj): - return obj - try: - obj = obj.__objclass__ - except AttributeError: - pass - - try: - imp_plz = obj.__module__ - except AttributeError: - # Unfortunately in some cases like `int` there's no __module__ - return builtins - else: - if imp_plz is None: - # Happens for example in `(_ for _ in []).send.__module__`. - return builtins - else: - try: - return __import__(imp_plz) - except ImportError: - # __module__ can be something arbitrary that doesn't exist. - return builtins - - -def _faked(grammar, module, obj, name): - # Crazy underscore actions to try to escape all the internal madness. - if module is None: - module = get_module(obj) - - faked_mod = _load_faked_module(grammar, module) - if faked_mod is None: - return None, None - - # Having the module as a `parser.python.tree.Module`, we need to scan - # for methods. - if name is None: - if inspect.isbuiltin(obj) or inspect.isclass(obj): - return _search_scope(faked_mod, obj.__name__), faked_mod - elif not inspect.isclass(obj): - # object is a method or descriptor - try: - objclass = obj.__objclass__ - except AttributeError: - return None, None - else: - cls = _search_scope(faked_mod, objclass.__name__) - if cls is None: - return None, None - return _search_scope(cls, obj.__name__), faked_mod - else: - if obj is module: - return _search_scope(faked_mod, name), faked_mod - else: - try: - cls_name = obj.__name__ - except AttributeError: - return None, None - cls = _search_scope(faked_mod, cls_name) - if cls is None: - return None, None - return _search_scope(cls, name), faked_mod - return None, None - - -def memoize_faked(obj): - """ - A typical memoize function that ignores issues with non hashable results. - """ - cache = obj.cache = {} - - def memoizer(*args, **kwargs): - key = (obj, args, frozenset(kwargs.items())) - try: - result = cache[key] - except (TypeError, ValueError): - return obj(*args, **kwargs) - except KeyError: - result = obj(*args, **kwargs) - if result is not None: - cache[key] = obj(*args, **kwargs) - return result - else: - return result - return memoizer - - -@memoize_faked -def _get_faked(grammar, module, obj, name=None): - result, fake_module = _faked(grammar, module, obj, name) - if result is None: - # We're not interested in classes. What we want is functions. - raise FakeDoesNotExist - elif result.type == 'classdef': - return result, fake_module - else: - # Set the docstr which was previously not set (faked modules don't - # contain it). - assert result.type == 'funcdef' - doc = '"""%s"""' % obj.__doc__ # TODO need escapes. - suite = result.children[-1] - string = tree.String(doc, (0, 0), '') - new_line = tree.Newline('\n', (0, 0)) - docstr_node = tree.PythonNode('simple_stmt', [string, new_line]) - suite.children.insert(1, docstr_node) - return result, fake_module - - -def get_faked(evaluator, module, obj, name=None, parent_context=None): - if parent_context and parent_context.tree_node is not None: +def get_faked_with_parent_context(parent_context, name): + if parent_context.tree_node is not None: # Try to search in already clearly defined stuff. found = _search_scope(parent_context.tree_node, name) if found is not None: return found - else: - raise FakeDoesNotExist + raise FakeDoesNotExist - faked, fake_module = _get_faked(evaluator.latest_grammar, module and module.obj, obj, name) - if module is not None: - module.get_used_names = fake_module.get_used_names - return faked - -def is_class_instance(obj): - """Like inspect.* methods.""" - try: - cls = obj.__class__ - except AttributeError: - return False - else: - return cls != type and not issubclass(cls, NOT_CLASS_TYPES) +def get_faked_module(evaluator, string_name): + module = _load_faked_module(evaluator, string_name) + if module is None: + raise FakeDoesNotExist + return module diff --git a/pythonFiles/jedi/evaluate/compiled/fake/builtins.pym b/pythonFiles/jedi/evaluate/compiled/fake/builtins.pym index 1225929c2cb7..46ec619fb420 100644 --- a/pythonFiles/jedi/evaluate/compiled/fake/builtins.pym +++ b/pythonFiles/jedi/evaluate/compiled/fake/builtins.pym @@ -201,10 +201,13 @@ class dict(): # has a strange docstr pass + def __getitem__(self, obj): + return self.__elements[obj] + def get(self, k, d=None): # TODO implement try: - #return self.__elements[k] + return self.__elements[k] pass except KeyError: return d diff --git a/pythonFiles/jedi/evaluate/compiled/getattr_static.py b/pythonFiles/jedi/evaluate/compiled/getattr_static.py index 9f8cd8a838cf..946ac09b6b06 100644 --- a/pythonFiles/jedi/evaluate/compiled/getattr_static.py +++ b/pythonFiles/jedi/evaluate/compiled/getattr_static.py @@ -10,6 +10,7 @@ _sentinel = object() + def _check_instance(obj, attr): instance_dict = {} try: @@ -28,6 +29,7 @@ def _check_class(klass, attr): pass return _sentinel + def _is_type(obj): try: _static_getmro(obj) @@ -87,7 +89,7 @@ class Foo(object): pass return getattr(klass, '__dict__', _sentinel) return _shadowed_dict_newstyle(klass) - class _OldStyleClass(): + class _OldStyleClass: pass _oldstyle_instance_type = type(_OldStyleClass()) @@ -122,7 +124,7 @@ def _safe_hasattr(obj, name): def _safe_is_data_descriptor(obj): - return (_safe_hasattr(obj, '__set__') or _safe_hasattr(obj, '__delete__')) + return _safe_hasattr(obj, '__set__') or _safe_hasattr(obj, '__delete__') def getattr_static(obj, attr, default=_sentinel): @@ -143,8 +145,7 @@ def getattr_static(obj, attr, default=_sentinel): if not _is_type(obj): klass = _get_type(obj) dict_attr = _shadowed_dict(klass) - if (dict_attr is _sentinel or - type(dict_attr) is types.MemberDescriptorType): + if (dict_attr is _sentinel or type(dict_attr) is types.MemberDescriptorType): instance_result = _check_instance(obj, attr) else: klass = obj diff --git a/pythonFiles/jedi/evaluate/compiled/mixed.py b/pythonFiles/jedi/evaluate/compiled/mixed.py index ac0f6dd614db..b8003502e3d7 100644 --- a/pythonFiles/jedi/evaluate/compiled/mixed.py +++ b/pythonFiles/jedi/evaluate/compiled/mixed.py @@ -5,6 +5,8 @@ import inspect import os +from jedi.parser_utils import get_cached_code_lines + from jedi import settings from jedi.evaluate import compiled from jedi.cache import underscore_memoization @@ -13,6 +15,8 @@ from jedi.evaluate.context import ModuleContext from jedi.evaluate.cache import evaluator_function_cache from jedi.evaluate.compiled.getattr_static import getattr_static +from jedi.evaluate.compiled.access import compiled_objects_cache +from jedi.evaluate.compiled.context import create_cached_compiled_object class MixedObject(object): @@ -37,7 +41,7 @@ def __init__(self, evaluator, parent_context, compiled_object, tree_context): self.parent_context = parent_context self.compiled_object = compiled_object self._context = tree_context - self.obj = compiled_object.obj + self.access_handle = compiled_object.access_handle # We have to overwrite everything that has to do with trailers, name # lookups and filters to make it possible to route name lookups towards @@ -49,7 +53,7 @@ def get_filters(self, *args, **kwargs): yield MixedObjectFilter(self.evaluator, self) def __repr__(self): - return '<%s: %s>' % (type(self).__name__, repr(self.obj)) + return '<%s: %s>' % (type(self).__name__, self.access_handle.get_repr()) def __getattr__(self, name): return getattr(self._context, name) @@ -64,7 +68,7 @@ def start_pos(self): contexts = list(self.infer()) if not contexts: # This means a start_pos that doesn't exist (compiled objects). - return (0, 0) + return 0, 0 return contexts[0].name.start_pos @start_pos.setter @@ -74,17 +78,11 @@ def start_pos(self, value): @underscore_memoization def infer(self): - obj = self.parent_context.obj - try: - # TODO use logic from compiled.CompiledObjectFilter - obj = getattr(obj, self.string_name) - except AttributeError: - # Happens e.g. in properties of - # PyQt4.QtGui.QStyleOptionComboBox.currentText - # -> just set it to None - obj = None + access_handle = self.parent_context.access_handle + # TODO use logic from compiled.CompiledObjectFilter + access_handle = access_handle.getattr(self.string_name, default=None) return ContextSet( - _create(self._evaluator, obj, parent_context=self.parent_context) + _create(self._evaluator, access_handle, parent_context=self.parent_context) ) @property @@ -105,17 +103,17 @@ def __init__(self, evaluator, mixed_object, is_instance=False): @evaluator_function_cache() -def _load_module(evaluator, path, python_object): - module = evaluator.grammar.parse( +def _load_module(evaluator, path): + module_node = evaluator.grammar.parse( path=path, cache=True, diff_cache=True, cache_path=settings.cache_directory ).get_root_node() - python_module = inspect.getmodule(python_object) - - evaluator.modules[python_module.__name__] = module - return module + # python_module = inspect.getmodule(python_object) + # TODO we should actually make something like this possible. + #evaluator.modules[python_module.__name__] = module_node + return module_node def _get_object_to_check(python_object): @@ -135,39 +133,43 @@ def _get_object_to_check(python_object): raise TypeError # Prevents computation of `repr` within inspect. -def find_syntax_node_name(evaluator, python_object): +def _find_syntax_node_name(evaluator, access_handle): + # TODO accessing this is bad, but it probably doesn't matter that much, + # because we're working with interpreteters only here. + python_object = access_handle.access._obj try: python_object = _get_object_to_check(python_object) path = inspect.getsourcefile(python_object) except TypeError: # The type might not be known (e.g. class_with_dict.__weakref__) - return None, None + return None if path is None or not os.path.exists(path): # The path might not exist or be e.g. . - return None, None + return None - module = _load_module(evaluator, path, python_object) + module_node = _load_module(evaluator, path) if inspect.ismodule(python_object): # We don't need to check names for modules, because there's not really # a way to write a module in a module in Python (and also __name__ can # be something like ``email.utils``). - return module, path + code_lines = get_cached_code_lines(evaluator.grammar, path) + return module_node, module_node, path, code_lines try: name_str = python_object.__name__ except AttributeError: # Stuff like python_function.__code__. - return None, None + return None if name_str == '': - return None, None # It's too hard to find lambdas. + return None # It's too hard to find lambdas. # Doesn't always work (e.g. os.stat_result) try: - names = module.get_used_names()[name_str] + names = module_node.get_used_names()[name_str] except KeyError: - return None, None + return None names = [n for n in names if n.is_definition()] try: @@ -184,33 +186,40 @@ def find_syntax_node_name(evaluator, python_object): # There's a chance that the object is not available anymore, because # the code has changed in the background. if line_names: - return line_names[-1].parent, path + names = line_names + code_lines = get_cached_code_lines(evaluator.grammar, path) # It's really hard to actually get the right definition, here as a last # resort we just return the last one. This chance might lead to odd # completions at some points but will lead to mostly correct type # inference, because people tend to define a public name in a module only # once. - return names[-1].parent, path + return module_node, names[-1].parent, path, code_lines -@compiled.compiled_objects_cache('mixed_cache') -def _create(evaluator, obj, parent_context=None, *args): - tree_node, path = find_syntax_node_name(evaluator, obj) +@compiled_objects_cache('mixed_cache') +def _create(evaluator, access_handle, parent_context, *args): + compiled_object = create_cached_compiled_object( + evaluator, access_handle, parent_context=parent_context.compiled_object) - compiled_object = compiled.create( - evaluator, obj, parent_context=parent_context.compiled_object) - if tree_node is None: + result = _find_syntax_node_name(evaluator, access_handle) + if result is None: return compiled_object - module_node = tree_node.get_root_node() + module_node, tree_node, path, code_lines = result + if parent_context.tree_node.get_root_node() == module_node: module_context = parent_context.get_root_context() else: - module_context = ModuleContext(evaluator, module_node, path=path) + module_context = ModuleContext( + evaluator, module_node, + path=path, + code_lines=code_lines, + ) # TODO this __name__ is probably wrong. name = compiled_object.get_root_context().py__name__() - imports.add_module(evaluator, name, module_context) + if name is not None: + imports.add_module_to_cache(evaluator, name, module_context) tree_context = module_context.create_context( tree_node, @@ -218,7 +227,7 @@ def _create(evaluator, obj, parent_context=None, *args): node_is_object=True ) if tree_node.type == 'classdef': - if not inspect.isclass(obj): + if not access_handle.is_class(): # Is an instance, not a class. tree_context, = tree_context.execute_evaluated() @@ -228,4 +237,3 @@ def _create(evaluator, obj, parent_context=None, *args): compiled_object, tree_context=tree_context ) - diff --git a/pythonFiles/jedi/evaluate/compiled/subprocess/__init__.py b/pythonFiles/jedi/evaluate/compiled/subprocess/__init__.py new file mode 100644 index 000000000000..9cc8704a457e --- /dev/null +++ b/pythonFiles/jedi/evaluate/compiled/subprocess/__init__.py @@ -0,0 +1,340 @@ +""" +Makes it possible to do the compiled analysis in a subprocess. This has two +goals: + +1. Making it safer - Segfaults and RuntimeErrors as well as stdout/stderr can + be ignored and dealt with. +2. Make it possible to handle different Python versions as well as virtualenvs. +""" + +import os +import sys +import subprocess +import socket +import errno +import weakref +import traceback +from functools import partial + +from jedi._compatibility import queue, is_py3, force_unicode, \ + pickle_dump, pickle_load, GeneralizedPopen +from jedi.cache import memoize_method +from jedi.evaluate.compiled.subprocess import functions +from jedi.evaluate.compiled.access import DirectObjectAccess, AccessPath, \ + SignatureParam +from jedi.api.exceptions import InternalError + +_subprocesses = {} + +_MAIN_PATH = os.path.join(os.path.dirname(__file__), '__main__.py') + + +def get_subprocess(executable): + try: + return _subprocesses[executable] + except KeyError: + sub = _subprocesses[executable] = _CompiledSubprocess(executable) + return sub + + +def _get_function(name): + return getattr(functions, name) + + +class _EvaluatorProcess(object): + def __init__(self, evaluator): + self._evaluator_weakref = weakref.ref(evaluator) + self._evaluator_id = id(evaluator) + self._handles = {} + + def get_or_create_access_handle(self, obj): + id_ = id(obj) + try: + return self.get_access_handle(id_) + except KeyError: + access = DirectObjectAccess(self._evaluator_weakref(), obj) + handle = AccessHandle(self, access, id_) + self.set_access_handle(handle) + return handle + + def get_access_handle(self, id_): + return self._handles[id_] + + def set_access_handle(self, handle): + self._handles[handle.id] = handle + + +class EvaluatorSameProcess(_EvaluatorProcess): + """ + Basically just an easy access to functions.py. It has the same API + as EvaluatorSubprocess and does the same thing without using a subprocess. + This is necessary for the Interpreter process. + """ + def __getattr__(self, name): + return partial(_get_function(name), self._evaluator_weakref()) + + +class EvaluatorSubprocess(_EvaluatorProcess): + def __init__(self, evaluator, compiled_subprocess): + super(EvaluatorSubprocess, self).__init__(evaluator) + self._used = False + self._compiled_subprocess = compiled_subprocess + + def __getattr__(self, name): + func = _get_function(name) + + def wrapper(*args, **kwargs): + self._used = True + + result = self._compiled_subprocess.run( + self._evaluator_weakref(), + func, + args=args, + kwargs=kwargs, + ) + # IMO it should be possible to create a hook in pickle.load to + # mess with the loaded objects. However it's extremely complicated + # to work around this so just do it with this call. ~ dave + return self._convert_access_handles(result) + + return wrapper + + def _convert_access_handles(self, obj): + if isinstance(obj, SignatureParam): + return SignatureParam(*self._convert_access_handles(tuple(obj))) + elif isinstance(obj, tuple): + return tuple(self._convert_access_handles(o) for o in obj) + elif isinstance(obj, list): + return [self._convert_access_handles(o) for o in obj] + elif isinstance(obj, AccessHandle): + try: + # Rewrite the access handle to one we're already having. + obj = self.get_access_handle(obj.id) + except KeyError: + obj.add_subprocess(self) + self.set_access_handle(obj) + elif isinstance(obj, AccessPath): + return AccessPath(self._convert_access_handles(obj.accesses)) + return obj + + def __del__(self): + if self._used: + self._compiled_subprocess.delete_evaluator(self._evaluator_id) + + +class _CompiledSubprocess(object): + _crashed = False + + def __init__(self, executable): + self._executable = executable + self._evaluator_deletion_queue = queue.deque() + + @property + @memoize_method + def _process(self): + parso_path = sys.modules['parso'].__file__ + args = ( + self._executable, + _MAIN_PATH, + os.path.dirname(os.path.dirname(parso_path)) + ) + return GeneralizedPopen( + args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + + def run(self, evaluator, function, args=(), kwargs={}): + # Delete old evaluators. + while True: + try: + evaluator_id = self._evaluator_deletion_queue.pop() + except IndexError: + break + else: + self._send(evaluator_id, None) + + assert callable(function) + return self._send(id(evaluator), function, args, kwargs) + + def get_sys_path(self): + return self._send(None, functions.get_sys_path, (), {}) + + def kill(self): + self._crashed = True + try: + subprocess = _subprocesses[self._executable] + except KeyError: + # Fine it was already removed from the cache. + pass + else: + # In the `!=` case there is already a new subprocess in place + # and we don't need to do anything here anymore. + if subprocess == self: + del _subprocesses[self._executable] + + self._process.kill() + self._process.wait() + + def _send(self, evaluator_id, function, args=(), kwargs={}): + if self._crashed: + raise InternalError("The subprocess %s has crashed." % self._executable) + + if not is_py3: + # Python 2 compatibility + kwargs = {force_unicode(key): value for key, value in kwargs.items()} + + data = evaluator_id, function, args, kwargs + try: + pickle_dump(data, self._process.stdin) + except (socket.error, IOError) as e: + # Once Python2 will be removed we can just use `BrokenPipeError`. + # Also, somehow in windows it returns EINVAL instead of EPIPE if + # the subprocess dies. + if e.errno not in (errno.EPIPE, errno.EINVAL): + # Not a broken pipe + raise + self.kill() + raise InternalError("The subprocess %s was killed. Maybe out of memory?" + % self._executable) + + try: + is_exception, traceback, result = pickle_load(self._process.stdout) + except EOFError: + self.kill() + raise InternalError("The subprocess %s has crashed." % self._executable) + + if is_exception: + # Replace the attribute error message with a the traceback. It's + # way more informative. + result.args = (traceback,) + raise result + return result + + def delete_evaluator(self, evaluator_id): + """ + Currently we are not deleting evalutors instantly. They only get + deleted once the subprocess is used again. It would probably a better + solution to move all of this into a thread. However, the memory usage + of a single evaluator shouldn't be that high. + """ + # With an argument - the evaluator gets deleted. + self._evaluator_deletion_queue.append(evaluator_id) + + +class Listener(object): + def __init__(self): + self._evaluators = {} + # TODO refactor so we don't need to process anymore just handle + # controlling. + self._process = _EvaluatorProcess(Listener) + + def _get_evaluator(self, function, evaluator_id): + from jedi.evaluate import Evaluator + + try: + evaluator = self._evaluators[evaluator_id] + except KeyError: + from jedi.api.environment import InterpreterEnvironment + evaluator = Evaluator( + # The project is not actually needed. Nothing should need to + # access it. + project=None, + environment=InterpreterEnvironment() + ) + self._evaluators[evaluator_id] = evaluator + return evaluator + + def _run(self, evaluator_id, function, args, kwargs): + if evaluator_id is None: + return function(*args, **kwargs) + elif function is None: + del self._evaluators[evaluator_id] + else: + evaluator = self._get_evaluator(function, evaluator_id) + + # Exchange all handles + args = list(args) + for i, arg in enumerate(args): + if isinstance(arg, AccessHandle): + args[i] = evaluator.compiled_subprocess.get_access_handle(arg.id) + for key, value in kwargs.items(): + if isinstance(value, AccessHandle): + kwargs[key] = evaluator.compiled_subprocess.get_access_handle(value.id) + + return function(evaluator, *args, **kwargs) + + def listen(self): + stdout = sys.stdout + # Mute stdout/stderr. Nobody should actually be able to write to those, + # because stdout is used for IPC and stderr will just be annoying if it + # leaks (on module imports). + sys.stdout = open(os.devnull, 'w') + sys.stderr = open(os.devnull, 'w') + stdin = sys.stdin + if sys.version_info[0] > 2: + stdout = stdout.buffer + stdin = stdin.buffer + + while True: + try: + payload = pickle_load(stdin) + except EOFError: + # It looks like the parent process closed. Don't make a big fuss + # here and just exit. + exit(1) + try: + result = False, None, self._run(*payload) + except Exception as e: + result = True, traceback.format_exc(), e + + pickle_dump(result, file=stdout) + + +class AccessHandle(object): + def __init__(self, subprocess, access, id_): + self.access = access + self._subprocess = subprocess + self.id = id_ + + def add_subprocess(self, subprocess): + self._subprocess = subprocess + + def __repr__(self): + try: + detail = self.access + except AttributeError: + detail = '#' + str(self.id) + return '<%s of %s>' % (self.__class__.__name__, detail) + + def __getstate__(self): + return self.id + + def __setstate__(self, state): + self.id = state + + def __getattr__(self, name): + if name in ('id', 'access') or name.startswith('_'): + raise AttributeError("Something went wrong with unpickling") + + #if not is_py3: print >> sys.stderr, name + #print('getattr', name, file=sys.stderr) + return partial(self._workaround, force_unicode(name)) + + def _workaround(self, name, *args, **kwargs): + """ + TODO Currently we're passing slice objects around. This should not + happen. They are also the only unhashable objects that we're passing + around. + """ + if args and isinstance(args[0], slice): + return self._subprocess.get_compiled_method_return(self.id, name, *args, **kwargs) + return self._cached_results(name, *args, **kwargs) + + @memoize_method + def _cached_results(self, name, *args, **kwargs): + #if type(self._subprocess) == EvaluatorSubprocess: + #print(name, args, kwargs, + #self._subprocess.get_compiled_method_return(self.id, name, *args, **kwargs) + #) + return self._subprocess.get_compiled_method_return(self.id, name, *args, **kwargs) diff --git a/pythonFiles/jedi/evaluate/compiled/subprocess/__main__.py b/pythonFiles/jedi/evaluate/compiled/subprocess/__main__.py new file mode 100644 index 000000000000..8a0148ce5040 --- /dev/null +++ b/pythonFiles/jedi/evaluate/compiled/subprocess/__main__.py @@ -0,0 +1,49 @@ +import sys +import os + + +def _get_paths(): + # Get the path to jedi. + _d = os.path.dirname + _jedi_path = _d(_d(_d(_d(_d(__file__))))) + _parso_path = sys.argv[1] + # The paths are the directory that jedi and parso lie in. + return {'jedi': _jedi_path, 'parso': _parso_path} + + +# Remove the first entry, because it's simply a directory entry that equals +# this directory. +del sys.path[0] + +if sys.version_info > (3, 4): + from importlib.machinery import PathFinder + + class _ExactImporter(object): + def __init__(self, path_dct): + self._path_dct = path_dct + + def find_module(self, fullname, path=None): + if path is None and fullname in self._path_dct: + p = self._path_dct[fullname] + loader = PathFinder.find_module(fullname, path=[p]) + return loader + return None + + # Try to import jedi/parso. + sys.meta_path.insert(0, _ExactImporter(_get_paths())) + from jedi.evaluate.compiled import subprocess # NOQA + sys.meta_path.pop(0) +else: + import imp + + def load(name): + paths = list(_get_paths().values()) + fp, pathname, description = imp.find_module(name, paths) + return imp.load_module(name, fp, pathname, description) + + load('parso') + load('jedi') + from jedi.evaluate.compiled import subprocess # NOQA + +# And finally start the client. +subprocess.Listener().listen() diff --git a/pythonFiles/jedi/evaluate/compiled/subprocess/functions.py b/pythonFiles/jedi/evaluate/compiled/subprocess/functions.py new file mode 100644 index 000000000000..593ff91e8caf --- /dev/null +++ b/pythonFiles/jedi/evaluate/compiled/subprocess/functions.py @@ -0,0 +1,113 @@ +import sys +import os + +from jedi._compatibility import find_module, cast_path, force_unicode, \ + iter_modules, all_suffixes, print_to_stderr +from jedi.evaluate.compiled import access +from jedi import parser_utils + + +def get_sys_path(): + return list(map(cast_path, sys.path)) + + +def load_module(evaluator, **kwargs): + return access.load_module(evaluator, **kwargs) + + +def get_compiled_method_return(evaluator, id, attribute, *args, **kwargs): + handle = evaluator.compiled_subprocess.get_access_handle(id) + return getattr(handle.access, attribute)(*args, **kwargs) + + +def get_special_object(evaluator, identifier): + return access.get_special_object(evaluator, identifier) + + +def create_simple_object(evaluator, obj): + return access.create_access_path(evaluator, obj) + + +def get_module_info(evaluator, sys_path=None, full_name=None, **kwargs): + if sys_path is not None: + sys.path, temp = sys_path, sys.path + try: + module_file, module_path, is_pkg = find_module(full_name=full_name, **kwargs) + except ImportError: + return None, None, None + finally: + if sys_path is not None: + sys.path = temp + + code = None + if is_pkg: + # In this case, we don't have a file yet. Search for the + # __init__ file. + if module_path.endswith(('.zip', '.egg')): + code = module_file.loader.get_source(full_name) + else: + module_path = _get_init_path(module_path) + elif module_file: + if module_path.endswith(('.zip', '.egg')): + # Unfortunately we are reading unicode here already, not byes. + # It seems however hard to get bytes, because the zip importer + # logic just unpacks the zip file and returns a file descriptor + # that we cannot as easily access. Therefore we just read it as + # a string. + code = module_file.read() + else: + # Read the code with a binary file, because the binary file + # might not be proper unicode. This is handled by the parser + # wrapper. + with open(module_path, 'rb') as f: + code = f.read() + + module_file.close() + + return code, cast_path(module_path), is_pkg + + +def list_module_names(evaluator, search_path): + return [ + name + for module_loader, name, is_pkg in iter_modules(search_path) + ] + + +def get_builtin_module_names(evaluator): + return list(map(force_unicode, sys.builtin_module_names)) + + +def _test_raise_error(evaluator, exception_type): + """ + Raise an error to simulate certain problems for unit tests. + """ + raise exception_type + + +def _test_print(evaluator, stderr=None, stdout=None): + """ + Force some prints in the subprocesses. This exists for unit tests. + """ + if stderr is not None: + print_to_stderr(stderr) + sys.stderr.flush() + if stdout is not None: + print(stdout) + sys.stdout.flush() + + +def _get_init_path(directory_path): + """ + The __init__ file can be searched in a directory. If found return it, else + None. + """ + for suffix in all_suffixes(): + path = os.path.join(directory_path, '__init__' + suffix) + if os.path.exists(path): + return path + return None + + +def safe_literal_eval(evaluator, value): + return parser_utils.safe_literal_eval(value) diff --git a/pythonFiles/jedi/evaluate/context/asynchronous.py b/pythonFiles/jedi/evaluate/context/asynchronous.py new file mode 100644 index 000000000000..51e59a48261e --- /dev/null +++ b/pythonFiles/jedi/evaluate/context/asynchronous.py @@ -0,0 +1,38 @@ +from jedi.evaluate.filters import publish_method, BuiltinOverwrite +from jedi.evaluate.base_context import ContextSet + + +class AsyncBase(BuiltinOverwrite): + def __init__(self, evaluator, func_execution_context): + super(AsyncBase, self).__init__(evaluator) + self.func_execution_context = func_execution_context + + @property + def name(self): + return self.get_object().name + + def __repr__(self): + return "<%s of %s>" % (type(self).__name__, self.func_execution_context) + + +class Coroutine(AsyncBase): + special_object_identifier = u'COROUTINE' + + @publish_method('__await__') + def _await(self): + return ContextSet(CoroutineWrapper(self.evaluator, self.func_execution_context)) + + +class CoroutineWrapper(AsyncBase): + special_object_identifier = u'COROUTINE_WRAPPER' + + def py__stop_iteration_returns(self): + return self.func_execution_context.get_return_values() + + +class AsyncGenerator(AsyncBase): + """Handling of `yield` functions.""" + special_object_identifier = u'ASYNC_GENERATOR' + + def py__aiter__(self): + return self.func_execution_context.get_yield_lazy_contexts(is_async=True) diff --git a/pythonFiles/jedi/evaluate/context/function.py b/pythonFiles/jedi/evaluate/context/function.py index 0dba9c91d707..2bb3a9b88bc3 100644 --- a/pythonFiles/jedi/evaluate/context/function.py +++ b/pythonFiles/jedi/evaluate/context/function.py @@ -17,17 +17,20 @@ from jedi.evaluate.lazy_context import LazyKnownContexts, LazyKnownContext, \ LazyTreeContext from jedi.evaluate.context import iterable +from jedi.evaluate.context import asynchronous from jedi import parser_utils from jedi.evaluate.parser_cache import get_yield_exprs class LambdaName(AbstractNameDefinition): string_name = '' + api_type = u'function' def __init__(self, lambda_context): self._lambda_context = lambda_context self.parent_context = lambda_context.parent_context + @property def start_pos(self): return self._lambda_context.tree_node.start_pos @@ -39,7 +42,7 @@ class FunctionContext(use_metaclass(CachedMetaClass, TreeContext)): """ Needed because of decorators. Decorators are evaluated here. """ - api_type = 'function' + api_type = u'function' def __init__(self, evaluator, parent_context, funcdef): """ This should not be called directly """ @@ -63,11 +66,23 @@ def infer_function_execution(self, function_execution): """ Created to be used by inheritance. """ - yield_exprs = get_yield_exprs(self.evaluator, self.tree_node) - if yield_exprs: - return ContextSet(iterable.Generator(self.evaluator, function_execution)) + is_coroutine = self.tree_node.parent.type == 'async_stmt' + is_generator = bool(get_yield_exprs(self.evaluator, self.tree_node)) + + if is_coroutine: + if is_generator: + if self.evaluator.environment.version_info < (3, 6): + return NO_CONTEXTS + return ContextSet(asynchronous.AsyncGenerator(self.evaluator, function_execution)) + else: + if self.evaluator.environment.version_info < (3, 5): + return NO_CONTEXTS + return ContextSet(asynchronous.Coroutine(self.evaluator, function_execution)) else: - return function_execution.get_return_values() + if is_generator: + return ContextSet(iterable.Generator(self.evaluator, function_execution)) + else: + return function_execution.get_return_values() def get_function_execution(self, arguments=None): if arguments is None: @@ -83,9 +98,9 @@ def py__class__(self): # This differentiation is only necessary for Python2. Python3 does not # use a different method class. if isinstance(parser_utils.get_parent_scope(self.tree_node), tree.Class): - name = 'METHOD_CLASS' + name = u'METHOD_CLASS' else: - name = 'FUNCTION_CLASS' + name = u'FUNCTION_CLASS' return compiled.get_special_object(self.evaluator, name) @property @@ -122,7 +137,7 @@ def __init__(self, evaluator, parent_context, function_context, var_args): def get_return_values(self, check_yields=False): funcdef = self.tree_node if funcdef.type == 'lambdef': - return self.evaluator.eval_element(self, funcdef.children[-1]) + return self.eval_node(funcdef.children[-1]) if check_yields: context_set = NO_CONTEXTS @@ -140,13 +155,14 @@ def get_return_values(self, check_yields=False): if check_yields: context_set |= ContextSet.from_sets( lazy_context.infer() - for lazy_context in self._eval_yield(r) + for lazy_context in self._get_yield_lazy_context(r) ) else: try: children = r.children except AttributeError: - context_set |= ContextSet(compiled.create(self.evaluator, None)) + ctx = compiled.builtin_from_name(self.evaluator, u'None') + context_set |= ContextSet(ctx) else: context_set |= self.eval_node(children[1]) if check is flow_analysis.REACHABLE: @@ -154,10 +170,11 @@ def get_return_values(self, check_yields=False): break return context_set - def _eval_yield(self, yield_expr): + def _get_yield_lazy_context(self, yield_expr): if yield_expr.type == 'keyword': # `yield` just yields None. - yield LazyKnownContext(compiled.create(self.evaluator, None)) + ctx = compiled.builtin_from_name(self.evaluator, u'None') + yield LazyKnownContext(ctx) return node = yield_expr.children[1] @@ -169,7 +186,8 @@ def _eval_yield(self, yield_expr): yield LazyTreeContext(self, node) @recursion.execution_recursion_decorator(default=iter([])) - def get_yield_values(self): + def get_yield_lazy_contexts(self, is_async=False): + # TODO: if is_async, wrap yield statements in Awaitable/async_generator_asend for_parents = [(y, tree.search_ancestor(y, 'for_stmt', 'funcdef', 'while_stmt', 'if_stmt')) for y in get_yield_exprs(self.evaluator, self.tree_node)] @@ -202,7 +220,7 @@ def get_yield_values(self): if for_stmt is None: # No for_stmt, just normal yields. for yield_ in yields: - for result in self._eval_yield(yield_): + for result in self._get_yield_lazy_context(yield_): yield result else: input_node = for_stmt.get_testlist() @@ -213,7 +231,7 @@ def get_yield_values(self): dct = {str(for_stmt.children[1].value): lazy_context.infer()} with helpers.predefine_names(self, for_stmt, dct): for yield_in_same_for_stmt in yields: - for result in self._eval_yield(yield_in_same_for_stmt): + for result in self._get_yield_lazy_context(yield_in_same_for_stmt): yield result def get_filters(self, search_global, until_position=None, origin_scope=None): diff --git a/pythonFiles/jedi/evaluate/context/instance.py b/pythonFiles/jedi/evaluate/context/instance.py index 2c8d796c9c6d..def5e19a2da9 100644 --- a/pythonFiles/jedi/evaluate/context/instance.py +++ b/pythonFiles/jedi/evaluate/context/instance.py @@ -1,6 +1,5 @@ from abc import abstractproperty -from jedi._compatibility import is_py3 from jedi import debug from jedi.evaluate import compiled from jedi.evaluate import filters @@ -16,30 +15,34 @@ from jedi.parser_utils import get_parent_scope +class BaseInstanceFunctionExecution(FunctionExecutionContext): + def __init__(self, instance, *args, **kwargs): + self.instance = instance + super(BaseInstanceFunctionExecution, self).__init__( + instance.evaluator, *args, **kwargs) + -class InstanceFunctionExecution(FunctionExecutionContext): +class InstanceFunctionExecution(BaseInstanceFunctionExecution): def __init__(self, instance, parent_context, function_context, var_args): - self.instance = instance var_args = InstanceVarArgs(self, var_args) super(InstanceFunctionExecution, self).__init__( - instance.evaluator, parent_context, function_context, var_args) + instance, parent_context, function_context, var_args) -class AnonymousInstanceFunctionExecution(FunctionExecutionContext): +class AnonymousInstanceFunctionExecution(BaseInstanceFunctionExecution): function_execution_filter = filters.AnonymousInstanceFunctionExecutionFilter def __init__(self, instance, parent_context, function_context, var_args): - self.instance = instance super(AnonymousInstanceFunctionExecution, self).__init__( - instance.evaluator, parent_context, function_context, var_args) + instance, parent_context, function_context, var_args) class AbstractInstanceContext(Context): """ This class is used to evaluate instances. """ - api_type = 'instance' + api_type = u'instance' function_execution_cls = InstanceFunctionExecution def __init__(self, evaluator, parent_context, class_context, var_args): @@ -54,7 +57,7 @@ def is_class(self): @property def py__call__(self): - names = self.get_function_slot_names('__call__') + names = self.get_function_slot_names(u'__call__') if not names: # Means the Instance is not callable. raise AttributeError @@ -90,12 +93,12 @@ def execute_function_slots(self, names, *evaluated_args): def py__get__(self, obj): # Arguments in __get__ descriptors are obj, class. # `method` is the new parent of the array, don't know if that's good. - names = self.get_function_slot_names('__get__') + names = self.get_function_slot_names(u'__get__') if names: if isinstance(obj, AbstractInstanceContext): return self.execute_function_slots(names, obj, obj.class_context) else: - none_obj = compiled.create(self.evaluator, None) + none_obj = compiled.builtin_from_name(self.evaluator, u'None') return self.execute_function_slots(names, none_obj, obj) else: return ContextSet(self) @@ -104,14 +107,12 @@ def get_filters(self, search_global=None, until_position=None, origin_scope=None, include_self_names=True): if include_self_names: for cls in self.class_context.py__mro__(): - if isinstance(cls, compiled.CompiledObject): - if cls.tree_node is not None: - # In this case we're talking about a fake object, it - # doesn't make sense for normal compiled objects to - # search for self variables. - yield SelfNameFilter(self.evaluator, self, cls, origin_scope) - else: - yield SelfNameFilter(self.evaluator, self, cls, origin_scope) + if not isinstance(cls, compiled.CompiledObject) \ + or cls.tree_node is not None: + # In this case we're excluding compiled objects that are + # not fake objects. It doesn't make sense for normal + # compiled objects to search for self variables. + yield SelfAttributeFilter(self.evaluator, self, cls, origin_scope) for cls in self.class_context.py__mro__(): if isinstance(cls, compiled.CompiledObject): @@ -121,16 +122,16 @@ def get_filters(self, search_global=None, until_position=None, def py__getitem__(self, index): try: - names = self.get_function_slot_names('__getitem__') + names = self.get_function_slot_names(u'__getitem__') except KeyError: debug.warning('No __getitem__, cannot access the array.') return NO_CONTEXTS else: - index_obj = compiled.create(self.evaluator, index) + index_obj = compiled.create_simple_object(self.evaluator, index) return self.execute_function_slots(names, index_obj) def py__iter__(self): - iter_slot_names = self.get_function_slot_names('__iter__') + iter_slot_names = self.get_function_slot_names(u'__iter__') if not iter_slot_names: debug.warning('No __iter__ on %s.' % self) return @@ -138,7 +139,10 @@ def py__iter__(self): for generator in self.execute_function_slots(iter_slot_names): if isinstance(generator, AbstractInstanceContext): # `__next__` logic. - name = '__next__' if is_py3 else 'next' + if self.evaluator.environment.version_info.major == 2: + name = u'next' + else: + name = u'__next__' iter_slot_names = generator.get_function_slot_names(name) if iter_slot_names: yield LazyKnownContexts( @@ -166,8 +170,8 @@ def _create_init_execution(self, class_context, func_node): ) def create_init_executions(self): - for name in self.get_function_slot_names('__init__'): - if isinstance(name, LazyInstanceName): + for name in self.get_function_slot_names(u'__init__'): + if isinstance(name, SelfName): yield self._create_init_execution(name.class_context, name.tree_name.parent) @evaluator_method_cache() @@ -189,7 +193,7 @@ def create_instance_context(self, class_context, node): ) return bound_method.get_function_execution() elif scope.type == 'classdef': - class_context = ClassContext(self.evaluator, scope, parent_context) + class_context = ClassContext(self.evaluator, parent_context, scope) return class_context elif scope.type == 'comp_for': # Comprehensions currently don't have a special scope in Jedi. @@ -208,8 +212,10 @@ def __init__(self, *args, **kwargs): super(CompiledInstance, self).__init__(*args, **kwargs) # I don't think that dynamic append lookups should happen here. That # sounds more like something that should go to py__iter__. + self._original_var_args = self.var_args + if self.class_context.name.string_name in ['list', 'set'] \ - and self.parent_context.get_root_context() == self.evaluator.BUILTINS: + and self.parent_context.get_root_context() == self.evaluator.builtins_module: # compare the module path with the builtin name. self.var_args = iterable.get_dynamic_array_instance(self) @@ -223,6 +229,13 @@ def create_instance_context(self, class_context, node): else: return super(CompiledInstance, self).create_instance_context(class_context, node) + def get_first_non_keyword_argument_contexts(self): + key, lazy_context = next(self._original_var_args.unpack(), ('', None)) + if key is not None: + return NO_CONTEXTS + + return lazy_context.infer() + class TreeInstance(AbstractInstanceContext): def __init__(self, evaluator, parent_context, class_context, var_args): @@ -255,7 +268,8 @@ def __init__(self, evaluator, instance, parent_context, name): @iterator_to_context_set def infer(self): for result_context in super(CompiledInstanceName, self).infer(): - if isinstance(result_context, FunctionContext): + is_function = result_context.api_type == 'function' + if result_context.tree_node is not None and is_function: parent_context = result_context.parent_context while parent_context.is_class(): parent_context = parent_context.parent_context @@ -265,7 +279,7 @@ def infer(self): parent_context, result_context.tree_node ) else: - if result_context.api_type == 'function': + if is_function: yield CompiledBoundMethod(result_context) else: yield result_context @@ -306,7 +320,7 @@ def get_function_execution(self, arguments=None): class CompiledBoundMethod(compiled.CompiledObject): def __init__(self, func): super(CompiledBoundMethod, self).__init__( - func.evaluator, func.obj, func.parent_context, func.tree_node) + func.evaluator, func.access_handle, func.parent_context, func.tree_node) def get_param_names(self): return list(super(CompiledBoundMethod, self).get_param_names())[1:] @@ -317,7 +331,7 @@ def infer(self): return super(InstanceNameDefinition, self).infer() -class LazyInstanceName(filters.TreeNameDefinition): +class SelfName(filters.TreeNameDefinition): """ This name calculates the parent_context lazily. """ @@ -331,7 +345,7 @@ def parent_context(self): return self._instance.create_instance_context(self.class_context, self.tree_name) -class LazyInstanceClassName(LazyInstanceName): +class LazyInstanceClassName(SelfName): @iterator_to_context_set def infer(self): for result_context in super(LazyInstanceClassName, self).infer(): @@ -384,8 +398,11 @@ def _convert_names(self, names): return [self.name_class(self.context, self._class_context, name) for name in names] -class SelfNameFilter(InstanceClassFilter): - name_class = LazyInstanceName +class SelfAttributeFilter(InstanceClassFilter): + """ + This class basically filters all the use cases where `self.*` was assigned. + """ + name_class = SelfName def _filter(self, names): names = self._filter_self_names(names) diff --git a/pythonFiles/jedi/evaluate/context/iterable.py b/pythonFiles/jedi/evaluate/context/iterable.py index d0f468e43425..df012f50dd8a 100644 --- a/pythonFiles/jedi/evaluate/context/iterable.py +++ b/pythonFiles/jedi/evaluate/context/iterable.py @@ -22,74 +22,56 @@ """ from jedi import debug from jedi import settings +from jedi._compatibility import force_unicode, is_py3 +from jedi.cache import memoize_method from jedi.evaluate import compiled from jedi.evaluate import analysis from jedi.evaluate import recursion from jedi.evaluate.lazy_context import LazyKnownContext, LazyKnownContexts, \ LazyTreeContext -from jedi.evaluate.helpers import is_string, predefine_names, evaluate_call_of_leaf +from jedi.evaluate.helpers import get_int_or_none, is_string, \ + predefine_names, evaluate_call_of_leaf from jedi.evaluate.utils import safe_property from jedi.evaluate.utils import to_list from jedi.evaluate.cache import evaluator_method_cache -from jedi.evaluate.filters import ParserTreeFilter, has_builtin_methods, \ - register_builtin_method, SpecialMethodFilter +from jedi.evaluate.filters import ParserTreeFilter, BuiltinOverwrite, \ + publish_method from jedi.evaluate.base_context import ContextSet, NO_CONTEXTS, Context, \ TreeContext, ContextualizedNode from jedi.parser_utils import get_comp_fors -class AbstractIterable(Context): - builtin_methods = {} - api_type = 'instance' +class IterableMixin(object): + def py__stop_iteration_returns(self): + return ContextSet(compiled.builtin_from_name(self.evaluator, u'None')) - def __init__(self, evaluator): - super(AbstractIterable, self).__init__(evaluator, evaluator.BUILTINS) - def get_filters(self, search_global, until_position=None, origin_scope=None): - raise NotImplementedError - - @property - def name(self): - return compiled.CompiledContextName(self, self.array_type) - - -@has_builtin_methods -class GeneratorMixin(object): +class GeneratorBase(BuiltinOverwrite, IterableMixin): array_type = None + special_object_identifier = u'GENERATOR_OBJECT' - @register_builtin_method('send') - @register_builtin_method('next', python_version_match=2) - @register_builtin_method('__next__', python_version_match=3) + @publish_method('send') + @publish_method('next', python_version_match=2) + @publish_method('__next__', python_version_match=3) def py__next__(self): - # TODO add TypeError if params are given. return ContextSet.from_sets(lazy_context.infer() for lazy_context in self.py__iter__()) - def get_filters(self, search_global, until_position=None, origin_scope=None): - gen_obj = compiled.get_special_object(self.evaluator, 'GENERATOR_OBJECT') - yield SpecialMethodFilter(self, self.builtin_methods, gen_obj) - for filter in gen_obj.get_filters(search_global): - yield filter - - def py__bool__(self): - return True - - def py__class__(self): - gen_obj = compiled.get_special_object(self.evaluator, 'GENERATOR_OBJECT') - return gen_obj.py__class__() - @property def name(self): return compiled.CompiledContextName(self, 'generator') -class Generator(GeneratorMixin, Context): +class Generator(GeneratorBase): """Handling of `yield` functions.""" def __init__(self, evaluator, func_execution_context): - super(Generator, self).__init__(evaluator, parent_context=evaluator.BUILTINS) + super(Generator, self).__init__(evaluator) self._func_execution_context = func_execution_context def py__iter__(self): - return self._func_execution_context.get_yield_values() + return self._func_execution_context.get_yield_lazy_contexts() + + def py__stop_iteration_returns(self): + return self._func_execution_context.get_return_values() def __repr__(self): return "<%s of %s>" % (type(self).__name__, self._func_execution_context) @@ -111,32 +93,33 @@ def get_filters(self, search_global, until_position=None, origin_scope=None): yield ParserTreeFilter(self.evaluator, self) -class Comprehension(AbstractIterable): - @staticmethod - def from_atom(evaluator, context, atom): - bracket = atom.children[0] - if bracket == '{': - if atom.children[1].children[1] == ':': - cls = DictComprehension - else: - cls = SetComprehension - elif bracket == '(': - cls = GeneratorComprehension - elif bracket == '[': - cls = ListComprehension - return cls(evaluator, context, atom) +def comprehension_from_atom(evaluator, context, atom): + bracket = atom.children[0] + if bracket == '{': + if atom.children[1].children[1] == ':': + cls = DictComprehension + else: + cls = SetComprehension + elif bracket == '(': + cls = GeneratorComprehension + elif bracket == '[': + cls = ListComprehension + return cls(evaluator, context, atom) + +class ComprehensionMixin(object): def __init__(self, evaluator, defining_context, atom): - super(Comprehension, self).__init__(evaluator) + super(ComprehensionMixin, self).__init__(evaluator) self._defining_context = defining_context self._atom = atom def _get_comprehension(self): + "return 'a for a in b'" # The atom contains a testlist_comp return self._atom.children[1] def _get_comp_for(self): - # The atom contains a testlist_comp + "return CompFor('for a in b')" return self._get_comprehension().children[1] def _eval_node(self, index=0): @@ -154,13 +137,17 @@ def _get_comp_for_context(self, parent_context, comp_for): def _nested(self, comp_fors, parent_context=None): comp_for = comp_fors[0] - input_node = comp_for.children[3] + + is_async = 'async' == comp_for.children[comp_for.children.index('for') - 1] + + input_node = comp_for.children[comp_for.children.index('in') + 1] parent_context = parent_context or self._defining_context input_types = parent_context.eval_node(input_node) + # TODO: simulate await if self.is_async cn = ContextualizedNode(parent_context, input_node) - iterated = input_types.iterate(cn) - exprlist = comp_for.children[1] + iterated = input_types.iterate(cn, is_async=is_async) + exprlist = comp_for.children[comp_for.children.index('for') + 1] for i, lazy_context in enumerate(iterated): types = lazy_context.infer() dct = unpack_tuple_to_dict(parent_context, types, exprlist) @@ -194,14 +181,18 @@ def __repr__(self): return "<%s of %s>" % (type(self).__name__, self._atom) -class ArrayMixin(object): - def get_filters(self, search_global, until_position=None, origin_scope=None): - # `array.type` is a string with the type, e.g. 'list'. +class Sequence(BuiltinOverwrite, IterableMixin): + api_type = u'instance' + + @property + def name(self): + return compiled.CompiledContextName(self, self.array_type) + + @memoize_method + def get_object(self): compiled_obj = compiled.builtin_from_name(self.evaluator, self.array_type) - yield SpecialMethodFilter(self, self.builtin_methods, compiled_obj) - for typ in compiled_obj.execute_evaluated(self): - for filter in typ.get_filters(): - yield filter + only_obj, = compiled_obj.execute_evaluated(self) + return only_obj def py__bool__(self): return None # We don't know the length, because of appends. @@ -211,7 +202,7 @@ def py__class__(self): @safe_property def parent(self): - return self.evaluator.BUILTINS + return self.evaluator.builtins_module def dict_values(self): return ContextSet.from_sets( @@ -220,8 +211,8 @@ def dict_values(self): ) -class ListComprehension(ArrayMixin, Comprehension): - array_type = 'list' +class ListComprehension(ComprehensionMixin, Sequence): + array_type = u'list' def py__getitem__(self, index): if isinstance(index, slice): @@ -231,13 +222,12 @@ def py__getitem__(self, index): return all_types[index].infer() -class SetComprehension(ArrayMixin, Comprehension): - array_type = 'set' +class SetComprehension(ComprehensionMixin, Sequence): + array_type = u'set' -@has_builtin_methods -class DictComprehension(ArrayMixin, Comprehension): - array_type = 'dict' +class DictComprehension(ComprehensionMixin, Sequence): + array_type = u'dict' def _get_comp_for(self): return self._get_comprehension().children[3] @@ -250,38 +240,38 @@ def py__getitem__(self, index): for keys, values in self._iterate(): for k in keys: if isinstance(k, compiled.CompiledObject): - if k.obj == index: + if k.get_safe_value(default=object()) == index: return values return self.dict_values() def dict_values(self): return ContextSet.from_sets(values for keys, values in self._iterate()) - @register_builtin_method('values') + @publish_method('values') def _imitate_values(self): lazy_context = LazyKnownContexts(self.dict_values()) - return ContextSet(FakeSequence(self.evaluator, 'list', [lazy_context])) + return ContextSet(FakeSequence(self.evaluator, u'list', [lazy_context])) - @register_builtin_method('items') + @publish_method('items') def _imitate_items(self): items = ContextSet.from_iterable( FakeSequence( - self.evaluator, 'tuple' + self.evaluator, u'tuple' (LazyKnownContexts(keys), LazyKnownContexts(values)) ) for keys, values in self._iterate() ) - return create_evaluated_sequence_set(self.evaluator, items, sequence_type='list') + return create_evaluated_sequence_set(self.evaluator, items, sequence_type=u'list') -class GeneratorComprehension(GeneratorMixin, Comprehension): +class GeneratorComprehension(ComprehensionMixin, GeneratorBase): pass -class SequenceLiteralContext(ArrayMixin, AbstractIterable): - mapping = {'(': 'tuple', - '[': 'list', - '{': 'set'} +class SequenceLiteralContext(Sequence): + mapping = {'(': u'tuple', + '[': u'list', + '{': u'set'} def __init__(self, evaluator, defining_context, atom): super(SequenceLiteralContext, self).__init__(evaluator) @@ -289,18 +279,19 @@ def __init__(self, evaluator, defining_context, atom): self._defining_context = defining_context if self.atom.type in ('testlist_star_expr', 'testlist'): - self.array_type = 'tuple' + self.array_type = u'tuple' else: self.array_type = SequenceLiteralContext.mapping[atom.children[0]] """The builtin name of the array (list, set, tuple or dict).""" def py__getitem__(self, index): """Here the index is an int/str. Raises IndexError/KeyError.""" - if self.array_type == 'dict': + if self.array_type == u'dict': + compiled_obj_index = compiled.create_simple_object(self.evaluator, index) for key, value in self._items(): for k in self._defining_context.eval_node(key): if isinstance(k, compiled.CompiledObject) \ - and index == k.obj: + and k.execute_operation(compiled_obj_index, u'==').get_safe_value(): return self._defining_context.eval_node(value) raise KeyError('No key found in dictionary %s.' % self) @@ -315,7 +306,7 @@ def py__iter__(self): While values returns the possible values for any array field, this function returns the value for a certain index. """ - if self.array_type == 'dict': + if self.array_type == u'dict': # Get keys. types = ContextSet() for k, _ in self._items(): @@ -333,7 +324,7 @@ def py__iter__(self): def _values(self): """Returns a list of a list of node.""" - if self.array_type == 'dict': + if self.array_type == u'dict': return ContextSet.from_sets(v for k, v in self._items()) else: return self._items() @@ -373,37 +364,36 @@ def exact_key_items(self): for key_node, value in self._items(): for key in self._defining_context.eval_node(key_node): if is_string(key): - yield key.obj, LazyTreeContext(self._defining_context, value) + yield key.get_safe_value(), LazyTreeContext(self._defining_context, value) def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.atom) -@has_builtin_methods class DictLiteralContext(SequenceLiteralContext): - array_type = 'dict' + array_type = u'dict' def __init__(self, evaluator, defining_context, atom): super(SequenceLiteralContext, self).__init__(evaluator) self._defining_context = defining_context self.atom = atom - @register_builtin_method('values') + @publish_method('values') def _imitate_values(self): lazy_context = LazyKnownContexts(self.dict_values()) - return ContextSet(FakeSequence(self.evaluator, 'list', [lazy_context])) + return ContextSet(FakeSequence(self.evaluator, u'list', [lazy_context])) - @register_builtin_method('items') + @publish_method('items') def _imitate_items(self): lazy_contexts = [ LazyKnownContext(FakeSequence( - self.evaluator, 'tuple', + self.evaluator, u'tuple', (LazyTreeContext(self._defining_context, key_node), LazyTreeContext(self._defining_context, value_node)) )) for key_node, value_node in self._items() ] - return ContextSet(FakeSequence(self.evaluator, 'list', lazy_contexts)) + return ContextSet(FakeSequence(self.evaluator, u'list', lazy_contexts)) class _FakeArray(SequenceLiteralContext): @@ -437,16 +427,38 @@ def __repr__(self): class FakeDict(_FakeArray): def __init__(self, evaluator, dct): - super(FakeDict, self).__init__(evaluator, dct, 'dict') + super(FakeDict, self).__init__(evaluator, dct, u'dict') self._dct = dct def py__iter__(self): for key in self._dct: - yield LazyKnownContext(compiled.create(self.evaluator, key)) + yield LazyKnownContext(compiled.create_simple_object(self.evaluator, key)) def py__getitem__(self, index): + if is_py3 and self.evaluator.environment.version_info.major == 2: + # In Python 2 bytes and unicode compare. + if isinstance(index, bytes): + index_unicode = force_unicode(index) + try: + return self._dct[index_unicode].infer() + except KeyError: + pass + elif isinstance(index, str): + index_bytes = index.encode('utf-8') + try: + return self._dct[index_bytes].infer() + except KeyError: + pass + return self._dct[index].infer() + @publish_method('values') + def _values(self): + return ContextSet(FakeSequence( + self.evaluator, u'tuple', + [LazyKnownContexts(self.dict_values())] + )) + def dict_values(self): return ContextSet.from_sets(lazy_context.infer() for lazy_context in self._dct.values()) @@ -649,7 +661,7 @@ def py__iter__(self): for addition in additions: yield addition - def iterate(self, contextualized_node=None): + def iterate(self, contextualized_node=None, is_async=False): return self.py__iter__() @@ -657,7 +669,7 @@ class Slice(Context): def __init__(self, context, start, stop, step): super(Slice, self).__init__( context.evaluator, - parent_context=context.evaluator.BUILTINS + parent_context=context.evaluator.builtins_module ) self._context = context # all of them are either a Precedence or None. @@ -680,10 +692,9 @@ def get(element): # For simplicity, we want slices to be clear defined with just # one type. Otherwise we will return an empty slice object. raise IndexError - try: - return list(result)[0].obj - except AttributeError: - return None + + context, = result + return get_int_or_none(context) try: return slice(get(self._start), get(self._stop), get(self._step)) diff --git a/pythonFiles/jedi/evaluate/context/klass.py b/pythonFiles/jedi/evaluate/context/klass.py index b7d61d3e16bf..3157250161e2 100644 --- a/pythonFiles/jedi/evaluate/context/klass.py +++ b/pythonFiles/jedi/evaluate/context/klass.py @@ -89,7 +89,7 @@ class ClassContext(use_metaclass(CachedMetaClass, TreeContext)): This class is not only important to extend `tree.Class`, it is also a important for descriptors (if the descriptor methods are evaluated or not). """ - api_type = 'class' + api_type = u'class' def __init__(self, evaluator, parent_context, classdef): super(ClassContext, self).__init__(evaluator, parent_context=parent_context) @@ -136,17 +136,17 @@ def py__bases__(self): arglist = self.tree_node.get_super_arglist() if arglist: from jedi.evaluate import arguments - args = arguments.TreeArguments(self.evaluator, self, arglist) + args = arguments.TreeArguments(self.evaluator, self.parent_context, arglist) return [value for key, value in args.unpack() if key is None] else: - return [LazyKnownContext(compiled.create(self.evaluator, object))] + return [LazyKnownContext(compiled.builtin_from_name(self.evaluator, u'object'))] def py__call__(self, params): from jedi.evaluate.context import TreeInstance return ContextSet(TreeInstance(self.evaluator, self.parent_context, self, params)) def py__class__(self): - return compiled.create(self.evaluator, type) + return compiled.builtin_from_name(self.evaluator, u'type') def get_params(self): from jedi.evaluate.context import AnonymousInstance @@ -182,7 +182,7 @@ def get_function_slot_names(self, name): return [] def get_param_names(self): - for name in self.get_function_slot_names('__init__'): + for name in self.get_function_slot_names(u'__init__'): for context_ in name.infer(): try: method = context_.get_param_names diff --git a/pythonFiles/jedi/evaluate/context/module.py b/pythonFiles/jedi/evaluate/context/module.py index 5ba92cdb1c3e..8d4da11bc91a 100644 --- a/pythonFiles/jedi/evaluate/context/module.py +++ b/pythonFiles/jedi/evaluate/context/module.py @@ -1,14 +1,12 @@ -import pkgutil -import imp import re import os from parso import python_bytes_to_unicode -from jedi._compatibility import use_metaclass -from jedi.evaluate.cache import CachedMetaClass, evaluator_method_cache +from jedi.evaluate.cache import evaluator_method_cache +from jedi._compatibility import iter_modules, all_suffixes from jedi.evaluate.filters import GlobalNameFilter, ContextNameMixin, \ - AbstractNameDefinition, ParserTreeFilter, DictFilter + AbstractNameDefinition, ParserTreeFilter, DictFilter, MergedFilter from jedi.evaluate import compiled from jedi.evaluate.base_context import TreeContext from jedi.evaluate.imports import SubModuleName, infer_import @@ -18,14 +16,14 @@ class _ModuleAttributeName(AbstractNameDefinition): """ For module attributes like __file__, __str__ and so on. """ - api_type = 'instance' + api_type = u'instance' def __init__(self, parent_module, string_name): self.parent_context = parent_module self.string_name = string_name def infer(self): - return compiled.create(self.parent_context.evaluator, str).execute_evaluated() + return compiled.get_string_context_set(self.parent_context.evaluator) class ModuleName(ContextNameMixin, AbstractNameDefinition): @@ -40,23 +38,26 @@ def string_name(self): return self._name -class ModuleContext(use_metaclass(CachedMetaClass, TreeContext)): - api_type = 'module' +class ModuleContext(TreeContext): + api_type = u'module' parent_context = None - def __init__(self, evaluator, module_node, path): + def __init__(self, evaluator, module_node, path, code_lines): super(ModuleContext, self).__init__(evaluator, parent_context=None) self.tree_node = module_node self._path = path + self.code_lines = code_lines def get_filters(self, search_global, until_position=None, origin_scope=None): - yield ParserTreeFilter( - self.evaluator, - context=self, - until_position=until_position, - origin_scope=origin_scope + yield MergedFilter( + ParserTreeFilter( + self.evaluator, + context=self, + until_position=until_position, + origin_scope=origin_scope + ), + GlobalNameFilter(self, self.tree_node), ) - yield GlobalNameFilter(self, self.tree_node) yield DictFilter(self._sub_modules_dict()) yield DictFilter(self._module_attributes_dict()) for star_module in self.star_imports(): @@ -64,7 +65,7 @@ def get_filters(self, search_global, until_position=None, origin_scope=None): # I'm not sure if the star import cache is really that effective anymore # with all the other really fast import caches. Recheck. Also we would need - # to push the star imports into Evaluator.modules, if we reenable this. + # to push the star imports into Evaluator.module_cache, if we reenable this. @evaluator_method_cache([]) def star_imports(self): modules = [] @@ -93,7 +94,7 @@ def _string_name(self): sep = (re.escape(os.path.sep),) * 2 r = re.search(r'([^%s]*?)(%s__init__)?(\.py|\.so)?$' % sep, self._path) # Remove PEP 3149 names - return re.sub('\.[a-z]+-\d{2}[mud]{0,3}$', '', r.group(1)) + return re.sub(r'\.[a-z]+-\d{2}[mud]{0,3}$', '', r.group(1)) @property @evaluator_method_cache() @@ -105,7 +106,7 @@ def _get_init_directory(self): :return: The path to the directory of a package. None in case it's not a package. """ - for suffix, _, _ in imp.get_suffixes(): + for suffix in all_suffixes(): ending = '__init__' + suffix py__file__ = self.py__file__() if py__file__ is not None and py__file__.endswith(ending): @@ -114,7 +115,7 @@ def _get_init_directory(self): return None def py__name__(self): - for name, module in self.evaluator.modules.items(): + for name, module in self.evaluator.module_cache.iterate_modules_with_names(): if module == self and name != '': return name @@ -131,12 +132,12 @@ def py__file__(self): def py__package__(self): if self._get_init_directory() is None: - return re.sub(r'\.?[^\.]+$', '', self.py__name__()) + return re.sub(r'\.?[^.]+$', '', self.py__name__()) else: return self.py__name__() def _py__path__(self): - search_path = self.evaluator.project.sys_path + search_path = self.evaluator.get_sys_path() init_path = self.py__file__() if os.path.basename(init_path) == '__init__.py': with open(init_path, 'rb') as f: @@ -188,7 +189,7 @@ def _sub_modules_dict(self): path = self._path names = {} if path is not None and path.endswith(os.path.sep + '__init__.py'): - mods = pkgutil.iter_modules([os.path.dirname(path)]) + mods = iter_modules([os.path.dirname(path)]) for module_loader, name, is_pkg in mods: # It's obviously a relative import to the current module. names[name] = SubModuleName(self, name) @@ -203,11 +204,9 @@ def _sub_modules_dict(self): return names def py__class__(self): - return compiled.get_special_object(self.evaluator, 'MODULE_CLASS') + return compiled.get_special_object(self.evaluator, u'MODULE_CLASS') def __repr__(self): return "<%s: %s@%s-%s>" % ( self.__class__.__name__, self._string_name, self.tree_node.start_pos[0], self.tree_node.end_pos[0]) - - diff --git a/pythonFiles/jedi/evaluate/context/namespace.py b/pythonFiles/jedi/evaluate/context/namespace.py index e40c23a5e8ca..380d7c2270b9 100644 --- a/pythonFiles/jedi/evaluate/context/namespace.py +++ b/pythonFiles/jedi/evaluate/context/namespace.py @@ -1,11 +1,10 @@ import os from itertools import chain -from jedi._compatibility import use_metaclass -from jedi.evaluate.cache import evaluator_method_cache, CachedMetaClass +from jedi.evaluate.cache import evaluator_method_cache from jedi.evaluate import imports from jedi.evaluate.filters import DictFilter, AbstractNameDefinition -from jedi.evaluate.base_context import NO_CONTEXTS, TreeContext +from jedi.evaluate.base_context import TreeContext, ContextSet class ImplicitNSName(AbstractNameDefinition): @@ -14,27 +13,31 @@ class ImplicitNSName(AbstractNameDefinition): This object will prevent Jedi from raising exceptions """ def __init__(self, implicit_ns_context, string_name): - self.implicit_ns_context = implicit_ns_context + self.parent_context = implicit_ns_context self.string_name = string_name def infer(self): - return NO_CONTEXTS + return ContextSet(self.parent_context) def get_root_context(self): - return self.implicit_ns_context + return self.parent_context -class ImplicitNamespaceContext(use_metaclass(CachedMetaClass, TreeContext)): +class ImplicitNamespaceContext(TreeContext): """ Provides support for implicit namespace packages """ - api_type = 'module' + # Is a module like every other module, because if you import an empty + # folder foobar it will be available as an object: + # . + api_type = u'module' parent_context = None - def __init__(self, evaluator, fullname): + def __init__(self, evaluator, fullname, paths): super(ImplicitNamespaceContext, self).__init__(evaluator, parent_context=None) self.evaluator = evaluator - self.fullname = fullname + self._fullname = fullname + self.paths = paths def get_filters(self, search_global, until_position=None, origin_scope=None): yield DictFilter(self._sub_modules_dict()) @@ -51,7 +54,7 @@ def py__file__(self): def py__package__(self): """Return the fullname """ - return self.fullname + return self._fullname @property def py__path__(self): @@ -61,8 +64,7 @@ def py__path__(self): def _sub_modules_dict(self): names = {} - paths = self.paths - file_names = chain.from_iterable(os.listdir(path) for path in paths) + file_names = chain.from_iterable(os.listdir(path) for path in self.paths) mods = [ file_name.rpartition('.')[0] if '.' in file_name else file_name for file_name in file_names diff --git a/pythonFiles/jedi/evaluate/docstrings.py b/pythonFiles/jedi/evaluate/docstrings.py index f9c1141226e9..a927abd09028 100644 --- a/pythonFiles/jedi/evaluate/docstrings.py +++ b/pythonFiles/jedi/evaluate/docstrings.py @@ -18,7 +18,7 @@ import re from textwrap import dedent -from parso import parse +from parso import parse, ParserSyntaxError from jedi._compatibility import u from jedi.evaluate.utils import indent_block @@ -42,49 +42,59 @@ REST_ROLE_PATTERN = re.compile(r':[^`]+:`([^`]+)`') -try: - from numpydoc.docscrape import NumpyDocString -except ImportError: - def _search_param_in_numpydocstr(docstr, param_str): - return [] +_numpy_doc_string_cache = None - def _search_return_in_numpydocstr(docstr): - return [] -else: - def _search_param_in_numpydocstr(docstr, param_str): - """Search `docstr` (in numpydoc format) for type(-s) of `param_str`.""" - try: - # This is a non-public API. If it ever changes we should be - # prepared and return gracefully. - params = NumpyDocString(docstr)._parsed_data['Parameters'] - except (KeyError, AttributeError): - return [] - for p_name, p_type, p_descr in params: - if p_name == param_str: - m = re.match('([^,]+(,[^,]+)*?)(,[ ]*optional)?$', p_type) - if m: - p_type = m.group(1) - return list(_expand_typestr(p_type)) + +def _get_numpy_doc_string_cls(): + global _numpy_doc_string_cache + try: + from numpydoc.docscrape import NumpyDocString + _numpy_doc_string_cache = NumpyDocString + except ImportError as e: + _numpy_doc_string_cache = e + if isinstance(_numpy_doc_string_cache, ImportError): + raise _numpy_doc_string_cache + return _numpy_doc_string_cache + + +def _search_param_in_numpydocstr(docstr, param_str): + """Search `docstr` (in numpydoc format) for type(-s) of `param_str`.""" + try: + # This is a non-public API. If it ever changes we should be + # prepared and return gracefully. + params = _get_numpy_doc_string_cls()(docstr)._parsed_data['Parameters'] + except (KeyError, AttributeError, ImportError): return [] + for p_name, p_type, p_descr in params: + if p_name == param_str: + m = re.match('([^,]+(,[^,]+)*?)(,[ ]*optional)?$', p_type) + if m: + p_type = m.group(1) + return list(_expand_typestr(p_type)) + return [] - def _search_return_in_numpydocstr(docstr): - """ - Search `docstr` (in numpydoc format) for type(-s) of function returns. - """ - doc = NumpyDocString(docstr) - try: - # This is a non-public API. If it ever changes we should be - # prepared and return gracefully. - returns = doc._parsed_data['Returns'] - returns += doc._parsed_data['Yields'] - except (KeyError, AttributeError): - raise StopIteration - for r_name, r_type, r_descr in returns: - #Return names are optional and if so the type is in the name - if not r_type: - r_type = r_name - for type_ in _expand_typestr(r_type): - yield type_ + +def _search_return_in_numpydocstr(docstr): + """ + Search `docstr` (in numpydoc format) for type(-s) of function returns. + """ + try: + doc = _get_numpy_doc_string_cls()(docstr) + except ImportError: + return + try: + # This is a non-public API. If it ever changes we should be + # prepared and return gracefully. + returns = doc._parsed_data['Returns'] + returns += doc._parsed_data['Yields'] + except (KeyError, AttributeError): + return + for r_name, r_type, r_descr in returns: + # Return names are optional and if so the type is in the name + if not r_type: + r_type = r_name + for type_ in _expand_typestr(r_type): + yield type_ def _expand_typestr(type_str): @@ -145,8 +155,7 @@ def _search_param_in_docstr(docstr, param_str): if match: return [_strip_rst_role(match.group(1))] - return (_search_param_in_numpydocstr(docstr, param_str) or - []) + return _search_param_in_numpydocstr(docstr, param_str) def _strip_rst_role(type_str): @@ -179,7 +188,7 @@ def pseudo_docstring_stuff(): Need this docstring so that if the below part is not valid Python this is still a function. ''' - {0} + {} """)) if string is None: return [] @@ -193,7 +202,10 @@ def pseudo_docstring_stuff(): # will be impossible to use `...` (Ellipsis) as a token. Docstring types # don't need to conform with the current grammar. grammar = module_context.evaluator.latest_grammar - module = grammar.parse(code.format(indent_block(string))) + try: + module = grammar.parse(code.format(indent_block(string)), error_recovery=False) + except ParserSyntaxError: + return [] try: funcdef = next(module.iter_funcdefs()) # First pick suite, then simple_stmt and then the node, @@ -243,7 +255,7 @@ def _execute_array_values(evaluator, array): for typ in lazy_context.infer() ) values.append(LazyKnownContexts(objects)) - return set([FakeSequence(evaluator, array.array_type, values)]) + return {FakeSequence(evaluator, array.array_type, values)} else: return array.execute_evaluated() diff --git a/pythonFiles/jedi/evaluate/dynamic.py b/pythonFiles/jedi/evaluate/dynamic.py index 7d05000dc9d5..9e8d57144bdb 100644 --- a/pythonFiles/jedi/evaluate/dynamic.py +++ b/pythonFiles/jedi/evaluate/dynamic.py @@ -73,24 +73,33 @@ def search_params(evaluator, execution_context, funcdef): # you will see the slowdown, especially in 3.6. return create_default_params(execution_context, funcdef) - debug.dbg('Dynamic param search in %s.', funcdef.name.value, color='MAGENTA') - - module_context = execution_context.get_root_context() - function_executions = _search_function_executions( - evaluator, - module_context, - funcdef - ) - if function_executions: - zipped_params = zip(*list( - function_execution.get_params() - for function_execution in function_executions - )) - params = [MergedExecutedParams(executed_params) for executed_params in zipped_params] - # Evaluate the ExecutedParams to types. + if funcdef.type == 'lambdef': + string_name = _get_lambda_name(funcdef) + if string_name is None: + return create_default_params(execution_context, funcdef) else: - return create_default_params(execution_context, funcdef) - debug.dbg('Dynamic param result finished', color='MAGENTA') + string_name = funcdef.name.value + debug.dbg('Dynamic param search in %s.', string_name, color='MAGENTA') + + try: + module_context = execution_context.get_root_context() + function_executions = _search_function_executions( + evaluator, + module_context, + funcdef, + string_name=string_name, + ) + if function_executions: + zipped_params = zip(*list( + function_execution.get_params() + for function_execution in function_executions + )) + params = [MergedExecutedParams(executed_params) for executed_params in zipped_params] + # Evaluate the ExecutedParams to types. + else: + return create_default_params(execution_context, funcdef) + finally: + debug.dbg('Dynamic param result finished', color='MAGENTA') return params finally: evaluator.dynamic_params_depth -= 1 @@ -98,25 +107,24 @@ def search_params(evaluator, execution_context, funcdef): @evaluator_function_cache(default=None) @to_list -def _search_function_executions(evaluator, module_context, funcdef): +def _search_function_executions(evaluator, module_context, funcdef, string_name): """ Returns a list of param names. """ - func_string_name = funcdef.name.value compare_node = funcdef - if func_string_name == '__init__': + if string_name == '__init__': cls = get_parent_scope(funcdef) if isinstance(cls, tree.Class): - func_string_name = cls.name.value + string_name = cls.name.value compare_node = cls found_executions = False i = 0 for for_mod_context in imports.get_modules_containing_name( - evaluator, [module_context], func_string_name): + evaluator, [module_context], string_name): if not isinstance(module_context, ModuleContext): return - for name, trailer in _get_possible_nodes(for_mod_context, func_string_name): + for name, trailer in _get_possible_nodes(for_mod_context, string_name): i += 1 # This is a simple way to stop Jedi's dynamic param recursion @@ -137,6 +145,18 @@ def _search_function_executions(evaluator, module_context, funcdef): return +def _get_lambda_name(node): + stmt = node.parent + if stmt.type == 'expr_stmt': + first_operator = next(stmt.yield_operators(), None) + if first_operator == '=': + first = stmt.children[0] + if first.type == 'name': + return first.value + + return None + + def _get_possible_nodes(module_context, func_string_name): try: names = module_context.tree_node.get_used_names()[func_string_name] @@ -156,11 +176,9 @@ def _check_name_for_execution(evaluator, context, compare_node, name, trailer): def create_func_excs(): arglist = trailer.children[1] if arglist == ')': - arglist = () + arglist = None args = TreeArguments(evaluator, context, arglist, trailer) - if value_node.type == 'funcdef': - yield value.get_function_execution(args) - else: + if value_node.type == 'classdef': created_instance = instance.TreeInstance( evaluator, value.parent_context, @@ -169,6 +187,8 @@ def create_func_excs(): ) for execution in created_instance.create_init_executions(): yield execution + else: + yield value.get_function_execution(args) for value in evaluator.goto_definitions(context, name): value_node = value.tree_node diff --git a/pythonFiles/jedi/evaluate/filters.py b/pythonFiles/jedi/evaluate/filters.py index 35dff9dace65..4294f2a60004 100644 --- a/pythonFiles/jedi/evaluate/filters.py +++ b/pythonFiles/jedi/evaluate/filters.py @@ -6,7 +6,8 @@ from parso.tree import search_ancestor -from jedi._compatibility import is_py3 +from jedi._compatibility import use_metaclass, Parameter +from jedi.cache import memoize_method from jedi.evaluate import flow_analysis from jedi.evaluate.base_context import ContextSet, Context from jedi.parser_utils import get_parent_scope @@ -27,7 +28,7 @@ def infer(self): def goto(self): # Typically names are already definitions and therefore a goto on that # name will always result on itself. - return set([self]) + return {self} def get_root_context(self): return self.parent_context.get_root_context() @@ -43,6 +44,9 @@ def execute(self, arguments): def execute_evaluated(self, *args, **kwargs): return self.infer().execute_evaluated(*args, **kwargs) + def is_import(self): + return False + @property def api_type(self): return self.parent_context.api_type @@ -56,6 +60,10 @@ def __init__(self, parent_context, tree_name): def goto(self): return self.parent_context.evaluator.goto(self.parent_context, self.tree_name) + def is_import(self): + imp = search_ancestor(self.tree_name, 'import_from', 'import_name') + return imp is not None + @property def string_name(self): return self.tree_name.value @@ -108,12 +116,28 @@ def api_type(self): class ParamName(AbstractTreeName): - api_type = 'param' + api_type = u'param' def __init__(self, parent_context, tree_name): self.parent_context = parent_context self.tree_name = tree_name + def get_kind(self): + tree_param = search_ancestor(self.tree_name, 'param') + if tree_param.star_count == 1: # *args + return Parameter.VAR_POSITIONAL + if tree_param.star_count == 2: # **kwargs + return Parameter.VAR_KEYWORD + + parent = tree_param.parent + for p in parent.children: + if p.type == 'param': + if p.star_count: + return Parameter.KEYWORD_ONLY + if p == tree_param: + break + return Parameter.POSITIONAL_OR_KEYWORD + def infer(self): return self.get_param().infer() @@ -163,7 +187,7 @@ def __init__(self, context, parser_scope): def get(self, name): try: - names = self._used_names[str(name)] + names = self._used_names[name] except KeyError: return [] @@ -213,7 +237,10 @@ def _is_name_reachable(self, name): def _check_flows(self, names): for name in sorted(names, key=lambda name: name.start_pos, reverse=True): check = flow_analysis.reachability_check( - self._node_context, self._parser_scope, name, self._origin_scope + context=self._node_context, + context_scope=self._parser_scope, + node=name, + origin_scope=self._origin_scope ) if check is not flow_analysis.UNREACHABLE: yield name @@ -266,22 +293,42 @@ def __init__(self, dct): def get(self, name): try: - value = self._convert(name, self._dct[str(name)]) + value = self._convert(name, self._dct[name]) except KeyError: return [] - - return list(self._filter([value])) + else: + return list(self._filter([value])) def values(self): - return self._filter(self._convert(*item) for item in self._dct.items()) + def yielder(): + for item in self._dct.items(): + try: + yield self._convert(*item) + except KeyError: + pass + return self._filter(yielder()) def _convert(self, name, value): return value +class MergedFilter(object): + def __init__(self, *filters): + self._filters = filters + + def get(self, name): + return [n for filter in self._filters for n in filter.get(name)] + + def values(self): + return [n for filter in self._filters for n in filter.values()] + + def __repr__(self): + return '%s(%s)' % (self.__class__.__name__, ', '.join(str(f) for f in self._filters)) + + class _BuiltinMappedMethod(Context): """``Generator.__next__`` ``dict.values`` methods and so on.""" - api_type = 'function' + api_type = u'function' def __init__(self, builtin_context, method, builtin_func): super(_BuiltinMappedMethod, self).__init__( @@ -292,6 +339,7 @@ def __init__(self, builtin_context, method, builtin_func): self._builtin_func = builtin_func def py__call__(self, params): + # TODO add TypeError if params are given/or not correct. return self._method(self.parent_context) def __getattr__(self, name): @@ -304,21 +352,33 @@ class SpecialMethodFilter(DictFilter): classes like Generator (for __next__, etc). """ class SpecialMethodName(AbstractNameDefinition): - api_type = 'function' + api_type = u'function' + + def __init__(self, parent_context, string_name, value, builtin_context): + callable_, python_version = value + if python_version is not None and \ + python_version != parent_context.evaluator.environment.version_info.major: + raise KeyError - def __init__(self, parent_context, string_name, callable_, builtin_context): self.parent_context = parent_context self.string_name = string_name self._callable = callable_ self._builtin_context = builtin_context def infer(self): - filter = next(self._builtin_context.get_filters()) - # We can take the first index, because on builtin methods there's - # always only going to be one name. The same is true for the - # inferred values. - builtin_func = next(iter(filter.get(self.string_name)[0].infer())) - return ContextSet(_BuiltinMappedMethod(self.parent_context, self._callable, builtin_func)) + for filter in self._builtin_context.get_filters(): + # We can take the first index, because on builtin methods there's + # always only going to be one name. The same is true for the + # inferred values. + for name in filter.get(self.string_name): + builtin_func = next(iter(name.infer())) + break + else: + continue + break + return ContextSet( + _BuiltinMappedMethod(self.parent_context, self._callable, builtin_func) + ) def __init__(self, context, dct, builtin_context): super(SpecialMethodFilter, self).__init__(dct) @@ -335,34 +395,58 @@ def _convert(self, name, value): return self.SpecialMethodName(self.context, name, value, self._builtin_context) -def has_builtin_methods(cls): - base_dct = {} - # Need to care properly about inheritance. Builtin Methods should not get - # lost, just because they are not mentioned in a class. - for base_cls in reversed(cls.__bases__): - try: - base_dct.update(base_cls.builtin_methods) - except AttributeError: - pass +class _OverwriteMeta(type): + def __init__(cls, name, bases, dct): + super(_OverwriteMeta, cls).__init__(name, bases, dct) - cls.builtin_methods = base_dct - for func in cls.__dict__.values(): - try: - cls.builtin_methods.update(func.registered_builtin_methods) - except AttributeError: - pass - return cls - - -def register_builtin_method(method_name, python_version_match=None): - def wrapper(func): - if python_version_match and python_version_match != 2 + int(is_py3): - # Some functions do only apply to certain versions. - return func - dct = func.__dict__.setdefault('registered_builtin_methods', {}) - dct[method_name] = func + base_dct = {} + for base_cls in reversed(cls.__bases__): + try: + base_dct.update(base_cls.overwritten_methods) + except AttributeError: + pass + + for func in cls.__dict__.values(): + try: + base_dct.update(func.registered_overwritten_methods) + except AttributeError: + pass + cls.overwritten_methods = base_dct + + +class AbstractObjectOverwrite(use_metaclass(_OverwriteMeta, object)): + def get_object(self): + raise NotImplementedError + + def get_filters(self, search_global, *args, **kwargs): + yield SpecialMethodFilter(self, self.overwritten_methods, self.get_object()) + + for filter in self.get_object().get_filters(search_global): + yield filter + + +class BuiltinOverwrite(Context, AbstractObjectOverwrite): + special_object_identifier = None + + def __init__(self, evaluator): + super(BuiltinOverwrite, self).__init__(evaluator, evaluator.builtins_module) + + @memoize_method + def get_object(self): + from jedi.evaluate import compiled + assert self.special_object_identifier + return compiled.get_special_object(self.evaluator, self.special_object_identifier) + + def py__class__(self): + return self.get_object().py__class__() + + +def publish_method(method_name, python_version_match=None): + def decorator(func): + dct = func.__dict__.setdefault('registered_overwritten_methods', {}) + dct[method_name] = func, python_version_match return func - return wrapper + return decorator def get_global_filters(evaluator, context, until_position, origin_scope): @@ -379,40 +463,37 @@ def get_global_filters(evaluator, context, until_position, origin_scope): ... def func(): ... y = None ... ''')) - >>> module_node = script._get_module_node() + >>> module_node = script._module_node >>> scope = next(module_node.iter_funcdefs()) >>> scope >>> context = script._get_module().create_context(scope) >>> filters = list(get_global_filters(context.evaluator, context, (4, 0), None)) - First we get the names names from the function scope. + First we get the names from the function scope. - >>> no_unicode_pprint(filters[0]) - > + >>> no_unicode_pprint(filters[0]) #doctest: +ELLIPSIS + MergedFilter(, ) >>> sorted(str(n) for n in filters[0].values()) ['', ''] - >>> filters[0]._until_position + >>> filters[0]._filters[0]._until_position (4, 0) + >>> filters[0]._filters[1]._until_position Then it yields the names from one level "lower". In this example, this is - the module scope. As a side note, you can see, that the position in the - filter is now None, because typically the whole module is loaded before the - function is called. + the module scope (including globals). + As a side note, you can see, that the position in the filter is None on the + globals filter, because there the whole module is searched. - >>> filters[1].values() # global names -> there are none in our example. - [] - >>> list(filters[2].values()) # package modules -> Also empty. + >>> list(filters[1].values()) # package modules -> Also empty. [] - >>> sorted(name.string_name for name in filters[3].values()) # Module attributes + >>> sorted(name.string_name for name in filters[2].values()) # Module attributes ['__doc__', '__file__', '__name__', '__package__'] - >>> print(filters[1]._until_position) - None Finally, it yields the builtin filter, if `include_builtin` is true (default). - >>> filters[4].values() #doctest: +ELLIPSIS + >>> filters[3].values() #doctest: +ELLIPSIS [, ...] """ from jedi.evaluate.context.function import FunctionExecutionContext @@ -430,5 +511,5 @@ def get_global_filters(evaluator, context, until_position, origin_scope): context = context.parent_context # Add builtins to the global scope. - for filter in evaluator.BUILTINS.get_filters(search_global=True): + for filter in evaluator.builtins_module.get_filters(search_global=True): yield filter diff --git a/pythonFiles/jedi/evaluate/finder.py b/pythonFiles/jedi/evaluate/finder.py index 96032ae9b792..5e7043f79600 100644 --- a/pythonFiles/jedi/evaluate/finder.py +++ b/pythonFiles/jedi/evaluate/finder.py @@ -56,7 +56,10 @@ def find(self, filters, attribute_lookup): names = self.filter_name(filters) if self._found_predefined_types is not None and names: check = flow_analysis.reachability_check( - self._context, self._context.tree_node, self._name) + context=self._context, + context_scope=self._context.tree_node, + node=self._name, + ) if check is flow_analysis.UNREACHABLE: return ContextSet() return self._found_predefined_types @@ -92,7 +95,26 @@ def _get_origin_scope(self): def get_filters(self, search_global=False): origin_scope = self._get_origin_scope() if search_global: - return get_global_filters(self._evaluator, self._context, self._position, origin_scope) + position = self._position + + # For functions and classes the defaults don't belong to the + # function and get evaluated in the context before the function. So + # make sure to exclude the function/class name. + if origin_scope is not None: + ancestor = search_ancestor(origin_scope, 'funcdef', 'classdef', 'lambdef') + lambdef = None + if ancestor == 'lambdef': + # For lambdas it's even more complicated since parts will + # be evaluated later. + lambdef = ancestor + ancestor = search_ancestor(origin_scope, 'funcdef', 'classdef') + if ancestor is not None: + colon = ancestor.children[-2] + if position < colon.start_pos: + if lambdef is None or position < lambdef.children[-2].start_pos: + position = ancestor.start_pos + + return get_global_filters(self._evaluator, self._context, position, origin_scope) else: return self._context.get_filters(search_global, self._position, origin_scope=origin_scope) @@ -102,8 +124,7 @@ def filter_name(self, filters): ``filters``), until a name fits. """ names = [] - if self._context.predefined_names: - # TODO is this ok? node might not always be a tree.Name + if self._context.predefined_names and isinstance(self._name, tree.Name): node = self._name while node is not None and not is_scope(node): node = node.parent @@ -133,14 +154,14 @@ def filter_name(self, filters): continue break - debug.dbg('finder.filter_name "%s" in (%s): %s@%s', self._string_name, - self._context, names, self._position) + debug.dbg('finder.filter_name %s in (%s): %s@%s', + self._string_name, self._context, names, self._position) return list(names) def _check_getattr(self, inst): """Checks for both __getattr__ and __getattribute__ methods""" # str is important, because it shouldn't be `Name`! - name = compiled.create(self._evaluator, self._string_name) + name = compiled.create_simple_object(self._evaluator, self._string_name) # This is a little bit special. `__getattribute__` is in Python # executed before `__getattr__`. But: I know no use case, where @@ -149,8 +170,8 @@ def _check_getattr(self, inst): # We are inversing this, because a hand-crafted `__getattribute__` # could still call another hand-crafted `__getattr__`, but not the # other way around. - names = (inst.get_function_slot_names('__getattr__') or - inst.get_function_slot_names('__getattribute__')) + names = (inst.get_function_slot_names(u'__getattr__') or + inst.get_function_slot_names(u'__getattribute__')) return inst.execute_function_slots(names, name) def _names_to_types(self, names, attribute_lookup): @@ -248,8 +269,7 @@ def _check_isinstance_type(context, element, search_name): context_set = ContextSet() for cls_or_tup in lazy_context_cls.infer(): - if isinstance(cls_or_tup, iterable.AbstractIterable) and \ - cls_or_tup.array_type == 'tuple': + if isinstance(cls_or_tup, iterable.Sequence) and cls_or_tup.array_type == 'tuple': for lazy_context in cls_or_tup.py__iter__(): for context in lazy_context.infer(): context_set |= context.execute_evaluated() diff --git a/pythonFiles/jedi/evaluate/flow_analysis.py b/pythonFiles/jedi/evaluate/flow_analysis.py index 670b7a71934c..946f5a2ecaf0 100644 --- a/pythonFiles/jedi/evaluate/flow_analysis.py +++ b/pythonFiles/jedi/evaluate/flow_analysis.py @@ -1,4 +1,5 @@ from jedi.parser_utils import get_flow_branch_keyword, is_scope, get_parent_scope +from jedi.evaluate.recursion import execution_allowed class Status(object): @@ -104,9 +105,13 @@ def _break_check(context, context_scope, flow_scope, node): def _check_if(context, node): - types = context.eval_node(node) - values = set(x.py__bool__() for x in types) - if len(values) == 1: - return Status.lookup_table[values.pop()] - else: - return UNSURE + with execution_allowed(context.evaluator, node) as allowed: + if not allowed: + return UNSURE + + types = context.eval_node(node) + values = set(x.py__bool__() for x in types) + if len(values) == 1: + return Status.lookup_table[values.pop()] + else: + return UNSURE diff --git a/pythonFiles/jedi/evaluate/helpers.py b/pythonFiles/jedi/evaluate/helpers.py index 3b21e01bda9e..c6226cde2b70 100644 --- a/pythonFiles/jedi/evaluate/helpers.py +++ b/pythonFiles/jedi/evaluate/helpers.py @@ -176,8 +176,6 @@ def get_module_names(module, all_scopes): @contextmanager def predefine_names(context, flow_scope, dct): predefined = context.predefined_names - if flow_scope in predefined: - raise NotImplementedError('Why does this happen?') predefined[flow_scope] = dct try: yield @@ -190,12 +188,27 @@ def is_compiled(context): def is_string(context): - return is_compiled(context) and isinstance(context.obj, (str, unicode)) + if context.evaluator.environment.version_info.major == 2: + str_classes = (unicode, bytes) + else: + str_classes = (unicode,) + return is_compiled(context) and isinstance(context.get_safe_value(default=None), str_classes) def is_literal(context): return is_number(context) or is_string(context) +def _get_safe_value_or_none(context, accept): + if is_compiled(context): + value = context.get_safe_value(default=None) + if isinstance(value, accept): + return value + + +def get_int_or_none(context): + return _get_safe_value_or_none(context, int) + + def is_number(context): - return is_compiled(context) and isinstance(context.obj, (int, float)) + return _get_safe_value_or_none(context, (int, float)) is not None diff --git a/pythonFiles/jedi/evaluate/imports.py b/pythonFiles/jedi/evaluate/imports.py index ecf656b1a676..bcd3bdc74a4d 100644 --- a/pythonFiles/jedi/evaluate/imports.py +++ b/pythonFiles/jedi/evaluate/imports.py @@ -9,31 +9,48 @@ correct implementation is delegated to _compatibility. This module also supports import autocompletion, which means to complete -statements like ``from datetim`` (curser at the end would return ``datetime``). +statements like ``from datetim`` (cursor at the end would return ``datetime``). """ -import imp import os -import pkgutil -import sys from parso.python import tree from parso.tree import search_ancestor -from parso.cache import parser_cache from parso import python_bytes_to_unicode -from jedi._compatibility import find_module, unicode, ImplicitNSInfo +from jedi._compatibility import unicode, ImplicitNSInfo, force_unicode from jedi import debug from jedi import settings +from jedi.parser_utils import get_cached_code_lines from jedi.evaluate import sys_path from jedi.evaluate import helpers from jedi.evaluate import compiled from jedi.evaluate import analysis -from jedi.evaluate.utils import unite +from jedi.evaluate.utils import unite, dotted_from_fs_path from jedi.evaluate.cache import evaluator_method_cache from jedi.evaluate.filters import AbstractNameDefinition from jedi.evaluate.base_context import ContextSet, NO_CONTEXTS +class ModuleCache(object): + def __init__(self): + self._path_cache = {} + self._name_cache = {} + + def add(self, module, name): + path = module.py__file__() + self._path_cache[path] = module + self._name_cache[name] = module + + def iterate_modules_with_names(self): + return self._name_cache.items() + + def get(self, name): + return self._name_cache[name] + + def get_from_path(self, path): + return self._path_cache[path] + + # This memoization is needed, because otherwise we will infinitely loop on # certain imports. @evaluator_method_cache(default=NO_CONTEXTS) @@ -130,20 +147,13 @@ def __repr__(self): def _add_error(context, name, message=None): # Should be a name, not a string! + if message is None: + name_str = str(name.value) if isinstance(name, tree.Name) else name + message = 'No module named ' + name_str if hasattr(name, 'parent'): analysis.add(context, 'import-error', name, message) - - -def get_init_path(directory_path): - """ - The __init__ file can be searched in a directory. If found return it, else - None. - """ - for suffix, _, _ in imp.get_suffixes(): - path = os.path.join(directory_path, '__init__' + suffix) - if os.path.exists(path): - return path - return None + else: + debug.warning('ImportError without origin: ' + message) class ImportName(AbstractNameDefinition): @@ -204,7 +214,7 @@ def __init__(self, evaluator, import_path, module_context, level=0): if level: base = module_context.py__package__().split('.') - if base == ['']: + if base == [''] or base == ['__main__']: base = [] if level > len(base): path = module_context.py__file__() @@ -226,10 +236,11 @@ def __init__(self, evaluator, import_path, module_context, level=0): else: import_path.insert(0, dir_name) else: - _add_error(module_context, import_path[-1]) + _add_error( + module_context, import_path[-1], + message='Attempted relative import beyond top-level package.' + ) import_path = [] - # TODO add import error. - debug.warning('Attempted relative import beyond top-level package.') # If no path is defined in the module we have no ideas where we # are in the file system. Therefore we cannot know what to do. # In this case we just let the path there and ignore that it's @@ -248,27 +259,19 @@ def str_import_path(self): """Returns the import path as pure strings instead of `Name`.""" return tuple( name.value if isinstance(name, tree.Name) else name - for name in self.import_path) + for name in self.import_path + ) def sys_path_with_modifications(self): - in_path = [] - sys_path_mod = self._evaluator.project.sys_path \ + sys_path_mod = self._evaluator.get_sys_path() \ + sys_path.check_sys_path_modifications(self.module_context) - if self.file_path is not None: - # If you edit e.g. gunicorn, there will be imports like this: - # `from gunicorn import something`. But gunicorn is not in the - # sys.path. Therefore look if gunicorn is a parent directory, #56. - if self.import_path: # TODO is this check really needed? - for path in sys_path.traverse_parents(self.file_path): - if os.path.basename(path) == self.str_import_path[0]: - in_path.append(os.path.dirname(path)) - - # Since we know nothing about the call location of the sys.path, - # it's a possibility that the current directory is the origin of - # the Python execution. - sys_path_mod.insert(0, os.path.dirname(self.file_path)) - - return in_path + sys_path_mod + + if self.import_path and self.file_path is not None \ + and self._evaluator.environment.version_info.major == 2: + # Python2 uses an old strange way of importing relative imports. + sys_path_mod.append(force_unicode(os.path.dirname(self.file_path))) + + return sys_path_mod def follow(self): if not self.import_path: @@ -280,7 +283,7 @@ def _do_import(self, import_path, sys_path): This method is very similar to importlib's `_gcd_import`. """ import_parts = [ - i.value if isinstance(i, tree.Name) else i + force_unicode(i.value if isinstance(i, tree.Name) else i) for i in import_path ] @@ -298,7 +301,7 @@ def _do_import(self, import_path, sys_path): module_name = '.'.join(import_parts) try: - return ContextSet(self._evaluator.modules[module_name]) + return ContextSet(self._evaluator.module_cache.get(module_name)) except KeyError: pass @@ -332,62 +335,43 @@ def _do_import(self, import_path, sys_path): for path in paths: # At the moment we are only using one path. So this is # not important to be correct. - try: - if not isinstance(path, list): - path = [path] - module_file, module_path, is_pkg = \ - find_module(import_parts[-1], path, fullname=module_name) + if not isinstance(path, list): + path = [path] + code, module_path, is_pkg = self._evaluator.compiled_subprocess.get_module_info( + string=import_parts[-1], + path=path, + full_name=module_name + ) + if module_path is not None: break - except ImportError: - module_path = None - if module_path is None: + else: _add_error(self.module_context, import_path[-1]) return NO_CONTEXTS else: - parent_module = None - try: - debug.dbg('search_module %s in %s', import_parts[-1], self.file_path) - # Override the sys.path. It works only good that way. - # Injecting the path directly into `find_module` did not work. - sys.path, temp = sys_path, sys.path - try: - module_file, module_path, is_pkg = \ - find_module(import_parts[-1], fullname=module_name) - finally: - sys.path = temp - except ImportError: + debug.dbg('search_module %s in %s', import_parts[-1], self.file_path) + # Override the sys.path. It works only good that way. + # Injecting the path directly into `find_module` did not work. + code, module_path, is_pkg = self._evaluator.compiled_subprocess.get_module_info( + string=import_parts[-1], + full_name=module_name, + sys_path=sys_path, + ) + if module_path is None: # The module is not a package. _add_error(self.module_context, import_path[-1]) return NO_CONTEXTS - code = None - if is_pkg: - # In this case, we don't have a file yet. Search for the - # __init__ file. - if module_path.endswith(('.zip', '.egg')): - code = module_file.loader.get_source(module_name) - else: - module_path = get_init_path(module_path) - elif module_file: - code = module_file.read() - module_file.close() - - if isinstance(module_path, ImplicitNSInfo): - from jedi.evaluate.context.namespace import ImplicitNamespaceContext - fullname, paths = module_path.name, module_path.paths - module = ImplicitNamespaceContext(self._evaluator, fullname=fullname) - module.paths = paths - elif module_file is None and not module_path.endswith(('.py', '.zip', '.egg')): - module = compiled.load_module(self._evaluator, module_path) - else: - module = _load_module(self._evaluator, module_path, code, sys_path, parent_module) + module = _load_module( + self._evaluator, module_path, code, sys_path, + module_name=module_name, + safe_module_name=True, + ) if module is None: # The file might raise an ImportError e.g. and therefore not be # importable. return NO_CONTEXTS - self._evaluator.modules[module_name] = module return ContextSet(module) def _generate_name(self, name, in_module=None): @@ -401,15 +385,17 @@ def _get_module_names(self, search_path=None, in_module=None): Get the names of all modules in the search_path. This means file names and not names defined in the files. """ + sub = self._evaluator.compiled_subprocess names = [] # add builtin module names if search_path is None and in_module is None: - names += [self._generate_name(name) for name in sys.builtin_module_names] + names += [self._generate_name(name) for name in sub.get_builtin_module_names()] if search_path is None: search_path = self.sys_path_with_modifications() - for module_loader, name, is_pkg in pkgutil.iter_modules(search_path): + + for name in sub.list_module_names(search_path): names.append(self._generate_name(name, in_module=in_module)) return names @@ -448,7 +434,7 @@ def completion_names(self, evaluator, only_modules=False): # implicit namespace packages elif isinstance(context, ImplicitNamespaceContext): paths = context.paths - names += self._get_module_names(paths) + names += self._get_module_names(paths, in_module=context) if only_modules: # In the case of an import like `from x.` we don't need to @@ -476,38 +462,65 @@ def completion_names(self, evaluator, only_modules=False): return names -def _load_module(evaluator, path=None, code=None, sys_path=None, parent_module=None): - if sys_path is None: - sys_path = evaluator.project.sys_path +def _load_module(evaluator, path=None, code=None, sys_path=None, + module_name=None, safe_module_name=False): + try: + return evaluator.module_cache.get(module_name) + except KeyError: + pass + try: + return evaluator.module_cache.get_from_path(path) + except KeyError: + pass - dotted_path = path and compiled.dotted_from_fs_path(path, sys_path) - if path is not None and path.endswith(('.py', '.zip', '.egg')) \ - and dotted_path not in settings.auto_import_modules: + if isinstance(path, ImplicitNSInfo): + from jedi.evaluate.context.namespace import ImplicitNamespaceContext + module = ImplicitNamespaceContext( + evaluator, + fullname=path.name, + paths=path.paths, + ) + else: + if sys_path is None: + sys_path = evaluator.get_sys_path() + + dotted_path = path and dotted_from_fs_path(path, sys_path) + if path is not None and path.endswith(('.py', '.zip', '.egg')) \ + and dotted_path not in settings.auto_import_modules: + + module_node = evaluator.parse( + code=code, path=path, cache=True, diff_cache=True, + cache_path=settings.cache_directory) + + from jedi.evaluate.context import ModuleContext + module = ModuleContext( + evaluator, module_node, + path=path, + code_lines=get_cached_code_lines(evaluator.grammar, path), + ) + else: + module = compiled.load_module(evaluator, path=path, sys_path=sys_path) - module_node = evaluator.grammar.parse( - code=code, path=path, cache=True, diff_cache=True, - cache_path=settings.cache_directory) + if module is not None and module_name is not None: + add_module_to_cache(evaluator, module_name, module, safe=safe_module_name) - from jedi.evaluate.context import ModuleContext - return ModuleContext(evaluator, module_node, path=path) - else: - return compiled.load_module(evaluator, path) + return module -def add_module(evaluator, module_name, module): - if '.' not in module_name: +def add_module_to_cache(evaluator, module_name, module, safe=False): + if not safe and '.' not in module_name: # We cannot add paths with dots, because that would collide with # the sepatator dots for nested packages. Therefore we return # `__main__` in ModuleWrapper.py__name__(), which is similar to # Python behavior. - evaluator.modules[module_name] = module + return + evaluator.module_cache.add(module, module_name) def get_modules_containing_name(evaluator, modules, name): """ Search a name in the directories of modules. """ - from jedi.evaluate.context import ModuleContext def check_directories(paths): for p in paths: if p is not None: @@ -519,28 +532,16 @@ def check_directories(paths): if file_name.endswith('.py'): yield path - def check_python_file(path): - try: - # TODO I don't think we should use the cache here?! - node_cache_item = parser_cache[evaluator.grammar._hashed][path] - except KeyError: - try: - return check_fs(path) - except IOError: - return None - else: - module_node = node_cache_item.node - return ModuleContext(evaluator, module_node, path=path) - def check_fs(path): with open(path, 'rb') as f: code = python_bytes_to_unicode(f.read(), errors='replace') if name in code: - module = _load_module(evaluator, path, code) - - module_name = sys_path.dotted_path_in_sys_path(evaluator.project.sys_path, path) - if module_name is not None: - add_module(evaluator, module_name, module) + e_sys_path = evaluator.get_sys_path() + module_name = sys_path.dotted_path_in_sys_path(e_sys_path, path) + module = _load_module( + evaluator, path, code, + sys_path=e_sys_path, module_name=module_name + ) return module # skip non python modules @@ -565,6 +566,6 @@ def check_fs(path): # Sort here to make issues less random. for p in sorted(paths): # make testing easier, sort it - same results on every interpreter - m = check_python_file(p) + m = check_fs(p) if m is not None and not isinstance(m, compiled.CompiledObject): yield m diff --git a/pythonFiles/jedi/evaluate/param.py b/pythonFiles/jedi/evaluate/param.py index a46394ce8a81..1445ef0c8ff6 100644 --- a/pythonFiles/jedi/evaluate/param.py +++ b/pythonFiles/jedi/evaluate/param.py @@ -97,7 +97,7 @@ def get_params(execution_context, var_args): var_arg_iterator.push_back((key, argument)) break lazy_context_list.append(argument) - seq = iterable.FakeSequence(execution_context.evaluator, 'tuple', lazy_context_list) + seq = iterable.FakeSequence(execution_context.evaluator, u'tuple', lazy_context_list) result_arg = LazyKnownContext(seq) elif param.star_count == 2: # **kwargs param @@ -176,7 +176,7 @@ def _error_argument_count(funcdef, actual_count): def _create_default_param(execution_context, param): if param.star_count == 1: result_arg = LazyKnownContext( - iterable.FakeSequence(execution_context.evaluator, 'tuple', []) + iterable.FakeSequence(execution_context.evaluator, u'tuple', []) ) elif param.star_count == 2: result_arg = LazyKnownContext( @@ -192,4 +192,3 @@ def _create_default_param(execution_context, param): def create_default_params(execution_context, funcdef): return [_create_default_param(execution_context, p) for p in funcdef.get_params()] - diff --git a/pythonFiles/jedi/evaluate/pep0484.py b/pythonFiles/jedi/evaluate/pep0484.py index 820f112c54e0..f23943e1a8a5 100644 --- a/pythonFiles/jedi/evaluate/pep0484.py +++ b/pythonFiles/jedi/evaluate/pep0484.py @@ -22,16 +22,17 @@ import os import re -from parso import ParserSyntaxError +from parso import ParserSyntaxError, parse, split_lines from parso.python import tree +from jedi._compatibility import unicode, force_unicode from jedi.evaluate.cache import evaluator_method_cache from jedi.evaluate import compiled from jedi.evaluate.base_context import NO_CONTEXTS, ContextSet from jedi.evaluate.lazy_context import LazyTreeContext from jedi.evaluate.context import ModuleContext +from jedi.evaluate.helpers import is_string from jedi import debug -from jedi import _compatibility from jedi import parser_utils @@ -41,17 +42,23 @@ def _evaluate_for_annotation(context, annotation, index=None): If index is not None, the annotation is expected to be a tuple and we're interested in that index """ - if annotation is not None: - context_set = context.eval_node(_fix_forward_reference(context, annotation)) - if index is not None: - context_set = context_set.filter( - lambda context: context.array_type == 'tuple' \ - and len(list(context.py__iter__())) >= index - ).py__getitem__(index) - return context_set.execute_evaluated() - else: + context_set = context.eval_node(_fix_forward_reference(context, annotation)) + return context_set.execute_evaluated() + + +def _evaluate_annotation_string(context, string, index=None): + node = _get_forward_reference_node(context, string) + if node is None: return NO_CONTEXTS + context_set = context.eval_node(node) + if index is not None: + context_set = context_set.filter( + lambda context: context.array_type == u'tuple' + and len(list(context.py__iter__())) >= index + ).py__getitem__(index) + return context_set.execute_evaluated() + def _fix_forward_reference(context, node): evaled_nodes = context.eval_node(node) @@ -59,30 +66,111 @@ def _fix_forward_reference(context, node): debug.warning("Eval'ed typing index %s should lead to 1 object, " " not %s" % (node, evaled_nodes)) return node - evaled_node = list(evaled_nodes)[0] - if isinstance(evaled_node, compiled.CompiledObject) and \ - isinstance(evaled_node.obj, str): - try: - new_node = context.evaluator.grammar.parse( - _compatibility.unicode(evaled_node.obj), - start_symbol='eval_input', - error_recovery=False - ) - except ParserSyntaxError: - debug.warning('Annotation not parsed: %s' % evaled_node.obj) - return node - else: - module = node.get_root_node() - parser_utils.move(new_node, module.end_pos[0]) - new_node.parent = context.tree_node - return new_node + + evaled_context = list(evaled_nodes)[0] + if is_string(evaled_context): + result = _get_forward_reference_node(context, evaled_context.get_safe_value()) + if result is not None: + return result + + return node + + +def _get_forward_reference_node(context, string): + try: + new_node = context.evaluator.grammar.parse( + force_unicode(string), + start_symbol='eval_input', + error_recovery=False + ) + except ParserSyntaxError: + debug.warning('Annotation not parsed: %s' % string) + return None else: - return node + module = context.tree_node.get_root_node() + parser_utils.move(new_node, module.end_pos[0]) + new_node.parent = context.tree_node + return new_node + + +def _split_comment_param_declaration(decl_text): + """ + Split decl_text on commas, but group generic expressions + together. + + For example, given "foo, Bar[baz, biz]" we return + ['foo', 'Bar[baz, biz]']. + + """ + try: + node = parse(decl_text, error_recovery=False).children[0] + except ParserSyntaxError: + debug.warning('Comment annotation is not valid Python: %s' % decl_text) + return [] + + if node.type == 'name': + return [node.get_code().strip()] + + params = [] + try: + children = node.children + except AttributeError: + return [] + else: + for child in children: + if child.type in ['name', 'atom_expr', 'power']: + params.append(child.get_code().strip()) + + return params @evaluator_method_cache() def infer_param(execution_context, param): + """ + Infers the type of a function parameter, using type annotations. + """ annotation = param.annotation + if annotation is None: + # If no Python 3-style annotation, look for a Python 2-style comment + # annotation. + # Identify parameters to function in the same sequence as they would + # appear in a type comment. + all_params = [child for child in param.parent.children + if child.type == 'param'] + + node = param.parent.parent + comment = parser_utils.get_following_comment_same_line(node) + if comment is None: + return NO_CONTEXTS + + match = re.match(r"^#\s*type:\s*\(([^#]*)\)\s*->", comment) + if not match: + return NO_CONTEXTS + params_comments = _split_comment_param_declaration(match.group(1)) + + # Find the specific param being investigated + index = all_params.index(param) + # If the number of parameters doesn't match length of type comment, + # ignore first parameter (assume it's self). + if len(params_comments) != len(all_params): + debug.warning( + "Comments length != Params length %s %s", + params_comments, all_params + ) + from jedi.evaluate.context.instance import BaseInstanceFunctionExecution + if isinstance(execution_context, BaseInstanceFunctionExecution): + if index == 0: + # Assume it's self, which is already handled + return NO_CONTEXTS + index -= 1 + if index >= len(params_comments): + return NO_CONTEXTS + + param_comment = params_comments[index] + return _evaluate_annotation_string( + execution_context.get_root_context(), + param_comment + ) module_context = execution_context.get_root_context() return _evaluate_for_annotation(module_context, annotation) @@ -102,12 +190,33 @@ def py__annotations__(funcdef): @evaluator_method_cache() def infer_return_types(function_context): + """ + Infers the type of a function's return value, + according to type annotations. + """ annotation = py__annotations__(function_context.tree_node).get("return", None) + if annotation is None: + # If there is no Python 3-type annotation, look for a Python 2-type annotation + node = function_context.tree_node + comment = parser_utils.get_following_comment_same_line(node) + if comment is None: + return NO_CONTEXTS + + match = re.match(r"^#\s*type:\s*\([^#]*\)\s*->\s*([^#]*)", comment) + if not match: + return NO_CONTEXTS + + return _evaluate_annotation_string( + function_context.get_root_context(), + match.group(1).strip() + ) + module_context = function_context.get_root_context() return _evaluate_for_annotation(module_context, annotation) _typing_module = None +_typing_module_code_lines = None def _get_typing_replacement_module(grammar): @@ -115,14 +224,15 @@ def _get_typing_replacement_module(grammar): The idea is to return our jedi replacement for the PEP-0484 typing module as discussed at https://github.com/davidhalter/jedi/issues/663 """ - global _typing_module + global _typing_module, _typing_module_code_lines if _typing_module is None: typing_path = \ os.path.abspath(os.path.join(__file__, "../jedi_typing.py")) with open(typing_path) as f: - code = _compatibility.unicode(f.read()) + code = unicode(f.read()) _typing_module = grammar.parse(code) - return _typing_module + _typing_module_code_lines = split_lines(code, keepends=True) + return _typing_module, _typing_module_code_lines def py__getitem__(context, typ, node): @@ -152,10 +262,12 @@ def py__getitem__(context, typ, node): # check for the instance typing._Optional (Python 3.6). return context.eval_node(nodes[0]) + module_node, code_lines = _get_typing_replacement_module(context.evaluator.latest_grammar) typing = ModuleContext( context.evaluator, - module_node=_get_typing_replacement_module(context.evaluator.latest_grammar), - path=None + module_node=module_node, + path=None, + code_lines=code_lines, ) factories = typing.py__getattribute__("factory") assert len(factories) == 1 @@ -167,12 +279,12 @@ def py__getitem__(context, typ, node): if isinstance(child, tree.Class)) if type_name not in valid_classnames: return None - compiled_classname = compiled.create(context.evaluator, type_name) + compiled_classname = compiled.create_simple_object(context.evaluator, type_name) from jedi.evaluate.context.iterable import FakeSequence args = FakeSequence( context.evaluator, - "tuple", + u'tuple', [LazyTreeContext(context, n) for n in nodes] ) @@ -213,10 +325,6 @@ def _find_type_from_comment_hint(context, node, varlist, name): if comment is None: return [] match = re.match(r"^#\s*type:\s*([^#]*)", comment) - if not match: + if match is None: return [] - annotation = tree.String( - repr(str(match.group(1).strip())), - node.start_pos) - annotation.parent = node.parent - return _evaluate_for_annotation(context, annotation, index) + return _evaluate_annotation_string(context, match.group(1).strip(), index) diff --git a/pythonFiles/jedi/evaluate/project.py b/pythonFiles/jedi/evaluate/project.py deleted file mode 100644 index b90f0f0c3be8..000000000000 --- a/pythonFiles/jedi/evaluate/project.py +++ /dev/null @@ -1,40 +0,0 @@ -import os -import sys - -from jedi.evaluate.sys_path import get_venv_path, detect_additional_paths -from jedi.cache import underscore_memoization - - -class Project(object): - def __init__(self, sys_path=None): - if sys_path is not None: - self._sys_path = sys_path - - venv = os.getenv('VIRTUAL_ENV') - if venv: - sys_path = get_venv_path(venv) - - if sys_path is None: - sys_path = sys.path - - base_sys_path = list(sys_path) - try: - base_sys_path.remove('') - except ValueError: - pass - - self._base_sys_path = base_sys_path - - def add_script_path(self, script_path): - self._script_path = script_path - - def add_evaluator(self, evaluator): - self._evaluator = evaluator - - @property - @underscore_memoization - def sys_path(self): - if self._script_path is None: - return self._base_sys_path - - return self._base_sys_path + detect_additional_paths(self._evaluator, self._script_path) diff --git a/pythonFiles/jedi/evaluate/recursion.py b/pythonFiles/jedi/evaluate/recursion.py index e2f34a4a9a06..5be3f8be4d29 100644 --- a/pythonFiles/jedi/evaluate/recursion.py +++ b/pythonFiles/jedi/evaluate/recursion.py @@ -49,6 +49,7 @@ A function may not be executed more than this number of times recursively. """ + class RecursionDetector(object): def __init__(self): self.pushed_nodes = [] @@ -67,9 +68,11 @@ def execution_allowed(evaluator, node): node.start_pos) yield False else: - pushed_nodes.append(node) - yield True - pushed_nodes.pop() + try: + pushed_nodes.append(node) + yield True + finally: + pushed_nodes.pop() def execution_recursion_decorator(default=NO_CONTEXTS): @@ -113,7 +116,7 @@ def push_execution(self, execution): self._parent_execution_funcs.append(funcdef) module = execution.get_root_context() - if module == self._evaluator.BUILTINS: + if module == self._evaluator.builtins_module: # We have control over builtins so we know they are not recursing # like crazy. Therefore we just let them execute always, because # they usually just help a lot with getting good results. diff --git a/pythonFiles/jedi/evaluate/site.py b/pythonFiles/jedi/evaluate/site.py deleted file mode 100644 index bf884faefaaf..000000000000 --- a/pythonFiles/jedi/evaluate/site.py +++ /dev/null @@ -1,110 +0,0 @@ -"""An adapted copy of relevant site-packages functionality from Python stdlib. - -This file contains some functions related to handling site-packages in Python -with jedi-specific modifications: - -- the functions operate on sys_path argument rather than global sys.path - -- in .pth files "import ..." lines that allow execution of arbitrary code are - skipped to prevent code injection into jedi interpreter - -""" - -# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -# 2011, 2012, 2013, 2014, 2015 Python Software Foundation; All Rights Reserved - -from __future__ import print_function - -import sys -import os - - -def makepath(*paths): - dir = os.path.join(*paths) - try: - dir = os.path.abspath(dir) - except OSError: - pass - return dir, os.path.normcase(dir) - - -def _init_pathinfo(sys_path): - """Return a set containing all existing directory entries from sys_path""" - d = set() - for dir in sys_path: - try: - if os.path.isdir(dir): - dir, dircase = makepath(dir) - d.add(dircase) - except TypeError: - continue - return d - - -def addpackage(sys_path, sitedir, name, known_paths): - """Process a .pth file within the site-packages directory: - For each line in the file, either combine it with sitedir to a path - and add that to known_paths, or execute it if it starts with 'import '. - """ - if known_paths is None: - known_paths = _init_pathinfo(sys_path) - reset = 1 - else: - reset = 0 - fullname = os.path.join(sitedir, name) - try: - f = open(fullname, "r") - except OSError: - return - with f: - for n, line in enumerate(f): - if line.startswith("#"): - continue - try: - if line.startswith(("import ", "import\t")): - # Change by immerrr: don't evaluate import lines to prevent - # code injection into jedi through pth files. - # - # exec(line) - continue - line = line.rstrip() - dir, dircase = makepath(sitedir, line) - if not dircase in known_paths and os.path.exists(dir): - sys_path.append(dir) - known_paths.add(dircase) - except Exception: - print("Error processing line {:d} of {}:\n".format(n+1, fullname), - file=sys.stderr) - import traceback - for record in traceback.format_exception(*sys.exc_info()): - for line in record.splitlines(): - print(' '+line, file=sys.stderr) - print("\nRemainder of file ignored", file=sys.stderr) - break - if reset: - known_paths = None - return known_paths - - -def addsitedir(sys_path, sitedir, known_paths=None): - """Add 'sitedir' argument to sys_path if missing and handle .pth files in - 'sitedir'""" - if known_paths is None: - known_paths = _init_pathinfo(sys_path) - reset = 1 - else: - reset = 0 - sitedir, sitedircase = makepath(sitedir) - if not sitedircase in known_paths: - sys_path.append(sitedir) # Add path component - known_paths.add(sitedircase) - try: - names = os.listdir(sitedir) - except OSError: - return - names = [name for name in names if name.endswith(".pth")] - for name in sorted(names): - addpackage(sys_path, sitedir, name, known_paths) - if reset: - known_paths = None - return known_paths diff --git a/pythonFiles/jedi/evaluate/stdlib.py b/pythonFiles/jedi/evaluate/stdlib.py index 259d692733db..06296d938183 100644 --- a/pythonFiles/jedi/evaluate/stdlib.py +++ b/pythonFiles/jedi/evaluate/stdlib.py @@ -9,9 +9,11 @@ the standard library. The usual way to understand the standard library is the compiled module that returns the types for C-builtins. """ -import collections import re +import parso + +from jedi._compatibility import force_unicode from jedi import debug from jedi.evaluate.arguments import ValuesArguments from jedi.evaluate import analysis @@ -57,7 +59,7 @@ def execute(evaluator, obj, arguments): except AttributeError: pass else: - if obj.parent_context == evaluator.BUILTINS: + if obj.parent_context == evaluator.builtins_module: module_name = 'builtins' elif isinstance(obj.parent_context, ModuleContext): module_name = obj.parent_context.name.string_name @@ -107,10 +109,11 @@ def argument_clinic(string, want_obj=False, want_context=False, want_arguments=F def f(func): def wrapper(evaluator, obj, arguments): debug.dbg('builtin start %s' % obj, color='MAGENTA') + result = NO_CONTEXTS try: lst = list(arguments.eval_argument_clinic(clinic_args)) except ValueError: - return NO_CONTEXTS + pass else: kwargs = {} if want_context: @@ -119,9 +122,10 @@ def wrapper(evaluator, obj, arguments): kwargs['obj'] = obj if want_arguments: kwargs['arguments'] = arguments - return func(evaluator, *lst, **kwargs) + result = func(evaluator, *lst, **kwargs) finally: - debug.dbg('builtin end', color='MAGENTA') + debug.dbg('builtin end: %s', result, color='MAGENTA') + return result return wrapper return f @@ -133,7 +137,7 @@ def builtins_next(evaluator, iterators, defaults): TODO this function is currently not used. It's a stab at implementing next in a different way than fake objects. This would be a bit more flexible. """ - if evaluator.python_version[0] == 2: + if evaluator.environment.version_info.major == 2: name = 'next' else: name = '__next__' @@ -157,7 +161,7 @@ def builtins_getattr(evaluator, objects, names, defaults=None): for obj in objects: for name in names: if is_string(name): - return obj.py__getattribute__(name.obj) + return obj.py__getattribute__(force_unicode(name.get_safe_value())) else: debug.warning('getattr called without str') continue @@ -207,22 +211,24 @@ def builtins_reversed(evaluator, sequences, obj, arguments): # necessary, because `reversed` is a function and autocompletion # would fail in certain cases like `reversed(x).__iter__` if we # just returned the result directly. - seq = iterable.FakeSequence(evaluator, 'list', rev) + seq = iterable.FakeSequence(evaluator, u'list', rev) arguments = ValuesArguments([ContextSet(seq)]) - return ContextSet(CompiledInstance(evaluator, evaluator.BUILTINS, obj, arguments)) + return ContextSet(CompiledInstance(evaluator, evaluator.builtins_module, obj, arguments)) @argument_clinic('obj, type, /', want_arguments=True) def builtins_isinstance(evaluator, objects, types, arguments): bool_results = set() for o in objects: + cls = o.py__class__() try: - mro_func = o.py__class__().py__mro__ + mro_func = cls.py__mro__ except AttributeError: # This is temporary. Everything should have a class attribute in # Python?! Maybe we'll leave it here, because some numpy objects or # whatever might not. - return ContextSet(compiled.create(True), compiled.create(False)) + bool_results = set([True, False]) + break mro = mro_func() @@ -230,7 +236,7 @@ def builtins_isinstance(evaluator, objects, types, arguments): if cls_or_tup.is_class(): bool_results.add(cls_or_tup in mro) elif cls_or_tup.name.string_name == 'tuple' \ - and cls_or_tup.get_root_context() == evaluator.BUILTINS: + and cls_or_tup.get_root_context() == evaluator.builtins_module: # Check for tuples. classes = ContextSet.from_sets( lazy_context.infer() @@ -246,7 +252,10 @@ def builtins_isinstance(evaluator, objects, types, arguments): 'not %s.' % cls_or_tup analysis.add(lazy_context._context, 'type-error-isinstance', node, message) - return ContextSet.from_iterable(compiled.create(evaluator, x) for x in bool_results) + return ContextSet.from_iterable( + compiled.builtin_from_name(evaluator, force_unicode(str(b))) + for b in bool_results + ) def collections_namedtuple(evaluator, obj, arguments): @@ -256,45 +265,54 @@ def collections_namedtuple(evaluator, obj, arguments): This has to be done by processing the namedtuple class template and evaluating the result. - .. note:: |jedi| only supports namedtuples on Python >2.6. - """ - # Namedtuples are not supported on Python 2.6 - if not hasattr(collections, '_class_template'): + collections_context = obj.parent_context + _class_template_set = collections_context.py__getattribute__(u'_class_template') + if not _class_template_set: + # Namedtuples are not supported on Python 2.6, early 2.7, because the + # _class_template variable is not defined, there. return NO_CONTEXTS # Process arguments # TODO here we only use one of the types, we should use all. - name = list(_follow_param(evaluator, arguments, 0))[0].obj + # TODO this is buggy, doesn't need to be a string + name = list(_follow_param(evaluator, arguments, 0))[0].get_safe_value() _fields = list(_follow_param(evaluator, arguments, 1))[0] if isinstance(_fields, compiled.CompiledObject): - fields = _fields.obj.replace(',', ' ').split() - elif isinstance(_fields, iterable.AbstractIterable): + fields = _fields.get_safe_value().replace(',', ' ').split() + elif isinstance(_fields, iterable.Sequence): fields = [ - v.obj + v.get_safe_value() for lazy_context in _fields.py__iter__() - for v in lazy_context.infer() if hasattr(v, 'obj') + for v in lazy_context.infer() if is_string(v) ] else: return NO_CONTEXTS - base = collections._class_template + def get_var(name): + x, = collections_context.py__getattribute__(name) + return x.get_safe_value() + + base = next(iter(_class_template_set)).get_safe_value() base += _NAMEDTUPLE_INIT - # Build source - source = base.format( + # Build source code + code = base.format( typename=name, field_names=tuple(fields), num_fields=len(fields), - arg_list = repr(tuple(fields)).replace("'", "")[1:-1], - repr_fmt=', '.join(collections._repr_template.format(name=name) for name in fields), - field_defs='\n'.join(collections._field_template.format(index=index, name=name) + arg_list=repr(tuple(fields)).replace("u'", "").replace("'", "")[1:-1], + repr_fmt=', '.join(get_var(u'_repr_template').format(name=name) for name in fields), + field_defs='\n'.join(get_var(u'_field_template').format(index=index, name=name) for index, name in enumerate(fields)) ) - # Parse source - module = evaluator.grammar.parse(source) + # Parse source code + module = evaluator.grammar.parse(code) generated_class = next(module.iter_classdefs()) - parent_context = ModuleContext(evaluator, module, '') + parent_context = ModuleContext( + evaluator, module, None, + code_lines=parso.split_lines(code, keepends=True), + ) return ContextSet(ClassContext(evaluator, parent_context, generated_class)) diff --git a/pythonFiles/jedi/evaluate/syntax_tree.py b/pythonFiles/jedi/evaluate/syntax_tree.py index 1d847a4960de..4efe845d476d 100644 --- a/pythonFiles/jedi/evaluate/syntax_tree.py +++ b/pythonFiles/jedi/evaluate/syntax_tree.py @@ -2,10 +2,10 @@ Functions evaluating the syntax tree. """ import copy -import operator as op from parso.python import tree +from jedi._compatibility import force_unicode, unicode from jedi import debug from jedi import parser_utils from jedi.evaluate.base_context import ContextSet, NO_CONTEXTS, ContextualizedNode, \ @@ -17,11 +17,13 @@ from jedi.evaluate import analysis from jedi.evaluate import imports from jedi.evaluate import arguments +from jedi.evaluate.pep0484 import _evaluate_for_annotation from jedi.evaluate.context import ClassContext, FunctionContext from jedi.evaluate.context import iterable from jedi.evaluate.context import TreeInstance, CompiledInstance from jedi.evaluate.finder import NameFinder from jedi.evaluate.helpers import is_string, is_literal, is_number, is_compiled +from jedi.evaluate.compiled.access import COMPARISON_OPERATORS def _limit_context_infers(func): @@ -48,13 +50,25 @@ def wrapper(context, *args, **kwargs): return wrapper +def _py__stop_iteration_returns(generators): + results = ContextSet() + for generator in generators: + try: + method = generator.py__stop_iteration_returns + except AttributeError: + debug.warning('%s is not actually a generator', generator) + else: + results |= method() + return results + + @debug.increase_indent @_limit_context_infers def eval_node(context, element): - debug.dbg('eval_element %s@%s', element, element.start_pos) + debug.dbg('eval_node %s@%s', element, element.start_pos) evaluator = context.evaluator typ = element.type - if typ in ('name', 'number', 'string', 'atom'): + if typ in ('name', 'number', 'string', 'atom', 'strings'): return eval_atom(context, element) elif typ == 'keyword': # For False/True/None @@ -68,22 +82,33 @@ def eval_node(context, element): return eval_expr_stmt(context, element) elif typ in ('power', 'atom_expr'): first_child = element.children[0] - if not (first_child.type == 'keyword' and first_child.value == 'await'): - context_set = eval_atom(context, first_child) - for trailer in element.children[1:]: - if trailer == '**': # has a power operation. - right = evaluator.eval_element(context, element.children[2]) - context_set = _eval_comparison( - evaluator, - context, - context_set, - trailer, - right - ) - break - context_set = eval_trailer(context, context_set, trailer) - return context_set - return NO_CONTEXTS + children = element.children[1:] + had_await = False + if first_child.type == 'keyword' and first_child.value == 'await': + had_await = True + first_child = children.pop(0) + + context_set = eval_atom(context, first_child) + for trailer in children: + if trailer == '**': # has a power operation. + right = context.eval_node(children[1]) + context_set = _eval_comparison( + evaluator, + context, + context_set, + trailer, + right + ) + break + context_set = eval_trailer(context, context_set, trailer) + + if had_await: + await_context_set = context_set.py__getattribute__(u"__await__") + if not await_context_set: + debug.warning('Tried to run py__await__ on context %s', context) + context_set = ContextSet() + return _py__stop_iteration_returns(await_context_set.execute_evaluated()) + return context_set elif typ in ('testlist_star_expr', 'testlist',): # The implicit tuple in statements. return ContextSet(iterable.SequenceLiteralContext(evaluator, context, element)) @@ -100,8 +125,10 @@ def eval_node(context, element): # Must be an ellipsis, other operators are not evaluated. # In Python 2 ellipsis is coded as three single dot tokens, not # as one token 3 dot token. - assert element.value in ('.', '...') - return ContextSet(compiled.create(evaluator, Ellipsis)) + if element.value not in ('.', '...'): + origin = element.parent + raise AssertionError("unhandled operator %s in %s " % (repr(element.value), origin)) + return ContextSet(compiled.builtin_from_name(evaluator, u'Ellipsis')) elif typ == 'dotted_name': context_set = eval_atom(context, element.children[0]) for next_name in element.children[2::2]: @@ -112,6 +139,15 @@ def eval_node(context, element): return eval_node(context, element.children[0]) elif typ == 'annassign': return pep0484._evaluate_for_annotation(context, element.children[1]) + elif typ == 'yield_expr': + if len(element.children) and element.children[1].type == 'yield_arg': + # Implies that it's a yield from. + element = element.children[1].children[1] + generators = context.eval_node(element) + return _py__stop_iteration_returns(generators) + + # Generator.send() is not implemented. + return NO_CONTEXTS else: return eval_or_test(context, element) @@ -119,7 +155,7 @@ def eval_node(context, element): def eval_trailer(context, base_contexts, trailer): trailer_op, node = trailer.children[:2] if node == ')': # `arglist` is optional. - node = () + node = None if trailer_op == '[': trailer_op, node, _ = trailer.children @@ -148,7 +184,7 @@ def eval_trailer(context, base_contexts, trailer): name_or_str=node ) else: - assert trailer_op == '(' + assert trailer_op == '(', 'trailer_op is actually %s' % trailer_op args = arguments.TreeArguments(context.evaluator, context, node, trailer) return base_contexts.execute(args) @@ -173,19 +209,19 @@ def eval_atom(context, atom): ) elif isinstance(atom, tree.Literal): - string = parser_utils.safe_literal_eval(atom.value) - return ContextSet(compiled.create(context.evaluator, string)) + string = context.evaluator.compiled_subprocess.safe_literal_eval(atom.value) + return ContextSet(compiled.create_simple_object(context.evaluator, string)) + elif atom.type == 'strings': + # Will be multiple string. + context_set = eval_atom(context, atom.children[0]) + for string in atom.children[1:]: + right = eval_atom(context, string) + context_set = _eval_comparison(context.evaluator, context, context_set, u'+', right) + return context_set else: c = atom.children - if c[0].type == 'string': - # Will be one string. - context_set = eval_atom(context, c[0]) - for string in c[1:]: - right = eval_atom(context, string) - context_set = _eval_comparison(context.evaluator, context, context_set, '+', right) - return context_set # Parentheses without commas are not tuples. - elif c[0] == '(' and not len(c) == 2 \ + if c[0] == '(' and not len(c) == 2 \ and not(c[1].type == 'testlist_comp' and len(c[1].children) > 1): return context.eval_node(c[1]) @@ -203,7 +239,9 @@ def eval_atom(context, atom): pass if comp_for.type == 'comp_for': - return ContextSet(iterable.Comprehension.from_atom(context.evaluator, context, atom)) + return ContextSet(iterable.comprehension_from_atom( + context.evaluator, context, atom + )) # It's a dict/list/tuple literal. array_node = c[1] @@ -221,7 +259,21 @@ def eval_atom(context, atom): @_limit_context_infers def eval_expr_stmt(context, stmt, seek_name=None): with recursion.execution_allowed(context.evaluator, stmt) as allowed: - if allowed or context.get_root_context() == context.evaluator.BUILTINS: + # Here we allow list/set to recurse under certain conditions. To make + # it possible to resolve stuff like list(set(list(x))), this is + # necessary. + if not allowed and context.get_root_context() == context.evaluator.builtins_module: + try: + instance = context.instance + except AttributeError: + pass + else: + if instance.name.string_name in ('list', 'set'): + c = instance.get_first_non_keyword_argument_contexts() + if instance not in c: + allowed = True + + if allowed: return _eval_expr_stmt(context, stmt, seek_name) return NO_CONTEXTS @@ -286,16 +338,16 @@ def eval_or_test(context, or_test): # handle lazy evaluation of and/or here. if operator in ('and', 'or'): left_bools = set(left.py__bool__() for left in types) - if left_bools == set([True]): + if left_bools == {True}: if operator == 'and': types = context.eval_node(right) - elif left_bools == set([False]): + elif left_bools == {False}: if operator != 'and': types = context.eval_node(right) # Otherwise continue, because of uncertainty. else: types = _eval_comparison(context.evaluator, context, types, operator, - context.eval_node(right)) + context.eval_node(right)) debug.dbg('eval_or_test types %s', types) return types @@ -308,29 +360,16 @@ def eval_factor(context_set, operator): for context in context_set: if operator == '-': if is_number(context): - yield compiled.create(context.evaluator, -context.obj) + yield context.negate() elif operator == 'not': value = context.py__bool__() if value is None: # Uncertainty. return - yield compiled.create(context.evaluator, not value) + yield compiled.create_simple_object(context.evaluator, not value) else: yield context -# Maps Python syntax to the operator module. -COMPARISON_OPERATORS = { - '==': op.eq, - '!=': op.ne, - 'is': op.is_, - 'is not': op.is_not, - '<': op.lt, - '<=': op.le, - '>': op.gt, - '>=': op.ge, -} - - def _literals_to_types(evaluator, result): # Changes literals ('a', 1, 1.0, etc) to its type instances (str(), # int(), float(), etc). @@ -366,49 +405,59 @@ def _eval_comparison(evaluator, context, left_contexts, operator, right_contexts def _is_tuple(context): - return isinstance(context, iterable.AbstractIterable) and context.array_type == 'tuple' + return isinstance(context, iterable.Sequence) and context.array_type == 'tuple' def _is_list(context): - return isinstance(context, iterable.AbstractIterable) and context.array_type == 'list' + return isinstance(context, iterable.Sequence) and context.array_type == 'list' + + +def _bool_to_context(evaluator, bool_): + return compiled.builtin_from_name(evaluator, force_unicode(str(bool_))) def _eval_comparison_part(evaluator, context, left, operator, right): l_is_num = is_number(left) r_is_num = is_number(right) - if operator == '*': + if isinstance(operator, unicode): + str_operator = operator + else: + str_operator = force_unicode(str(operator.value)) + + if str_operator == '*': # for iterables, ignore * operations - if isinstance(left, iterable.AbstractIterable) or is_string(left): + if isinstance(left, iterable.Sequence) or is_string(left): return ContextSet(left) - elif isinstance(right, iterable.AbstractIterable) or is_string(right): + elif isinstance(right, iterable.Sequence) or is_string(right): return ContextSet(right) - elif operator == '+': + elif str_operator == '+': if l_is_num and r_is_num or is_string(left) and is_string(right): - return ContextSet(compiled.create(evaluator, left.obj + right.obj)) + return ContextSet(left.execute_operation(right, str_operator)) elif _is_tuple(left) and _is_tuple(right) or _is_list(left) and _is_list(right): return ContextSet(iterable.MergedArray(evaluator, (left, right))) - elif operator == '-': + elif str_operator == '-': if l_is_num and r_is_num: - return ContextSet(compiled.create(evaluator, left.obj - right.obj)) - elif operator == '%': + return ContextSet(left.execute_operation(right, str_operator)) + elif str_operator == '%': # With strings and numbers the left type typically remains. Except for # `int() % float()`. return ContextSet(left) - elif operator in COMPARISON_OPERATORS: - operation = COMPARISON_OPERATORS[operator] + elif str_operator in COMPARISON_OPERATORS: if is_compiled(left) and is_compiled(right): # Possible, because the return is not an option. Just compare. - left = left.obj - right = right.obj - - try: - result = operation(left, right) - except TypeError: - # Could be True or False. - return ContextSet(compiled.create(evaluator, True), compiled.create(evaluator, False)) + try: + return ContextSet(left.execute_operation(right, str_operator)) + except TypeError: + # Could be True or False. + pass else: - return ContextSet(compiled.create(evaluator, result)) - elif operator == 'in': + if str_operator in ('is', '!=', '==', 'is not'): + operation = COMPARISON_OPERATORS[str_operator] + bool_ = operation(left, right) + return ContextSet(_bool_to_context(evaluator, bool_)) + + return ContextSet(_bool_to_context(evaluator, True), _bool_to_context(evaluator, False)) + elif str_operator == 'in': return NO_CONTEXTS def check(obj): @@ -417,7 +466,7 @@ def check(obj): obj.name.string_name in ('int', 'float') # Static analysis, one is a number, the other one is not. - if operator in ('+', '-') and l_is_num != r_is_num \ + if str_operator in ('+', '-') and l_is_num != r_is_num \ and not (check(left) or check(right)): message = "TypeError: unsupported operand type(s) for +: %s and %s" analysis.add(context, 'type-error-operation', operator, @@ -442,6 +491,22 @@ def _remove_statements(evaluator, context, stmt, name): def tree_name_to_contexts(evaluator, context, tree_name): + + context_set = ContextSet() + module_node = context.get_root_context().tree_node + if module_node is not None: + names = module_node.get_used_names().get(tree_name.value, []) + for name in names: + expr_stmt = name.parent + + correct_scope = parser_utils.get_parent_scope(name) == context.tree_node + + if expr_stmt.type == "expr_stmt" and expr_stmt.children[1].type == "annassign" and correct_scope: + context_set |= _evaluate_for_annotation(context, expr_stmt.children[1].children[1]) + + if context_set: + return context_set + types = [] node = tree_name.get_definition(import_name_always=True) if node is None: @@ -455,7 +520,7 @@ def tree_name_to_contexts(evaluator, context, tree_name): filters = [next(filters)] return finder.find(filters, attribute_lookup=False) elif node.type not in ('import_from', 'import_name'): - raise ValueError("Should not happen.") + raise ValueError("Should not happen. type: %s", node.type) typ = node.type if typ == 'for_stmt': @@ -472,14 +537,18 @@ def tree_name_to_contexts(evaluator, context, tree_name): types = context.predefined_names[node][tree_name.value] except KeyError: cn = ContextualizedNode(context, node.children[3]) - for_types = iterate_contexts(cn.infer(), cn) + for_types = iterate_contexts( + cn.infer(), + contextualized_node=cn, + is_async=node.parent.type == 'async_stmt', + ) c_node = ContextualizedName(context, tree_name) types = check_tuple_assignments(evaluator, c_node, for_types) elif typ == 'expr_stmt': types = _remove_statements(evaluator, context, node, tree_name) elif typ == 'with_stmt': context_managers = context.eval_node(node.get_test_node_from_name(tree_name)) - enter_methods = context_managers.py__getattribute__('__enter__') + enter_methods = context_managers.py__getattribute__(u'__enter__') return enter_methods.execute_evaluated() elif typ in ('import_from', 'import_name'): types = imports.infer_import(context, tree_name) @@ -492,7 +561,7 @@ def tree_name_to_contexts(evaluator, context, tree_name): exceptions = context.eval_node(tree_name.get_previous_sibling().get_previous_sibling()) types = exceptions.execute_evaluated() else: - raise ValueError("Should not happen.") + raise ValueError("Should not happen. type: %s" % typ) return types @@ -583,6 +652,8 @@ def eval_subscript_list(evaluator, context, index): result += [None] * (3 - len(result)) return ContextSet(iterable.Slice(context, *result)) + elif index.type == 'subscriptlist': + return NO_CONTEXTS # No slices return context.eval_node(index) diff --git a/pythonFiles/jedi/evaluate/sys_path.py b/pythonFiles/jedi/evaluate/sys_path.py index 82e5e9df9ceb..d765a6653c02 100644 --- a/pythonFiles/jedi/evaluate/sys_path.py +++ b/pythonFiles/jedi/evaluate/sys_path.py @@ -1,85 +1,27 @@ -import glob import os -import sys -import imp -from jedi.evaluate.site import addsitedir -from jedi._compatibility import unicode +from jedi._compatibility import unicode, force_unicode, all_suffixes from jedi.evaluate.cache import evaluator_method_cache from jedi.evaluate.base_context import ContextualizedNode from jedi.evaluate.helpers import is_string +from jedi.common.utils import traverse_parents +from jedi.parser_utils import get_cached_code_lines from jedi import settings from jedi import debug -from jedi.evaluate.utils import ignored - - -def get_venv_path(venv): - """Get sys.path for specified virtual environment.""" - sys_path = _get_venv_path_dirs(venv) - with ignored(ValueError): - sys_path.remove('') - sys_path = _get_sys_path_with_egglinks(sys_path) - # As of now, get_venv_path_dirs does not scan built-in pythonpath and - # user-local site-packages, let's approximate them using path from Jedi - # interpreter. - return sys_path + sys.path - - -def _get_sys_path_with_egglinks(sys_path): - """Find all paths including those referenced by egg-links. - - Egg-link-referenced directories are inserted into path immediately before - the directory on which their links were found. Such directories are not - taken into consideration by normal import mechanism, but they are traversed - when doing pkg_resources.require. - """ - result = [] - for p in sys_path: - # pkg_resources does not define a specific order for egg-link files - # using os.listdir to enumerate them, we're sorting them to have - # reproducible tests. - for egg_link in sorted(glob.glob(os.path.join(p, '*.egg-link'))): - with open(egg_link) as fd: - for line in fd: - line = line.strip() - if line: - result.append(os.path.join(p, line)) - # pkg_resources package only interprets the first - # non-empty line in egg-link files. - break - result.append(p) - return result - - -def _get_venv_path_dirs(venv): - """Get sys.path for venv without starting up the interpreter.""" - venv = os.path.abspath(venv) - sitedir = _get_venv_sitepackages(venv) - sys_path = [] - addsitedir(sys_path, sitedir) - return sys_path - - -def _get_venv_sitepackages(venv): - if os.name == 'nt': - p = os.path.join(venv, 'lib', 'site-packages') - else: - p = os.path.join(venv, 'lib', 'python%d.%d' % sys.version_info[:2], - 'site-packages') - return p def _abs_path(module_context, path): - module_path = module_context.py__file__() if os.path.isabs(path): return path + module_path = module_context.py__file__() if module_path is None: # In this case we have no idea where we actually are in the file # system. return None base_dir = os.path.dirname(module_path) + path = force_unicode(path) return os.path.abspath(os.path.join(base_dir, path)) @@ -87,7 +29,7 @@ def _paths_from_assignment(module_context, expr_stmt): """ Extracts the assigned strings from an assignment that looks as follows:: - >>> sys.path[0:0] = ['module/path', 'another/module/path'] + sys.path[0:0] = ['module/path', 'another/module/path'] This function is in general pretty tolerant (and therefore 'buggy'). However, it's not a big issue usually to add more paths to Jedi's sys_path, @@ -121,7 +63,7 @@ def _paths_from_assignment(module_context, expr_stmt): for lazy_context in cn.infer().iterate(cn): for context in lazy_context.infer(): if is_string(context): - abs_path = _abs_path(module_context, context.obj) + abs_path = _abs_path(module_context, context.get_safe_value()) if abs_path is not None: yield abs_path @@ -144,7 +86,7 @@ def _paths_from_list_modifications(module_context, trailer1, trailer2): for context in module_context.create_context(arg).eval_node(arg): if is_string(context): - abs_path = _abs_path(module_context, context.obj) + abs_path = _abs_path(module_context, context.get_safe_value()) if abs_path is not None: yield abs_path @@ -187,24 +129,19 @@ def get_sys_path_powers(names): return added -def sys_path_with_modifications(evaluator, module_context): - return evaluator.project.sys_path + check_sys_path_modifications(module_context) - - -def detect_additional_paths(evaluator, script_path): - django_paths = _detect_django_path(script_path) +def discover_buildout_paths(evaluator, script_path): buildout_script_paths = set() for buildout_script_path in _get_buildout_script_paths(script_path): for path in _get_paths_from_buildout_script(evaluator, buildout_script_path): buildout_script_paths.add(path) - return django_paths + list(buildout_script_paths) + return buildout_script_paths def _get_paths_from_buildout_script(evaluator, buildout_script_path): try: - module_node = evaluator.grammar.parse( + module_node = evaluator.parse( path=buildout_script_path, cache=True, cache_path=settings.cache_directory @@ -214,20 +151,14 @@ def _get_paths_from_buildout_script(evaluator, buildout_script_path): return from jedi.evaluate.context import ModuleContext - module = ModuleContext(evaluator, module_node, buildout_script_path) + module = ModuleContext( + evaluator, module_node, buildout_script_path, + code_lines=get_cached_code_lines(evaluator.grammar, buildout_script_path), + ) for path in check_sys_path_modifications(module): yield path -def traverse_parents(path): - while True: - new = os.path.dirname(path) - if new == path: - return - path = new - yield path - - def _get_parent_dir_with_file(path, filename): for parent in traverse_parents(path): if os.path.isfile(os.path.join(parent, filename)): @@ -235,47 +166,34 @@ def _get_parent_dir_with_file(path, filename): return None -def _detect_django_path(module_path): - """ Detects the path of the very well known Django library (if used) """ - result = [] - - for parent in traverse_parents(module_path): - with ignored(IOError): - with open(parent + os.path.sep + 'manage.py'): - debug.dbg('Found django path: %s', module_path) - result.append(parent) - return result - - -def _get_buildout_script_paths(module_path): +def _get_buildout_script_paths(search_path): """ if there is a 'buildout.cfg' file in one of the parent directories of the given module it will return a list of all files in the buildout bin directory that look like python files. - :param module_path: absolute path to the module. - :type module_path: str + :param search_path: absolute path to the module. + :type search_path: str """ - project_root = _get_parent_dir_with_file(module_path, 'buildout.cfg') + project_root = _get_parent_dir_with_file(search_path, 'buildout.cfg') if not project_root: - return [] + return bin_path = os.path.join(project_root, 'bin') if not os.path.exists(bin_path): - return [] - extra_module_paths = [] + return + for filename in os.listdir(bin_path): try: filepath = os.path.join(bin_path, filename) with open(filepath, 'r') as f: firstline = f.readline() if firstline.startswith('#!') and 'python' in firstline: - extra_module_paths.append(filepath) + yield filepath except (UnicodeDecodeError, IOError) as e: - # Probably a binary file; permission error or race cond. because file got deleted - # ignore + # Probably a binary file; permission error or race cond. because + # file got deleted. Ignore it. debug.warning(unicode(e)) continue - return extra_module_paths def dotted_path_in_sys_path(sys_path, module_path): @@ -283,7 +201,7 @@ def dotted_path_in_sys_path(sys_path, module_path): Returns the dotted path inside a sys.path. """ # First remove the suffix. - for suffix, _, _ in imp.get_suffixes(): + for suffix in all_suffixes(): if module_path.endswith(suffix): module_path = module_path[:-len(suffix)] break diff --git a/pythonFiles/jedi/evaluate/utils.py b/pythonFiles/jedi/evaluate/utils.py index 7fc1c246de0d..e00e477441de 100644 --- a/pythonFiles/jedi/evaluate/utils.py +++ b/pythonFiles/jedi/evaluate/utils.py @@ -2,10 +2,19 @@ import sys import contextlib import functools +import re +import os from jedi._compatibility import reraise +_sep = os.path.sep +if os.path.altsep is not None: + _sep += os.path.altsep +_path_re = re.compile('(?:\.[^{0}]+|[{0}]__init__\.py)$'.format(re.escape(_sep))) +del _sep + + def to_list(func): def wrapper(*args, **kwargs): return list(func(*args, **kwargs)) @@ -108,3 +117,38 @@ def indent_block(text, indention=' '): text = text[:-1] lines = text.split('\n') return '\n'.join(map(lambda s: indention + s, lines)) + temp + + +def dotted_from_fs_path(fs_path, sys_path): + """ + Changes `/usr/lib/python3.4/email/utils.py` to `email.utils`. I.e. + compares the path with sys.path and then returns the dotted_path. If the + path is not in the sys.path, just returns None. + """ + if os.path.basename(fs_path).startswith('__init__.'): + # We are calculating the path. __init__ files are not interesting. + fs_path = os.path.dirname(fs_path) + + # prefer + # - UNIX + # /path/to/pythonX.Y/lib-dynload + # /path/to/pythonX.Y/site-packages + # - Windows + # C:\path\to\DLLs + # C:\path\to\Lib\site-packages + # over + # - UNIX + # /path/to/pythonX.Y + # - Windows + # C:\path\to\Lib + path = '' + for s in sys_path: + if (fs_path.startswith(s) and len(path) < len(s)): + path = s + + # - Window + # X:\path\to\lib-dynload/datetime.pyd => datetime + module_path = fs_path[len(path):].lstrip(os.path.sep).lstrip('/') + # - Window + # Replace like X:\path\to\something/foo/bar.py + return _path_re.sub('', module_path).replace(os.path.sep, '.').replace('/', '.') diff --git a/pythonFiles/jedi/parser_utils.py b/pythonFiles/jedi/parser_utils.py index 59c6408ea1c6..e630265314e4 100644 --- a/pythonFiles/jedi/parser_utils.py +++ b/pythonFiles/jedi/parser_utils.py @@ -1,14 +1,15 @@ import textwrap from inspect import cleandoc -from jedi._compatibility import literal_eval, is_py3 from parso.python import tree +from parso.cache import parser_cache -_EXECUTE_NODES = set([ - 'funcdef', 'classdef', 'import_from', 'import_name', 'test', 'or_test', - 'and_test', 'not_test', 'comparison', 'expr', 'xor_expr', 'and_expr', - 'shift_expr', 'arith_expr', 'atom_expr', 'term', 'factor', 'power', 'atom' -]) +from jedi._compatibility import literal_eval, force_unicode + +_EXECUTE_NODES = {'funcdef', 'classdef', 'import_from', 'import_name', 'test', + 'or_test', 'and_test', 'not_test', 'comparison', 'expr', + 'xor_expr', 'and_expr', 'shift_expr', 'arith_expr', + 'atom_expr', 'term', 'factor', 'power', 'atom'} _FLOW_KEYWORDS = ( 'try', 'except', 'finally', 'else', 'if', 'elif', 'with', 'for', 'while' @@ -112,10 +113,7 @@ def clean_scope_docstring(scope_node): cleaned = cleandoc(safe_literal_eval(node.value)) # Since we want the docstr output to be always unicode, just # force it. - if is_py3 or isinstance(cleaned, unicode): - return cleaned - else: - return unicode(cleaned, 'UTF-8', 'replace') + return force_unicode(cleaned) return '' @@ -205,6 +203,9 @@ def get_following_comment_same_line(node): whitespace = node.children[5].get_first_leaf().prefix elif node.type == 'with_stmt': whitespace = node.children[3].get_first_leaf().prefix + elif node.type == 'funcdef': + # actually on the next line + whitespace = node.children[4].get_first_leaf().get_next_leaf().prefix else: whitespace = node.get_last_leaf().get_next_leaf().prefix except AttributeError: @@ -239,3 +240,11 @@ def get_parent_scope(node, include_flows=False): break scope = scope.parent return scope + + +def get_cached_code_lines(grammar, path): + """ + Basically access the cached code lines in parso. This is not the nicest way + to do this, but we avoid splitting all the lines again. + """ + return parser_cache[grammar._hashed][path].lines diff --git a/pythonFiles/jedi/refactoring.py b/pythonFiles/jedi/refactoring.py index ee938427fcee..6c1d74d1bdb8 100644 --- a/pythonFiles/jedi/refactoring.py +++ b/pythonFiles/jedi/refactoring.py @@ -1,11 +1,14 @@ """ +THIS is not in active development, please check +https://github.com/davidhalter/jedi/issues/667 first before editing. + Introduce some basic refactoring functions to |jedi|. This module is still in a very early development stage and needs much testing and improvement. .. warning:: I won't do too much here, but if anyone wants to step in, please do. Refactoring is none of my priorities -It uses the |jedi| `API `_ and supports currently the +It uses the |jedi| `API `_ and supports currently the following functions (sometimes bug-prone): - rename @@ -50,9 +53,8 @@ def diff(self): def rename(script, new_name): """ The `args` / `kwargs` params are the same as in `api.Script`. - :param operation: The refactoring operation to execute. - :type operation: str - :type source: str + :param new_name: The new name of the script. + :param script: The source Script object. :return: list of changed lines/changed files """ return Refactoring(_rename(script.usages(), new_name)) @@ -105,11 +107,12 @@ def extract(script, new_name): user_stmt = script._parser.user_stmt() - # TODO care for multiline extracts + # TODO care for multi-line extracts dct = {} if user_stmt: pos = script._pos line_index = pos[0] - 1 + # Be careful here. 'array_for_pos' does not exist in 'helpers'. arr, index = helpers.array_for_pos(user_stmt, pos) if arr is not None: start_pos = arr[index].start_pos @@ -120,7 +123,7 @@ def extract(script, new_name): start_line = new_lines[start_pos[0] - 1] text = start_line[start_pos[1]:e] for l in range(start_pos[0], end_pos[0] - 1): - text += '\n' + l + text += '\n' + str(l) if e is None: end_line = new_lines[end_pos[0] - 1] text += '\n' + end_line[:end_pos[1]] @@ -140,7 +143,7 @@ def extract(script, new_name): new_lines[start_pos[0] - 1] = start_line new_lines[start_pos[0]:end_pos[0] - 1] = [] - # add parentheses in multiline case + # add parentheses in multi-line case open_brackets = ['(', '[', '{'] close_brackets = [')', ']', '}'] if '\n' in text and not (text[0] in open_brackets and text[-1] == @@ -172,7 +175,7 @@ def inline(script): inlines = sorted(inlines, key=lambda x: (x.module_path, x.line, x.column), reverse=True) expression_list = stmt.expression_list() - # don't allow multiline refactorings for now. + # don't allow multi-line refactorings for now. assert stmt.start_pos[0] == stmt.end_pos[0] index = stmt.start_pos[0] - 1 diff --git a/pythonFiles/jedi/utils.py b/pythonFiles/jedi/utils.py index 177524c50168..0f42e7d55858 100644 --- a/pythonFiles/jedi/utils.py +++ b/pythonFiles/jedi/utils.py @@ -89,12 +89,13 @@ def complete(self, text, state): lines = split_lines(text) position = (len(lines), len(lines[-1])) name = get_on_completion_name( - interpreter._get_module_node(), + interpreter._module_node, lines, position ) before = text[:len(text) - len(name)] completions = interpreter.completions() + logging.debug("REPL completions: %s", completions) except: logging.error("REPL Completion error:\n" + traceback.format_exc()) raise @@ -108,6 +109,11 @@ def complete(self, text, state): return None try: + # Need to import this one as well to make sure it's executed before + # this code. This didn't use to be an issue until 3.3. Starting with + # 3.4 this is different, it always overwrites the completer if it's not + # already imported here. + import rlcompleter import readline except ImportError: print("Jedi: Module readline not available.") diff --git a/pythonFiles/normalizeForInterpreter.py b/pythonFiles/normalizeForInterpreter.py new file mode 100644 index 000000000000..678ce8caec4e --- /dev/null +++ b/pythonFiles/normalizeForInterpreter.py @@ -0,0 +1,122 @@ +import ast +import io +import operator +import os +import sys +import token +import tokenize + + +class Visitor(ast.NodeVisitor): + def __init__(self, lines): + self._lines = lines + self.line_numbers_with_nodes = set() + self.line_numbers_with_statements = [] + + def generic_visit(self, node): + if hasattr(node, 'col_offset') and hasattr(node, 'lineno') and node.col_offset == 0: + self.line_numbers_with_nodes.add(node.lineno) + if isinstance(node, ast.stmt): + self.line_numbers_with_statements.append(node.lineno) + + ast.NodeVisitor.generic_visit(self, node) + + +def _tokenize(source): + """Tokenize Python source code.""" + # Using an undocumented API as the documented one in Python 2.7 does not work as needed + # cross-version. + return tokenize.generate_tokens(io.StringIO(source).readline) + + +def _indent_size(line): + for index, char in enumerate(line): + if not char.isspace(): + return index + + +def _get_global_statement_blocks(source, lines): + """Return a list of all global statement blocks. + + The list comprises of 3-item tuples that contain the starting line number, + ending line number and whether the statement is a single line. + + """ + tree = ast.parse(source) + visitor = Visitor(lines) + visitor.visit(tree) + + statement_ranges = [] + for index, line_number in enumerate(visitor.line_numbers_with_statements): + remaining_line_numbers = visitor.line_numbers_with_statements[index+1:] + end_line_number = len(lines) if len(remaining_line_numbers) == 0 else min(remaining_line_numbers) - 1 + current_statement_is_oneline = line_number == end_line_number + + if len(statement_ranges) == 0: + statement_ranges.append((line_number, end_line_number, current_statement_is_oneline)) + continue + + previous_statement = statement_ranges[-1] + previous_statement_is_oneline = previous_statement[2] + if previous_statement_is_oneline and current_statement_is_oneline: + statement_ranges[-1] = previous_statement[0], end_line_number, True + else: + statement_ranges.append((line_number, end_line_number, current_statement_is_oneline)) + + return statement_ranges + + +def normalize_lines(source): + """Normalize blank lines for sending to the terminal. + + Blank lines within a statement block are removed to prevent the REPL + from thinking the block is finished. Newlines are added to separate + top-level statements so that the REPL does not think there is a syntax + error. + + """ + lines = source.splitlines(False) + # Find out if we have any trailing blank lines + if len(lines[-1].strip()) == 0 or source.endswith('\n'): + trailing_newline = '\n' + else: + trailing_newline = '' + + # Step 1: Remove empty lines. + tokens = _tokenize(source) + newlines_indexes_to_remove = (spos[0] for (toknum, tokval, spos, epos, line) in tokens + if len(line.strip()) == 0 + and token.tok_name[toknum] == 'NL' + and spos[0] == epos[0]) + + for line_number in reversed(list(newlines_indexes_to_remove)): + del lines[line_number-1] + + # Step 2: Add blank lines between each global statement block. + # A consequtive single lines blocks of code will be treated as a single statement, + # just to ensure we do not unnecessarily add too many blank lines. + source = '\n'.join(lines) + tokens = _tokenize(source) + dedent_indexes = (spos[0] for (toknum, tokval, spos, epos, line) in tokens + if toknum == token.DEDENT and _indent_size(line) == 0) + + global_statement_ranges = _get_global_statement_blocks(source, lines) + start_positions = map(operator.itemgetter(0), reversed(global_statement_ranges)) + for line_number in filter(lambda x: x > 1, start_positions): + lines.insert(line_number-1, '') + + sys.stdout.write('\n'.join(lines) + trailing_newline) + sys.stdout.flush() + + +if __name__ == '__main__': + contents = sys.argv[1] + try: + default_encoding = sys.getdefaultencoding() + encoded_contents = contents.encode(default_encoding, 'surrogateescape') + contents = encoded_contents.decode(default_encoding, 'replace') + except (UnicodeError, LookupError): + pass + if isinstance(contents, bytes): + contents = contents.decode('utf8') + normalize_lines(contents) diff --git a/pythonFiles/parso/__init__.py b/pythonFiles/parso/__init__.py index f0a0fc4f5015..9654389dea9f 100644 --- a/pythonFiles/parso/__init__.py +++ b/pythonFiles/parso/__init__.py @@ -43,7 +43,7 @@ from parso.utils import split_lines, python_bytes_to_unicode -__version__ = '0.1.1' +__version__ = '0.2.1' def parse(code=None, **kwargs): diff --git a/pythonFiles/parso/_compatibility.py b/pythonFiles/parso/_compatibility.py index 9ddf23dc6786..db411eebf981 100644 --- a/pythonFiles/parso/_compatibility.py +++ b/pythonFiles/parso/_compatibility.py @@ -36,7 +36,7 @@ def use_metaclass(meta, *bases): def u(string): """Cast to unicode DAMMIT! Written because Python2 repr always implicitly casts to a string, so we - have to cast back to a unicode (and we now that we always deal with valid + have to cast back to a unicode (and we know that we always deal with valid unicode, because we check that in the beginning). """ if py_version >= 30: diff --git a/pythonFiles/parso/grammar.py b/pythonFiles/parso/grammar.py index 2cf26d77fb27..6c13f002f90d 100644 --- a/pythonFiles/parso/grammar.py +++ b/pythonFiles/parso/grammar.py @@ -12,7 +12,6 @@ from parso.python.parser import Parser as PythonParser from parso.python.errors import ErrorFinderConfig from parso.python import pep8 -from parso.python import fstring _loaded_grammars = {} @@ -21,7 +20,7 @@ class Grammar(object): """ :py:func:`parso.load_grammar` returns instances of this class. - Creating custom grammars by calling this is not supported, yet. + Creating custom none-python grammars by calling this is not supported, yet. """ #:param text: A BNF representation of your grammar. _error_normalizer_config = None @@ -73,7 +72,7 @@ def parse(self, code=None, **kwargs): :py:class:`parso.python.tree.Module`. """ if 'start_pos' in kwargs: - raise TypeError("parse() got an unexpected keyworda argument.") + raise TypeError("parse() got an unexpected keyword argument.") return self._parse(code=code, **kwargs) def _parse(self, code=None, error_recovery=True, path=None, @@ -186,7 +185,6 @@ def _get_normalizer_issues(self, node, normalizer_config=None): normalizer.walk(node) return normalizer.issues - def __repr__(self): labels = self._pgen_grammar.number2symbol.values() txt = ' '.join(list(labels)[:3]) + ' ...' @@ -215,46 +213,19 @@ def _tokenize(self, code): return tokenize(code, self.version_info) -class PythonFStringGrammar(Grammar): - _token_namespace = fstring.TokenNamespace - _start_symbol = 'fstring' - - def __init__(self): - super(PythonFStringGrammar, self).__init__( - text=fstring.GRAMMAR, - tokenizer=fstring.tokenize, - parser=fstring.Parser - ) - - def parse(self, code, **kwargs): - return self._parse(code, **kwargs) - - def _parse(self, code, error_recovery=True, start_pos=(1, 0)): - tokens = self._tokenizer(code, start_pos=start_pos) - p = self._parser( - self._pgen_grammar, - error_recovery=error_recovery, - start_symbol=self._start_symbol, - ) - return p.parse(tokens=tokens) - - def parse_leaf(self, leaf, error_recovery=True): - code = leaf._get_payload() - return self.parse(code, error_recovery=True, start_pos=leaf.start_pos) - - def load_grammar(**kwargs): """ Loads a :py:class:`parso.Grammar`. The default version is the current Python version. :param str version: A python version string, e.g. ``version='3.3'``. + :param str path: A path to a grammar file """ - def load_grammar(language='python', version=None): + def load_grammar(language='python', version=None, path=None): if language == 'python': version_info = parse_version_string(version) - file = os.path.join( + file = path or os.path.join( 'python', 'grammar%s%s.txt' % (version_info.major, version_info.minor) ) @@ -273,10 +244,6 @@ def load_grammar(language='python', version=None): except FileNotFoundError: message = "Python version %s is currently not supported." % version raise NotImplementedError(message) - elif language == 'python-f-string': - if version is not None: - raise NotImplementedError("Currently different versions are not supported.") - return PythonFStringGrammar() else: raise NotImplementedError("No support for language %s." % language) diff --git a/pythonFiles/parso/pgen2/pgen.py b/pythonFiles/parso/pgen2/pgen.py index 10ef6ffd1532..a3e39fa5fe74 100644 --- a/pythonFiles/parso/pgen2/pgen.py +++ b/pythonFiles/parso/pgen2/pgen.py @@ -28,6 +28,7 @@ def make_grammar(self): c = grammar.Grammar(self._bnf_text) names = list(self.dfas.keys()) names.sort() + # TODO do we still need this? names.remove(self.startsymbol) names.insert(0, self.startsymbol) for name in names: @@ -316,8 +317,8 @@ def _parse_atom(self): def _expect(self, type): if self.type != type: - self._raise_error("expected %s, got %s(%s)", - type, self.type, self.value) + self._raise_error("expected %s(%s), got %s(%s)", + type, token.tok_name[type], self.type, self.value) value = self.value self._gettoken() return value diff --git a/pythonFiles/parso/python/diff.py b/pythonFiles/parso/python/diff.py index c2e44fd3cb21..f8b73c75d3d5 100644 --- a/pythonFiles/parso/python/diff.py +++ b/pythonFiles/parso/python/diff.py @@ -133,7 +133,7 @@ def update(self, old_lines, new_lines): LOG.debug('diff: line_lengths old: %s, new: %s' % (len(old_lines), line_length)) for operation, i1, i2, j1, j2 in opcodes: - LOG.debug('diff %s old[%s:%s] new[%s:%s]', + LOG.debug('diff code[%s] old[%s:%s] new[%s:%s]', operation, i1 + 1, i2, j1 + 1, j2) if j2 == line_length and new_lines[-1] == '': @@ -454,7 +454,7 @@ def _remove_endmarker(self, tree_nodes): self._last_prefix = '' if is_endmarker: try: - separation = last_leaf.prefix.rindex('\n') + separation = last_leaf.prefix.rindex('\n') + 1 except ValueError: pass else: @@ -462,7 +462,7 @@ def _remove_endmarker(self, tree_nodes): # That is not relevant if parentheses were opened. Always parse # until the end of a line. last_leaf.prefix, self._last_prefix = \ - last_leaf.prefix[:separation + 1], last_leaf.prefix[separation + 1:] + last_leaf.prefix[:separation], last_leaf.prefix[separation:] first_leaf = tree_nodes[0].get_first_leaf() first_leaf.prefix = self.prefix + first_leaf.prefix @@ -472,7 +472,6 @@ def _remove_endmarker(self, tree_nodes): self.prefix = last_leaf.prefix tree_nodes = tree_nodes[:-1] - return tree_nodes def copy_nodes(self, tree_nodes, until_line, line_offset): @@ -491,12 +490,20 @@ def _copy_nodes(self, tos, nodes, until_line, line_offset): new_tos = tos for node in nodes: + if node.start_pos[0] > until_line: + break + if node.type == 'endmarker': + # We basically removed the endmarker, but we are not allowed to + # remove the newline at the end of the line, otherwise it's + # going to be missing. + try: + self.prefix = node.prefix[:node.prefix.rindex('\n') + 1] + except ValueError: + pass # Endmarkers just distort all the checks below. Remove them. break - if node.start_pos[0] > until_line: - break # TODO this check might take a bit of time for large files. We # might want to change this to do more intelligent guessing or # binary search. diff --git a/pythonFiles/parso/python/errors.py b/pythonFiles/parso/python/errors.py index 65296568b54c..cfb8380ea743 100644 --- a/pythonFiles/parso/python/errors.py +++ b/pythonFiles/parso/python/errors.py @@ -563,7 +563,8 @@ def is_issue(self, leaf): and self._normalizer.version == (3, 5): self.add_issue(self.get_node(leaf), message=self.message_async_yield) -@ErrorFinder.register_rule(type='atom') + +@ErrorFinder.register_rule(type='strings') class _BytesAndStringMix(SyntaxRule): # e.g. 's' b'' message = "cannot mix bytes and nonbytes literals" @@ -744,7 +745,12 @@ def is_issue(self, node): @ErrorFinder.register_rule(type='arglist') class _ArglistRule(SyntaxRule): - message = "Generator expression must be parenthesized if not sole argument" + @property + def message(self): + if self._normalizer.version < (3, 7): + return "Generator expression must be parenthesized if not sole argument" + else: + return "Generator expression must be parenthesized" def is_issue(self, node): first_arg = node.children[0] @@ -837,101 +843,36 @@ def is_issue(self, try_stmt): self.add_issue(default_except, message=self.message) -@ErrorFinder.register_rule(type='string') +@ErrorFinder.register_rule(type='fstring') class _FStringRule(SyntaxRule): _fstring_grammar = None - message_empty = "f-string: empty expression not allowed" # f'{}' - message_single_closing = "f-string: single '}' is not allowed" # f'}' message_nested = "f-string: expressions nested too deeply" - message_backslash = "f-string expression part cannot include a backslash" # f'{"\"}' or f'{"\\"}' - message_comment = "f-string expression part cannot include '#'" # f'{#}' - message_unterminated_string = "f-string: unterminated string" # f'{"}' message_conversion = "f-string: invalid conversion character: expected 's', 'r', or 'a'" - message_incomplete = "f-string: expecting '}'" # f'{' - message_syntax = "invalid syntax" - @classmethod - def _load_grammar(cls): - import parso + def _check_format_spec(self, format_spec, depth): + self._check_fstring_contents(format_spec.children[1:], depth) - if cls._fstring_grammar is None: - cls._fstring_grammar = parso.load_grammar(language='python-f-string') - return cls._fstring_grammar + def _check_fstring_expr(self, fstring_expr, depth): + if depth >= 2: + self.add_issue(fstring_expr, message=self.message_nested) - def is_issue(self, fstring): - if 'f' not in fstring.string_prefix.lower(): - return + conversion = fstring_expr.children[2] + if conversion.type == 'fstring_conversion': + name = conversion.children[1] + if name.value not in ('s', 'r', 'a'): + self.add_issue(name, message=self.message_conversion) - parsed = self._load_grammar().parse_leaf(fstring) - for child in parsed.children: - if child.type == 'expression': - self._check_expression(child) - elif child.type == 'error_node': - next_ = child.get_next_leaf() - if next_.type == 'error_leaf' and next_.original_type == 'unterminated_string': - self.add_issue(next_, message=self.message_unterminated_string) - # At this point nothing more is comming except the error - # leaf that we've already checked here. - break - self.add_issue(child, message=self.message_incomplete) - elif child.type == 'error_leaf': - self.add_issue(child, message=self.message_single_closing) - - def _check_python_expr(self, python_expr): - value = python_expr.value - if '\\' in value: - self.add_issue(python_expr, message=self.message_backslash) - return - if '#' in value: - self.add_issue(python_expr, message=self.message_comment) - return - if re.match('\s*$', value) is not None: - self.add_issue(python_expr, message=self.message_empty) - return - - # This is now nested parsing. We parsed the fstring and now - # we're parsing Python again. - try: - # CPython has a bit of a special ways to parse Python code within - # f-strings. It wraps the code in brackets to make sure that - # whitespace doesn't make problems (indentation/newlines). - # Just use that algorithm as well here and adapt start positions. - start_pos = python_expr.start_pos - start_pos = start_pos[0], start_pos[1] - 1 - eval_input = self._normalizer.grammar._parse( - '(%s)' % value, - start_symbol='eval_input', - start_pos=start_pos, - error_recovery=False - ) - except ParserSyntaxError as e: - self.add_issue(e.error_leaf, message=self.message_syntax) - return + format_spec = fstring_expr.children[-2] + if format_spec.type == 'fstring_format_spec': + self._check_format_spec(format_spec, depth + 1) - issues = self._normalizer.grammar.iter_errors(eval_input) - self._normalizer.issues += issues - - def _check_format_spec(self, format_spec): - for expression in format_spec.children[1:]: - nested_format_spec = expression.children[-2] - if nested_format_spec.type == 'format_spec': - if len(nested_format_spec.children) > 1: - self.add_issue( - nested_format_spec.children[1], - message=self.message_nested - ) - - self._check_expression(expression) + def is_issue(self, fstring): + self._check_fstring_contents(fstring.children[1:-1]) - def _check_expression(self, expression): - for c in expression.children: - if c.type == 'python_expr': - self._check_python_expr(c) - elif c.type == 'conversion': - if c.value not in ('s', 'r', 'a'): - self.add_issue(c, message=self.message_conversion) - elif c.type == 'format_spec': - self._check_format_spec(c) + def _check_fstring_contents(self, children, depth=0): + for fstring_content in children: + if fstring_content.type == 'fstring_expr': + self._check_fstring_expr(fstring_content, depth) class _CheckAssignmentRule(SyntaxRule): @@ -944,7 +885,7 @@ def _check_assignment(self, node, is_deletion=False): first, second = node.children[:2] error = _get_comprehension_type(node) if error is None: - if second.type in ('dictorsetmaker', 'string'): + if second.type == 'dictorsetmaker': error = 'literal' elif first in ('(', '['): if second.type == 'yield_expr': @@ -963,7 +904,7 @@ def _check_assignment(self, node, is_deletion=False): error = 'Ellipsis' elif type_ == 'comparison': error = 'comparison' - elif type_ in ('string', 'number'): + elif type_ in ('string', 'number', 'strings'): error = 'literal' elif type_ == 'yield_expr': # This one seems to be a slightly different warning in Python. diff --git a/pythonFiles/parso/python/fstring.py b/pythonFiles/parso/python/fstring.py deleted file mode 100644 index a8fe7b452df5..000000000000 --- a/pythonFiles/parso/python/fstring.py +++ /dev/null @@ -1,211 +0,0 @@ -import re - -from itertools import count -from parso.utils import PythonVersionInfo -from parso.utils import split_lines -from parso.python.tokenize import Token -from parso import parser -from parso.tree import TypedLeaf, ErrorNode, ErrorLeaf - -version36 = PythonVersionInfo(3, 6) - - -class TokenNamespace: - _c = count() - LBRACE = next(_c) - RBRACE = next(_c) - ENDMARKER = next(_c) - COLON = next(_c) - CONVERSION = next(_c) - PYTHON_EXPR = next(_c) - EXCLAMATION_MARK = next(_c) - UNTERMINATED_STRING = next(_c) - - token_map = dict((v, k) for k, v in locals().items() if not k.startswith('_')) - - @classmethod - def generate_token_id(cls, string): - if string == '{': - return cls.LBRACE - elif string == '}': - return cls.RBRACE - elif string == '!': - return cls.EXCLAMATION_MARK - elif string == ':': - return cls.COLON - return getattr(cls, string) - - -GRAMMAR = """ -fstring: expression* ENDMARKER -format_spec: ':' expression* -expression: '{' PYTHON_EXPR [ '!' CONVERSION ] [ format_spec ] '}' -""" - -_prefix = r'((?:[^{}]+)*)' -_expr = _prefix + r'(\{|\}|$)' -_in_expr = r'([^{}\[\]:"\'!]*)(.?)' -# There's only one conversion character allowed. But the rules have to be -# checked later anyway, so allow more here. This makes error recovery nicer. -_conversion = r'([^={}:]*)(.?)' - -_compiled_expr = re.compile(_expr) -_compiled_in_expr = re.compile(_in_expr) -_compiled_conversion = re.compile(_conversion) - - -def tokenize(code, start_pos=(1, 0)): - def add_to_pos(string): - lines = split_lines(string) - l = len(lines[-1]) - if len(lines) > 1: - start_pos[0] += len(lines) - 1 - start_pos[1] = l - else: - start_pos[1] += l - - def tok(value, type=None, prefix=''): - if type is None: - type = TokenNamespace.generate_token_id(value) - - add_to_pos(prefix) - token = Token(type, value, tuple(start_pos), prefix) - add_to_pos(value) - return token - - start = 0 - recursion_level = 0 - added_prefix = '' - start_pos = list(start_pos) - while True: - match = _compiled_expr.match(code, start) - prefix = added_prefix + match.group(1) - found = match.group(2) - start = match.end() - if not found: - # We're at the end. - break - - if found == '}': - if recursion_level == 0 and len(code) > start and code[start] == '}': - # This is a }} escape. - added_prefix = prefix + '}}' - start += 1 - continue - - recursion_level = max(0, recursion_level - 1) - yield tok(found, prefix=prefix) - added_prefix = '' - else: - assert found == '{' - if recursion_level == 0 and len(code) > start and code[start] == '{': - # This is a {{ escape. - added_prefix = prefix + '{{' - start += 1 - continue - - recursion_level += 1 - yield tok(found, prefix=prefix) - added_prefix = '' - - expression = '' - squared_count = 0 - curly_count = 0 - while True: - expr_match = _compiled_in_expr.match(code, start) - expression += expr_match.group(1) - found = expr_match.group(2) - start = expr_match.end() - - if found == '{': - curly_count += 1 - expression += found - elif found == '}' and curly_count > 0: - curly_count -= 1 - expression += found - elif found == '[': - squared_count += 1 - expression += found - elif found == ']': - # Use a max function here, because the Python code might - # just have syntax errors. - squared_count = max(0, squared_count - 1) - expression += found - elif found == ':' and (squared_count or curly_count): - expression += found - elif found in ('"', "'"): - search = found - if len(code) > start + 1 and \ - code[start] == found == code[start+1]: - search *= 3 - start += 2 - - index = code.find(search, start) - if index == -1: - yield tok(expression, type=TokenNamespace.PYTHON_EXPR) - yield tok( - found + code[start:], - type=TokenNamespace.UNTERMINATED_STRING, - ) - start = len(code) - break - expression += found + code[start:index+1] - start = index + 1 - elif found == '!' and len(code) > start and code[start] == '=': - # This is a python `!=` and not a conversion. - expression += found - else: - yield tok(expression, type=TokenNamespace.PYTHON_EXPR) - if found: - yield tok(found) - break - - if found == '!': - conversion_match = _compiled_conversion.match(code, start) - found = conversion_match.group(2) - start = conversion_match.end() - yield tok(conversion_match.group(1), type=TokenNamespace.CONVERSION) - if found: - yield tok(found) - if found == '}': - recursion_level -= 1 - - # We don't need to handle everything after ':', because that is - # basically new tokens. - - yield tok('', type=TokenNamespace.ENDMARKER, prefix=prefix) - - -class Parser(parser.BaseParser): - def parse(self, tokens): - node = super(Parser, self).parse(tokens) - if isinstance(node, self.default_leaf): # Is an endmarker. - # If there's no curly braces we get back a non-module. We always - # want an fstring. - node = self.default_node('fstring', [node]) - - return node - - def convert_leaf(self, pgen_grammar, type, value, prefix, start_pos): - # TODO this is so ugly. - leaf_type = TokenNamespace.token_map[type].lower() - return TypedLeaf(leaf_type, value, start_pos, prefix) - - def error_recovery(self, pgen_grammar, stack, arcs, typ, value, start_pos, prefix, - add_token_callback): - if not self._error_recovery: - return super(Parser, self).error_recovery( - pgen_grammar, stack, arcs, typ, value, start_pos, prefix, - add_token_callback - ) - - token_type = TokenNamespace.token_map[typ].lower() - if len(stack) == 1: - error_leaf = ErrorLeaf(token_type, value, start_pos, prefix) - stack[0][2][1].append(error_leaf) - else: - dfa, state, (type_, nodes) = stack[1] - stack[0][2][1].append(ErrorNode(nodes)) - stack[1:] = [] - - add_token_callback(typ, value, start_pos, prefix) diff --git a/pythonFiles/parso/python/grammar26.txt b/pythonFiles/parso/python/grammar26.txt index b972a41d6a4a..d9cede2e9da9 100644 --- a/pythonFiles/parso/python/grammar26.txt +++ b/pythonFiles/parso/python/grammar26.txt @@ -119,7 +119,8 @@ atom: ('(' [yield_expr|testlist_comp] ')' | '[' [listmaker] ']' | '{' [dictorsetmaker] '}' | '`' testlist1 '`' | - NAME | NUMBER | STRING+) + NAME | NUMBER | strings) +strings: STRING+ listmaker: test ( list_for | (',' test)* [','] ) # Dave: Renamed testlist_gexpr to testlist_comp, because in 2.7+ this is the # default. It's more consistent like this. diff --git a/pythonFiles/parso/python/grammar27.txt b/pythonFiles/parso/python/grammar27.txt index 4c3f33da32d5..359f12b43e1f 100644 --- a/pythonFiles/parso/python/grammar27.txt +++ b/pythonFiles/parso/python/grammar27.txt @@ -104,7 +104,8 @@ atom: ('(' [yield_expr|testlist_comp] ')' | '[' [listmaker] ']' | '{' [dictorsetmaker] '}' | '`' testlist1 '`' | - NAME | NUMBER | STRING+) + NAME | NUMBER | strings) +strings: STRING+ listmaker: test ( list_for | (',' test)* [','] ) testlist_comp: test ( comp_for | (',' test)* [','] ) lambdef: 'lambda' [varargslist] ':' test diff --git a/pythonFiles/parso/python/grammar33.txt b/pythonFiles/parso/python/grammar33.txt index d7aaffd60e14..3a5580926797 100644 --- a/pythonFiles/parso/python/grammar33.txt +++ b/pythonFiles/parso/python/grammar33.txt @@ -103,7 +103,8 @@ power: atom trailer* ['**' factor] atom: ('(' [yield_expr|testlist_comp] ')' | '[' [testlist_comp] ']' | '{' [dictorsetmaker] '}' | - NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False') + NAME | NUMBER | strings | '...' | 'None' | 'True' | 'False') +strings: STRING+ testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] ) trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME subscriptlist: subscript (',' subscript)* [','] diff --git a/pythonFiles/parso/python/grammar34.txt b/pythonFiles/parso/python/grammar34.txt index 05c3181627db..324bba18753d 100644 --- a/pythonFiles/parso/python/grammar34.txt +++ b/pythonFiles/parso/python/grammar34.txt @@ -103,7 +103,8 @@ power: atom trailer* ['**' factor] atom: ('(' [yield_expr|testlist_comp] ')' | '[' [testlist_comp] ']' | '{' [dictorsetmaker] '}' | - NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False') + NAME | NUMBER | strings | '...' | 'None' | 'True' | 'False') +strings: STRING+ testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] ) trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME subscriptlist: subscript (',' subscript)* [','] diff --git a/pythonFiles/parso/python/grammar35.txt b/pythonFiles/parso/python/grammar35.txt index c38217f3f97f..5868b8f7031a 100644 --- a/pythonFiles/parso/python/grammar35.txt +++ b/pythonFiles/parso/python/grammar35.txt @@ -110,7 +110,8 @@ atom_expr: ['await'] atom trailer* atom: ('(' [yield_expr|testlist_comp] ')' | '[' [testlist_comp] ']' | '{' [dictorsetmaker] '}' | - NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False') + NAME | NUMBER | strings | '...' | 'None' | 'True' | 'False') +strings: STRING+ testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] ) trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME subscriptlist: subscript (',' subscript)* [','] diff --git a/pythonFiles/parso/python/grammar36.txt b/pythonFiles/parso/python/grammar36.txt index e76147e9e4fc..b82c1fec1145 100644 --- a/pythonFiles/parso/python/grammar36.txt +++ b/pythonFiles/parso/python/grammar36.txt @@ -108,7 +108,7 @@ atom_expr: ['await'] atom trailer* atom: ('(' [yield_expr|testlist_comp] ')' | '[' [testlist_comp] ']' | '{' [dictorsetmaker] '}' | - NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False') + NAME | NUMBER | strings | '...' | 'None' | 'True' | 'False') testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] ) trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME subscriptlist: subscript (',' subscript)* [','] @@ -148,3 +148,10 @@ encoding_decl: NAME yield_expr: 'yield' [yield_arg] yield_arg: 'from' test | testlist + +strings: (STRING | fstring)+ +fstring: FSTRING_START fstring_content* FSTRING_END +fstring_content: FSTRING_STRING | fstring_expr +fstring_conversion: '!' NAME +fstring_expr: '{' testlist_comp [ fstring_conversion ] [ fstring_format_spec ] '}' +fstring_format_spec: ':' fstring_content* diff --git a/pythonFiles/parso/python/grammar37.txt b/pythonFiles/parso/python/grammar37.txt index e76147e9e4fc..7d112f79852b 100644 --- a/pythonFiles/parso/python/grammar37.txt +++ b/pythonFiles/parso/python/grammar37.txt @@ -108,7 +108,7 @@ atom_expr: ['await'] atom trailer* atom: ('(' [yield_expr|testlist_comp] ')' | '[' [testlist_comp] ']' | '{' [dictorsetmaker] '}' | - NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False') + NAME | NUMBER | strings | '...' | 'None' | 'True' | 'False') testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] ) trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME subscriptlist: subscript (',' subscript)* [','] @@ -148,3 +148,10 @@ encoding_decl: NAME yield_expr: 'yield' [yield_arg] yield_arg: 'from' test | testlist + +strings: (STRING | fstring)+ +fstring: FSTRING_START fstring_content* FSTRING_END +fstring_content: FSTRING_STRING | fstring_expr +fstring_conversion: '!' NAME +fstring_expr: '{' testlist [ fstring_conversion ] [ fstring_format_spec ] '}' +fstring_format_spec: ':' fstring_content* diff --git a/pythonFiles/parso/python/issue_list.txt b/pythonFiles/parso/python/issue_list.txt deleted file mode 100644 index e5e2c9dda764..000000000000 --- a/pythonFiles/parso/python/issue_list.txt +++ /dev/null @@ -1,176 +0,0 @@ -A list of syntax/indentation errors I've encountered in CPython. - -# Python/compile.c - "'continue' not properly in loop" - "'continue' not supported inside 'finally' clause" # Until loop - "default 'except:' must be last" - "from __future__ imports must occur at the beginning of the file" - "'return' outside function" - "'return' with value in async generator" - "'break' outside loop" - "two starred expressions in assignment" - "asynchronous comprehension outside of an asynchronous function" - "'yield' outside function" # For both yield and yield from - "'yield from' inside async function" - "'await' outside function" - "'await' outside async function" - "starred assignment target must be in a list or tuple" - "can't use starred expression here" - "too many statically nested blocks" # Max. 20 - # This is one of the few places in the cpython code base that I really - # don't understand. It feels a bit hacky if you look at the implementation - # of UNPACK_EX. - "too many expressions in star-unpacking assignment" - - # Just ignore this one, newer versions will not be affected anymore and - # it's a limit of 2^16 - 1. - "too many annotations" # Only python 3.0 - 3.5, 3.6 is not affected. - -# Python/ast.c - # used with_item exprlist expr_stmt - "can't %s %s" % ("assign to" or "delete", - "lambda" - "function call" # foo() - "generator expression" - "list comprehension" - "set comprehension" - "dict comprehension" - "keyword" - "Ellipsis" - "comparison" - Dict: Set: Num: Str: Bytes: JoinedStr: FormattedValue: - "literal" - BoolOp: BinOp: UnaryOp: - "operator" - Yield: YieldFrom: - "yield expression" - Await: - "await expression" - IfExp: - "conditional expression" - "assignment to keyword" # (keywords + __debug__) # None = 2 - "named arguments must follow bare *" # def foo(*): pass - "non-default argument follows default argument" # def f(x=3, y): pass - "iterable unpacking cannot be used in comprehension" # [*[] for a in [1]] - "dict unpacking cannot be used in dict comprehension" # {**{} for a in [1]} - "Generator expression must be parenthesized if not sole argument" # foo(x for x in [], b) - "positional argument follows keyword argument unpacking" # f(**x, y) >= 3.5 - "positional argument follows keyword argument" # f(x=2, y) >= 3.5 - "iterable argument unpacking follows keyword argument unpacking" # foo(**kwargs, *args) - "lambda cannot contain assignment" # f(lambda: 1=1) - "keyword can't be an expression" # f(+x=1) - "keyword argument repeated" # f(x=1, x=2) - "illegal expression for augmented assignment" # x, y += 1 - "only single target (not list) can be annotated" # [x, y]: int - "only single target (not tuple) can be annotated" # x, y: str - "illegal target for annotation" # True: 1` - "trailing comma not allowed without surrounding parentheses" # from foo import a, - "bytes can only contain ASCII literal characters." # b'ä' # prob. only python 3 - "cannot mix bytes and nonbytes literals" # 's' b'' - "assignment to yield expression not possible" # x = yield 1 = 3 - - "f-string: empty expression not allowed" # f'{}' - "f-string: single '}' is not allowed" # f'}' - "f-string: expressions nested too deeply" # f'{1:{5:{3}}}' - "f-string expression part cannot include a backslash" # f'{"\"}' or f'{"\\"}' - "f-string expression part cannot include '#'" # f'{#}' - "f-string: unterminated string" # f'{"}' - "f-string: mismatched '(', '{', or '['" - "f-string: invalid conversion character: expected 's', 'r', or 'a'" # f'{1!b}' - "f-string: unexpected end of string" # Doesn't really happen?! - "f-string: expecting '}'" # f'{' - "(unicode error) unknown error - "(value error) unknown error - "(unicode error) MESSAGE - MESSAGES = { - "\\ at end of string" - "truncated \\xXX escape" - "truncated \\uXXXX escape" - "truncated \\UXXXXXXXX escape" - "illegal Unicode character" # '\Uffffffff' - "malformed \\N character escape" # '\N{}' - "unknown Unicode character name" # '\N{foo}' - } - "(value error) MESSAGE # bytes - MESSAGES = { - "Trailing \\ in string" - "invalid \\x escape at position %d" - } - - "invalid escape sequence \\%c" # Only happens when used in `python -W error` - "unexpected node" # Probably irrelevant - "Unexpected node-type in from-import" # Irrelevant, doesn't happen. - "malformed 'try' statement" # Irrelevant, doesn't happen. - -# Python/symtable.c - "duplicate argument '%U' in function definition" - "name '%U' is assigned to before global declaration" - "name '%U' is assigned to before nonlocal declaration" - "name '%U' is used prior to global declaration" - "name '%U' is used prior to nonlocal declaration" - "annotated name '%U' can't be global" - "annotated name '%U' can't be nonlocal" - "import * only allowed at module level" - - "name '%U' is parameter and global", - "name '%U' is nonlocal and global", - "name '%U' is parameter and nonlocal", - - "nonlocal declaration not allowed at module level"); - "no binding for nonlocal '%U' found", - # RecursionError. Not handled. For all human written code, this is probably - # not an issue. eval("()"*x) with x>=2998 for example fails, but that's - # more than 2000 executions on one line. - "maximum recursion depth exceeded during compilation"); - -# Python/future.c - "not a chance" - "future feature %.100s is not defined" - "from __future__ imports must occur at the beginning of the file" # Also in compile.c - -# Parser/tokenizer.c - # All the following issues seem to be irrelevant for parso, because the - # encoding stuff is done before it reaches the tokenizer. It's already - # unicode at that point. - "encoding problem: %s" - "encoding problem: %s with BOM" - "Non-UTF-8 code starting with '\\x%.2x' in file %U on line %i, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details" - -# Parser/pythonrun.c - E_SYNTAX: "invalid syntax" - E_LINECONT: "unexpected character after line continuation character" - E_IDENTIFIER: "invalid character in identifier" - # Also just use 'invalid syntax'. Happens mostly with stuff like `(`. This - # message doesn't really help the user, because it only appears very - # randomly, e.g. `(or` wouldn't yield this error. - E_EOF: "unexpected EOF while parsing" - # Even in 3.6 this is implemented kind of shaky. Not implemented, I think - # cPython needs to fix this one first. - # e.g. `ast.parse('def x():\n\t if 1:\n \t \tpass')` works :/ - E_TABSPACE: "inconsistent use of tabs and spaces in indentation" - # Ignored, just shown as "invalid syntax". The error has mostly to do with - # numbers like 0b2 everywhere or 1.6_ in Python3.6. - E_TOKEN: "invalid token" - E_EOFS: "EOF while scanning triple-quoted string literal" - E_EOLS: "EOL while scanning string literal" - - # IndentationError - E_DEDENT: "unindent does not match any outer indentation level" - E_TOODEEP: "too many levels of indentation" # 100 levels - E_SYNTAX: "expected an indented block" - "unexpected indent" - # I don't think this actually ever happens. - "unexpected unindent" - - - # Irrelevant for parso for now. - E_OVERFLOW: "expression too long" - E_DECODE: "unknown decode error" - E_BADSINGLE: "multiple statements found while compiling a single statement" - - -Version specific: -Python 3.5: - 'yield' inside async function -Python 3.3/3.4: - can use starred expression only as assignment target diff --git a/pythonFiles/parso/python/parser.py b/pythonFiles/parso/python/parser.py index 1897f53e8d6f..7cdf987ab365 100644 --- a/pythonFiles/parso/python/parser.py +++ b/pythonFiles/parso/python/parser.py @@ -1,6 +1,7 @@ from parso.python import tree from parso.python.token import (DEDENT, INDENT, ENDMARKER, NEWLINE, NUMBER, - STRING, tok_name, NAME) + STRING, tok_name, NAME, FSTRING_STRING, + FSTRING_START, FSTRING_END) from parso.parser import BaseParser from parso.pgen2.parse import token_to_ilabel @@ -50,6 +51,17 @@ class structure of different scopes. } default_node = tree.PythonNode + # Names/Keywords are handled separately + _leaf_map = { + STRING: tree.String, + NUMBER: tree.Number, + NEWLINE: tree.Newline, + ENDMARKER: tree.EndMarker, + FSTRING_STRING: tree.FStringString, + FSTRING_START: tree.FStringStart, + FSTRING_END: tree.FStringEnd, + } + def __init__(self, pgen_grammar, error_recovery=True, start_symbol='file_input'): super(Parser, self).__init__(pgen_grammar, start_symbol, error_recovery=error_recovery) @@ -121,16 +133,8 @@ def convert_leaf(self, pgen_grammar, type, value, prefix, start_pos): return tree.Keyword(value, start_pos, prefix) else: return tree.Name(value, start_pos, prefix) - elif type == STRING: - return tree.String(value, start_pos, prefix) - elif type == NUMBER: - return tree.Number(value, start_pos, prefix) - elif type == NEWLINE: - return tree.Newline(value, start_pos, prefix) - elif type == ENDMARKER: - return tree.EndMarker(value, start_pos, prefix) - else: - return tree.Operator(value, start_pos, prefix) + + return self._leaf_map.get(type, tree.Operator)(value, start_pos, prefix) def error_recovery(self, pgen_grammar, stack, arcs, typ, value, start_pos, prefix, add_token_callback): diff --git a/pythonFiles/parso/python/token.py b/pythonFiles/parso/python/token.py index fb590a5f28c6..dd849b01daa7 100644 --- a/pythonFiles/parso/python/token.py +++ b/pythonFiles/parso/python/token.py @@ -32,6 +32,14 @@ ERROR_DEDENT = next(_counter) tok_name[ERROR_DEDENT] = 'ERROR_DEDENT' +FSTRING_START = next(_counter) +tok_name[FSTRING_START] = 'FSTRING_START' +FSTRING_END = next(_counter) +tok_name[FSTRING_END] = 'FSTRING_END' +FSTRING_STRING = next(_counter) +tok_name[FSTRING_STRING] = 'FSTRING_STRING' +EXCLAMATION = next(_counter) +tok_name[EXCLAMATION] = 'EXCLAMATION' # Map from operator to number (since tokenize doesn't do this) @@ -84,6 +92,7 @@ //= DOUBLESLASHEQUAL -> RARROW ... ELLIPSIS +! EXCLAMATION """ opmap = {} diff --git a/pythonFiles/parso/python/tokenize.py b/pythonFiles/parso/python/tokenize.py index ecd2437f5ebb..0ac8a8d5275f 100644 --- a/pythonFiles/parso/python/tokenize.py +++ b/pythonFiles/parso/python/tokenize.py @@ -20,14 +20,16 @@ from parso.python.token import (tok_name, ENDMARKER, STRING, NUMBER, opmap, NAME, ERRORTOKEN, NEWLINE, INDENT, DEDENT, - ERROR_DEDENT) + ERROR_DEDENT, FSTRING_STRING, FSTRING_START, + FSTRING_END) from parso._compatibility import py_version from parso.utils import split_lines TokenCollection = namedtuple( 'TokenCollection', - 'pseudo_token single_quoted triple_quoted endpats always_break_tokens', + 'pseudo_token single_quoted triple_quoted endpats whitespace ' + 'fstring_pattern_map always_break_tokens', ) BOM_UTF8_STRING = BOM_UTF8.decode('utf-8') @@ -52,32 +54,35 @@ def group(*choices, **kwargs): return start + '|'.join(choices) + ')' -def any(*choices): - return group(*choices) + '*' - - def maybe(*choices): return group(*choices) + '?' # Return the empty string, plus all of the valid string prefixes. -def _all_string_prefixes(version_info): +def _all_string_prefixes(version_info, include_fstring=False, only_fstring=False): def different_case_versions(prefix): for s in _itertools.product(*[(c, c.upper()) for c in prefix]): yield ''.join(s) # The valid string prefixes. Only contain the lower case versions, # and don't contain any permuations (include 'fr', but not # 'rf'). The various permutations will be generated. - _valid_string_prefixes = ['b', 'r', 'u'] + valid_string_prefixes = ['b', 'r', 'u'] if version_info >= (3, 0): - _valid_string_prefixes.append('br') + valid_string_prefixes.append('br') - if version_info >= (3, 6): - _valid_string_prefixes += ['f', 'fr'] + result = set(['']) + if version_info >= (3, 6) and include_fstring: + f = ['f', 'fr'] + if only_fstring: + valid_string_prefixes = f + result = set() + else: + valid_string_prefixes += f + elif only_fstring: + return set() # if we add binary f-strings, add: ['fb', 'fbr'] - result = set(['']) - for prefix in _valid_string_prefixes: + for prefix in valid_string_prefixes: for t in _itertools.permutations(prefix): # create a list with upper and lower versions of each # character @@ -102,10 +107,15 @@ def _get_token_collection(version_info): return result +fstring_string_single_line = _compile(r'(?:[^{}\r\n]+|\{\{|\}\})+') +fstring_string_multi_line = _compile(r'(?:[^{}]+|\{\{|\}\})+') + + def _create_token_collection(version_info): # Note: we use unicode matching for names ("\w") but ascii matching for # number literals. Whitespace = r'[ \f\t]*' + whitespace = _compile(Whitespace) Comment = r'#[^\r\n]*' Name = r'\w+' @@ -141,6 +151,9 @@ def _create_token_collection(version_info): # StringPrefix can be the empty string (making it optional). possible_prefixes = _all_string_prefixes(version_info) StringPrefix = group(*possible_prefixes) + StringPrefixWithF = group(*_all_string_prefixes(version_info, include_fstring=True)) + fstring_prefixes = _all_string_prefixes(version_info, include_fstring=True, only_fstring=True) + FStringStart = group(*fstring_prefixes) # Tail end of ' string. Single = r"[^'\\]*(?:\\.[^'\\]*)*'" @@ -150,14 +163,14 @@ def _create_token_collection(version_info): Single3 = r"[^'\\]*(?:(?:\\.|'(?!''))[^'\\]*)*'''" # Tail end of """ string. Double3 = r'[^"\\]*(?:(?:\\.|"(?!""))[^"\\]*)*"""' - Triple = group(StringPrefix + "'''", StringPrefix + '"""') + Triple = group(StringPrefixWithF + "'''", StringPrefixWithF + '"""') # Because of leftmost-then-longest match semantics, be sure to put the # longest operators first (e.g., if = came before ==, == would get # recognized as two instances of =). - Operator = group(r"\*\*=?", r">>=?", r"<<=?", r"!=", + Operator = group(r"\*\*=?", r">>=?", r"<<=?", r"//=?", r"->", - r"[+\-*/%&@`|^=<>]=?", + r"[+\-*/%&@`|^!=<>]=?", r"~") Bracket = '[][(){}]' @@ -174,7 +187,12 @@ def _create_token_collection(version_info): group("'", r'\\\r?\n'), StringPrefix + r'"[^\n"\\]*(?:\\.[^\n"\\]*)*' + group('"', r'\\\r?\n')) - PseudoExtras = group(r'\\\r?\n|\Z', Comment, Triple) + pseudo_extra_pool = [Comment, Triple] + all_quotes = '"', "'", '"""', "'''" + if fstring_prefixes: + pseudo_extra_pool.append(FStringStart + group(*all_quotes)) + + PseudoExtras = group(r'\\\r?\n|\Z', *pseudo_extra_pool) PseudoToken = group(Whitespace, capture=True) + \ group(PseudoExtras, Number, Funny, ContStr, Name, capture=True) @@ -192,18 +210,24 @@ def _create_token_collection(version_info): # including the opening quotes. single_quoted = set() triple_quoted = set() + fstring_pattern_map = {} for t in possible_prefixes: - for p in (t + '"', t + "'"): - single_quoted.add(p) - for p in (t + '"""', t + "'''"): - triple_quoted.add(p) + for quote in '"', "'": + single_quoted.add(t + quote) + + for quote in '"""', "'''": + triple_quoted.add(t + quote) + + for t in fstring_prefixes: + for quote in all_quotes: + fstring_pattern_map[t + quote] = quote ALWAYS_BREAK_TOKENS = (';', 'import', 'class', 'def', 'try', 'except', 'finally', 'while', 'with', 'return') pseudo_token_compiled = _compile(PseudoToken) return TokenCollection( pseudo_token_compiled, single_quoted, triple_quoted, endpats, - ALWAYS_BREAK_TOKENS + whitespace, fstring_pattern_map, ALWAYS_BREAK_TOKENS ) @@ -222,16 +246,108 @@ def _get_type_name(self, exact=True): return tok_name[self.type] def __repr__(self): - return ('TokenInfo(type=%s, string=%r, start=%r, prefix=%r)' % + return ('TokenInfo(type=%s, string=%r, start_pos=%r, prefix=%r)' % self._replace(type=self._get_type_name())) +class FStringNode(object): + def __init__(self, quote): + self.quote = quote + self.parentheses_count = 0 + self.previous_lines = '' + self.last_string_start_pos = None + # In the syntax there can be multiple format_spec's nested: + # {x:{y:3}} + self.format_spec_count = 0 + + def open_parentheses(self, character): + self.parentheses_count += 1 + + def close_parentheses(self, character): + self.parentheses_count -= 1 + + def allow_multiline(self): + return len(self.quote) == 3 + + def is_in_expr(self): + return (self.parentheses_count - self.format_spec_count) > 0 + + +def _check_fstring_ending(fstring_stack, token, from_start=False): + fstring_end = float('inf') + fstring_index = None + for i, node in enumerate(fstring_stack): + if from_start: + if token.startswith(node.quote): + fstring_index = i + fstring_end = len(node.quote) + else: + continue + else: + try: + end = token.index(node.quote) + except ValueError: + pass + else: + if fstring_index is None or end < fstring_end: + fstring_index = i + fstring_end = end + return fstring_index, fstring_end + + +def _find_fstring_string(fstring_stack, line, lnum, pos): + tos = fstring_stack[-1] + if tos.is_in_expr(): + return '', pos + else: + new_pos = pos + allow_multiline = tos.allow_multiline() + if allow_multiline: + match = fstring_string_multi_line.match(line, pos) + else: + match = fstring_string_single_line.match(line, pos) + if match is None: + string = tos.previous_lines + else: + if not tos.previous_lines: + tos.last_string_start_pos = (lnum, pos) + + string = match.group(0) + for fstring_stack_node in fstring_stack: + try: + string = string[:string.index(fstring_stack_node.quote)] + except ValueError: + pass # The string was not found. + + new_pos += len(string) + if allow_multiline and string.endswith('\n'): + tos.previous_lines += string + string = '' + else: + string = tos.previous_lines + string + + return string, new_pos + + def tokenize(code, version_info, start_pos=(1, 0)): """Generate tokens from a the source code (string).""" lines = split_lines(code, keepends=True) return tokenize_lines(lines, version_info, start_pos=start_pos) +def _print_tokens(func): + """ + A small helper function to help debug the tokenize_lines function. + """ + def wrapper(*args, **kwargs): + for token in func(*args, **kwargs): + print(token) + yield token + + return wrapper + + +# @_print_tokens def tokenize_lines(lines, version_info, start_pos=(1, 0)): """ A heavily modified Python standard library tokenizer. @@ -240,7 +356,8 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0)): token. This idea comes from lib2to3. The prefix contains all information that is irrelevant for the parser like newlines in parentheses or comments. """ - pseudo_token, single_quoted, triple_quoted, endpats, always_break_tokens, = \ + pseudo_token, single_quoted, triple_quoted, endpats, whitespace, \ + fstring_pattern_map, always_break_tokens, = \ _get_token_collection(version_info) paren_level = 0 # count parentheses indents = [0] @@ -257,6 +374,7 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0)): additional_prefix = '' first = True lnum = start_pos[0] - 1 + fstring_stack = [] for line in lines: # loop over lines in stream lnum += 1 pos = 0 @@ -287,12 +405,47 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0)): continue while pos < max: + if fstring_stack: + string, pos = _find_fstring_string(fstring_stack, line, lnum, pos) + if string: + yield PythonToken( + FSTRING_STRING, string, + fstring_stack[-1].last_string_start_pos, + # Never has a prefix because it can start anywhere and + # include whitespace. + prefix='' + ) + fstring_stack[-1].previous_lines = '' + continue + + if pos == max: + break + + rest = line[pos:] + fstring_index, end = _check_fstring_ending(fstring_stack, rest, from_start=True) + + if fstring_index is not None: + yield PythonToken( + FSTRING_END, + fstring_stack[fstring_index].quote, + (lnum, pos), + prefix=additional_prefix, + ) + additional_prefix = '' + del fstring_stack[fstring_index:] + pos += end + continue + pseudomatch = pseudo_token.match(line, pos) if not pseudomatch: # scan for tokens - txt = line[pos:] - if txt.endswith('\n'): + if line.endswith('\n'): new_line = True - yield PythonToken(ERRORTOKEN, txt, (lnum, pos), additional_prefix) + match = whitespace.match(line, pos) + pos = match.end() + yield PythonToken( + ERRORTOKEN, line[pos:], (lnum, pos), + additional_prefix + match.group(0) + ) additional_prefix = '' break @@ -311,10 +464,11 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0)): if new_line and initial not in '\r\n#': new_line = False - if paren_level == 0: + if paren_level == 0 and not fstring_stack: i = 0 while line[i] == '\f': i += 1 + # TODO don't we need to change spos as well? start -= 1 if start > indents[-1]: yield PythonToken(INDENT, '', spos, '') @@ -326,11 +480,33 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0)): yield PythonToken(DEDENT, '', spos, '') indents.pop() + if fstring_stack: + fstring_index, end = _check_fstring_ending(fstring_stack, token) + if fstring_index is not None: + if end != 0: + yield PythonToken(ERRORTOKEN, token[:end], spos, prefix) + prefix = '' + + yield PythonToken( + FSTRING_END, + fstring_stack[fstring_index].quote, + (lnum, spos[1] + 1), + prefix=prefix + ) + del fstring_stack[fstring_index:] + pos -= len(token) - end + continue + if (initial in numchars or # ordinary number (initial == '.' and token != '.' and token != '...')): yield PythonToken(NUMBER, token, spos, prefix) elif initial in '\r\n': - if not new_line and paren_level == 0: + if any(not f.allow_multiline() for f in fstring_stack): + # Would use fstring_stack.clear, but that's not available + # in Python 2. + fstring_stack[:] = [] + + if not new_line and paren_level == 0 and not fstring_stack: yield PythonToken(NEWLINE, token, spos, prefix) else: additional_prefix = prefix + token @@ -362,8 +538,12 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0)): break else: # ordinary string yield PythonToken(STRING, token, spos, prefix) + elif token in fstring_pattern_map: # The start of an fstring. + fstring_stack.append(FStringNode(fstring_pattern_map[token])) + yield PythonToken(FSTRING_START, token, spos, prefix) elif is_identifier(initial): # ordinary name if token in always_break_tokens: + fstring_stack[:] = [] paren_level = 0 while True: indent = indents.pop() @@ -378,9 +558,18 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0)): break else: if token in '([{': - paren_level += 1 + if fstring_stack: + fstring_stack[-1].open_parentheses(token) + else: + paren_level += 1 elif token in ')]}': - paren_level -= 1 + if fstring_stack: + fstring_stack[-1].close_parentheses(token) + else: + paren_level -= 1 + elif token == ':' and fstring_stack \ + and fstring_stack[-1].parentheses_count == 1: + fstring_stack[-1].format_spec_count += 1 try: # This check is needed in any case to check if it's a valid diff --git a/pythonFiles/parso/python/tree.py b/pythonFiles/parso/python/tree.py index eb977800a607..f6b4dd38fad0 100644 --- a/pythonFiles/parso/python/tree.py +++ b/pythonFiles/parso/python/tree.py @@ -60,7 +60,6 @@ _IMPORTS = set(['import_name', 'import_from']) - class DocstringMixin(object): __slots__ = () @@ -133,7 +132,6 @@ def get_start_pos_of_prefix(self): return previous_leaf.end_pos - class _LeafWithoutNewlines(PythonLeaf): """ Simply here to optimize performance. @@ -166,6 +164,10 @@ class EndMarker(_LeafWithoutNewlines): __slots__ = () type = 'endmarker' + @utf8_repr + def __repr__(self): + return "<%s: prefix=%s>" % (type(self).__name__, repr(self.prefix)) + class Newline(PythonLeaf): """Contains NEWLINE and ENDMARKER tokens.""" @@ -235,7 +237,6 @@ def get_definition(self, import_name_always=False): return None - class Literal(PythonLeaf): __slots__ = () @@ -262,6 +263,33 @@ def _get_payload(self): return match.group(2)[:-len(match.group(1))] +class FStringString(Leaf): + """ + f-strings contain f-string expressions and normal python strings. These are + the string parts of f-strings. + """ + type = 'fstring_string' + __slots__ = () + + +class FStringStart(Leaf): + """ + f-strings contain f-string expressions and normal python strings. These are + the string parts of f-strings. + """ + type = 'fstring_start' + __slots__ = () + + +class FStringEnd(Leaf): + """ + f-strings contain f-string expressions and normal python strings. These are + the string parts of f-strings. + """ + type = 'fstring_end' + __slots__ = () + + class _StringComparisonMixin(object): def __eq__(self, other): """ @@ -626,6 +654,7 @@ def annotation(self): except IndexError: return None + class Lambda(Function): """ Lambdas are basically trimmed functions, so give it the same interface. diff --git a/pythonFiles/parso/tree.py b/pythonFiles/parso/tree.py index 72a14945b0f6..5316795be57c 100644 --- a/pythonFiles/parso/tree.py +++ b/pythonFiles/parso/tree.py @@ -55,7 +55,6 @@ def get_previous_sibling(self): Returns the node immediately preceding this node in this parent's children list. If this node does not have a previous sibling, it is None. - None. """ # Can't use index(); we need to test by identity for i, child in enumerate(self.parent.children): @@ -339,7 +338,7 @@ def __repr__(self): class ErrorNode(BaseNode): """ - A node that containes valid nodes/leaves that we're follow by a token that + A node that contains valid nodes/leaves that we're follow by a token that was invalid. This basically means that the leaf after this node is where Python would mark a syntax error. """ diff --git a/pythonFiles/refactor.py b/pythonFiles/refactor.py index dd4b23ee7727..b782eee26ffb 100644 --- a/pythonFiles/refactor.py +++ b/pythonFiles/refactor.py @@ -2,9 +2,11 @@ # 1. Working directory. # 2. Rope folder +import difflib import io -import sys import json +import os +import sys import traceback try: @@ -55,6 +57,27 @@ def __init__(self, filePath, fileMode=ChangeType.EDIT, diff=""): self.diff = diff self.fileMode = fileMode +def get_diff(changeset): + """This is a copy of the code form the ChangeSet.get_description method found in Rope.""" + new = changeset.new_contents + old = changeset.old_contents + if old is None: + if changeset.resource.exists(): + old = changeset.resource.read() + else: + old = '' + + # Ensure code has a trailing empty lines, before generating a diff. + # https://github.com/Microsoft/vscode-python/issues/695. + old_lines = old.splitlines(True) + if not old_lines[-1].endswith('\n'): + old_lines[-1] = old_lines[-1] + os.linesep + new = new + os.linesep + + result = difflib.unified_diff( + old_lines, new.splitlines(True), + 'a/' + changeset.resource.path, 'b/' + changeset.resource.path) + return ''.join(list(result)) class BaseRefactoring(object): """ @@ -117,7 +140,7 @@ def onRefactor(self): for item in changes.changes: if isinstance(item, rope.base.change.ChangeContents): self.changes.append( - Change(item.resource.real_path, ChangeType.EDIT, item.get_description())) + Change(item.resource.real_path, ChangeType.EDIT, get_diff(item))) else: raise Exception('Unknown Change') @@ -141,7 +164,7 @@ def onRefactor(self): for item in changes.changes: if isinstance(item, rope.base.change.ChangeContents): self.changes.append( - Change(item.resource.real_path, ChangeType.EDIT, item.get_description())) + Change(item.resource.real_path, ChangeType.EDIT, get_diff(item))) else: raise Exception('Unknown Change') @@ -160,7 +183,7 @@ def onRefactor(self): for item in changes.changes: if isinstance(item, rope.base.change.ChangeContents): self.changes.append( - Change(item.resource.real_path, ChangeType.EDIT, item.get_description())) + Change(item.resource.real_path, ChangeType.EDIT, get_diff(item))) else: raise Exception('Unknown Change') diff --git a/requirements.txt b/requirements.txt index 752eee82422f..cb71d94eb4e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,15 @@ -autopep8==1.2.1 -yapf==0.6.2 -pylint==1.8.2 -pep8==1.7.0 -prospector==0.11.7 -flake8==2.6.0 -pydocstyle==1.0.0 +# Install flake8 first, as both flake8 and autopep8 require pycodestyle, +# but flake8 has a tighter pinning. +flake8 +autopep8 +black ; python_version>='3.6' +yapf +pylint<2.0.0 +pep8 +prospector +pydocstyle nose pytest -fabric -numba rope +flask +django diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts new file mode 100644 index 000000000000..73af6ba7cd0b --- /dev/null +++ b/src/client/activation/activationService.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { ConfigurationChangeEvent, Disposable, OutputChannel, Uri } from 'vscode'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; +import { isLanguageServerTest, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; +import '../common/extensions'; +import { IConfigurationService, IDisposableRegistry, IOutputChannel, IPythonSettings } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { ExtensionActivators, IExtensionActivationService, IExtensionActivator } from './types'; + +const jediEnabledSetting: keyof IPythonSettings = 'jediEnabled'; + +type ActivatorInfo = { jedi: boolean; activator: IExtensionActivator }; + +@injectable() +export class ExtensionActivationService implements IExtensionActivationService, Disposable { + private currentActivator?: ActivatorInfo; + private readonly workspaceService: IWorkspaceService; + private readonly output: OutputChannel; + private readonly appShell: IApplicationShell; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.workspaceService = this.serviceContainer.get(IWorkspaceService); + this.output = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + this.appShell = this.serviceContainer.get(IApplicationShell); + + const disposables = serviceContainer.get(IDisposableRegistry); + disposables.push(this); + disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); + } + public async activate(): Promise { + if (this.currentActivator) { + return; + } + + const jedi = this.useJedi(); + + const engineName = jedi ? 'Jedi Python language engine' : 'Microsoft Python language server'; + this.output.appendLine(`Starting ${engineName}.`); + const activatorName = jedi ? ExtensionActivators.Jedi : ExtensionActivators.DotNet; + const activator = this.serviceContainer.get(IExtensionActivator, activatorName); + this.currentActivator = { jedi, activator }; + + await activator.activate(); + } + public dispose() { + if (this.currentActivator) { + this.currentActivator.activator.deactivate().ignoreErrors(); + } + } + private async onDidChangeConfiguration(event: ConfigurationChangeEvent) { + const workspacesUris: (Uri | undefined)[] = this.workspaceService.hasWorkspaceFolders ? this.workspaceService.workspaceFolders!.map(workspace => workspace.uri) : [undefined]; + if (workspacesUris.findIndex(uri => event.affectsConfiguration(`python.${jediEnabledSetting}`, uri)) === -1) { + return; + } + const jedi = this.useJedi(); + if (this.currentActivator && this.currentActivator.jedi === jedi) { + return; + } + + const item = await this.appShell.showInformationMessage('Please reload the window switching between language engines.', 'Reload'); + if (item === 'Reload') { + this.serviceContainer.get(ICommandManager).executeCommand('workbench.action.reloadWindow'); + } + } + private useJedi(): boolean { + const workspacesUris: (Uri | undefined)[] = this.workspaceService.hasWorkspaceFolders ? this.workspaceService.workspaceFolders!.map(item => item.uri) : [undefined]; + const configuraionService = this.serviceContainer.get(IConfigurationService); + const jediEnabledForAnyWorkspace = workspacesUris.filter(uri => configuraionService.getSettings(uri).jediEnabled).length > 0; + return !isLanguageServerTest() && jediEnabledForAnyWorkspace; + } +} diff --git a/src/client/activation/downloader.ts b/src/client/activation/downloader.ts new file mode 100644 index 000000000000..0bb6b9dbcf20 --- /dev/null +++ b/src/client/activation/downloader.ts @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as fileSystem from 'fs'; +import * as path from 'path'; +import * as request from 'request'; +import * as requestProgress from 'request-progress'; +import { OutputChannel, ProgressLocation, window } from 'vscode'; +import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; +import { createDeferred } from '../common/helpers'; +import { IFileSystem, IPlatformService } from '../common/platform/types'; +import { IExtensionContext, IOutputChannel } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { PlatformData, PlatformName } from './platformData'; + +// tslint:disable-next-line:no-require-imports no-var-requires +const StreamZip = require('node-stream-zip'); + +const downloadUriPrefix = 'https://pvsc.blob.core.windows.net/python-language-server'; +const downloadBaseFileName = 'Python-Language-Server'; +const downloadVersion = '0.1.18204.3'; +const downloadFileExtension = '.nupkg'; + +export const DownloadLinks = { + [PlatformName.Windows32Bit]: `${downloadUriPrefix}/${downloadBaseFileName}-${PlatformName.Windows32Bit}.${downloadVersion}${downloadFileExtension}`, + [PlatformName.Windows64Bit]: `${downloadUriPrefix}/${downloadBaseFileName}-${PlatformName.Windows64Bit}.${downloadVersion}${downloadFileExtension}`, + [PlatformName.Linux64Bit]: `${downloadUriPrefix}/${downloadBaseFileName}-${PlatformName.Linux64Bit}.${downloadVersion}${downloadFileExtension}`, + [PlatformName.Mac64Bit]: `${downloadUriPrefix}/${downloadBaseFileName}-${PlatformName.Mac64Bit}.${downloadVersion}${downloadFileExtension}` +}; + +export class LanguageServerDownloader { + private readonly output: OutputChannel; + private readonly platform: IPlatformService; + private readonly platformData: PlatformData; + private readonly fs: IFileSystem; + + constructor(private readonly services: IServiceContainer, private engineFolder: string) { + this.output = this.services.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + this.fs = this.services.get(IFileSystem); + this.platform = this.services.get(IPlatformService); + this.platformData = new PlatformData(this.platform, this.fs); + } + + public async getDownloadUri() { + const platformString = await this.platformData.getPlatformName(); + return DownloadLinks[platformString]; + } + + public async downloadLanguageServer(context: IExtensionContext): Promise { + const downloadUri = await this.getDownloadUri(); + + let localTempFilePath = ''; + try { + localTempFilePath = await this.downloadFile(downloadUri, 'Downloading Microsoft Python Language Server... '); + await this.unpackArchive(context.extensionPath, localTempFilePath); + } catch (err) { + this.output.appendLine('failed.'); + this.output.appendLine(err); + throw new Error(err); + } finally { + if (localTempFilePath.length > 0) { + await this.fs.deleteFile(localTempFilePath); + } + } + } + + private async downloadFile(uri: string, title: string): Promise { + this.output.append(`Downloading ${uri}... `); + const tempFile = await this.fs.createTemporaryFile(downloadFileExtension); + + const deferred = createDeferred(); + const fileStream = fileSystem.createWriteStream(tempFile.filePath); + fileStream.on('finish', () => { + fileStream.close(); + }).on('error', (err) => { + tempFile.dispose(); + deferred.reject(err); + }); + + await window.withProgress({ + location: ProgressLocation.Window + }, (progress) => { + + requestProgress(request(uri)) + .on('progress', (state) => { + // https://www.npmjs.com/package/request-progress + const received = Math.round(state.size.transferred / 1024); + const total = Math.round(state.size.total / 1024); + const percentage = Math.round(100 * state.percent); + progress.report({ + message: `${title}${received} of ${total} KB (${percentage}%)` + }); + }) + .on('error', (err) => { + deferred.reject(err); + }) + .on('end', () => { + this.output.append('complete.'); + deferred.resolve(); + }) + .pipe(fileStream); + return deferred.promise; + }); + + return tempFile.filePath; + } + + private async unpackArchive(extensionPath: string, tempFilePath: string): Promise { + this.output.append('Unpacking archive... '); + + const installFolder = path.join(extensionPath, this.engineFolder); + const deferred = createDeferred(); + + const title = 'Extracting files... '; + await window.withProgress({ + location: ProgressLocation.Window, + title + }, (progress) => { + const zip = new StreamZip({ + file: tempFilePath, + storeEntries: true + }); + + let totalFiles = 0; + let extractedFiles = 0; + zip.on('ready', async () => { + totalFiles = zip.entriesCount; + if (!await this.fs.directoryExists(installFolder)) { + await this.fs.createDirectory(installFolder); + } + zip.extract(null, installFolder, (err, count) => { + if (err) { + deferred.reject(err); + } else { + deferred.resolve(); + } + zip.close(); + }); + }).on('extract', (entry, file) => { + extractedFiles += 1; + progress.report({ message: `${title}${Math.round(100 * extractedFiles / totalFiles)}%` }); + }).on('error', e => { + deferred.reject(e); + }); + return deferred.promise; + }); + + // Set file to executable + if (!this.platform.isWindows) { + const executablePath = path.join(installFolder, this.platformData.getEngineExecutableName()); + fileSystem.chmodSync(executablePath, '0764'); // -rwxrw-r-- + } + this.output.appendLine('done.'); + } +} diff --git a/src/client/activation/hashVerifier.ts b/src/client/activation/hashVerifier.ts new file mode 100644 index 000000000000..61d1177966f9 --- /dev/null +++ b/src/client/activation/hashVerifier.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { createHash } from 'crypto'; +import * as fs from 'fs'; +import { createDeferred } from '../common/helpers'; + +export class HashVerifier { + public async verifyHash(filePath: string, platformString: string, expectedDigest: string): Promise { + const readStream = fs.createReadStream(filePath); + const deferred = createDeferred(); + const hash = createHash('sha512'); + hash.setEncoding('hex'); + readStream + .on('end', () => { + hash.end(); + deferred.resolve(); + }) + .on('error', (err) => { + deferred.reject(`Unable to calculate file hash. Error ${err}`); + }); + + readStream.pipe(hash); + await deferred.promise; + const actual = hash.read() as string; + return expectedDigest === platformString ? true : actual.toLowerCase() === expectedDigest.toLowerCase(); + } +} diff --git a/src/client/activation/interpreterDataService.ts b/src/client/activation/interpreterDataService.ts new file mode 100644 index 000000000000..f923758b36bf --- /dev/null +++ b/src/client/activation/interpreterDataService.ts @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { createHash } from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import { ExtensionContext, Uri } from 'vscode'; +import { IApplicationShell } from '../common/application/types'; +import '../common/extensions'; +import { createDeferred } from '../common/helpers'; +import { IPlatformService } from '../common/platform/types'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../common/process/types'; +import { IServiceContainer } from '../ioc/types'; + +const DataVersion = 1; + +export class InterpreterData { + constructor( + public readonly dataVersion: number, + // tslint:disable-next-line:no-shadowed-variable + public readonly path: string, + public readonly version: string, + public readonly prefix: string, + public readonly searchPaths: string, + public readonly hash: string + ) { } +} + +export class InterpreterDataService { + constructor( + private readonly context: ExtensionContext, + private readonly serviceContainer: IServiceContainer) { } + + public async getInterpreterData(resource?: Uri): Promise { + const executionFactory = this.serviceContainer.get(IPythonExecutionFactory); + const execService = await executionFactory.create({ resource }); + + const interpreterPath = await execService.getExecutablePath(); + if (interpreterPath.length === 0) { + return; + } + + const cacheKey = `InterpreterData-${interpreterPath}`; + let interpreterData = this.context.globalState.get(cacheKey) as InterpreterData; + let interpreterChanged = false; + if (interpreterData) { + // Check if interpreter executable changed + if (interpreterData.dataVersion !== DataVersion) { + interpreterChanged = true; + } else { + const currentHash = await this.getInterpreterHash(interpreterPath); + interpreterChanged = currentHash !== interpreterData.hash; + } + } + + if (interpreterChanged || !interpreterData) { + interpreterData = await this.getInterpreterDataFromPython(execService, interpreterPath); + this.context.globalState.update(interpreterPath, interpreterData); + } else { + // Make sure we verify that search paths did not change. This must be done + // completely async so we don't delay Python language server startup. + this.verifySearchPaths(interpreterData.searchPaths, interpreterPath, execService); + } + return interpreterData; + } + + public getInterpreterHash(interpreterPath: string): Promise { + const platform = this.serviceContainer.get(IPlatformService); + const pythonExecutable = path.join(path.dirname(interpreterPath), platform.isWindows ? 'python.exe' : 'python'); + // Hash mod time and creation time + const deferred = createDeferred(); + fs.lstat(pythonExecutable, (err, stats) => { + if (err) { + deferred.resolve(''); + } else { + const actual = createHash('sha512').update(`${stats.ctime}-${stats.mtime}`).digest('hex'); + deferred.resolve(actual); + } + }); + return deferred.promise; + } + + private async getInterpreterDataFromPython(execService: IPythonExecutionService, interpreterPath: string): Promise { + const result = await execService.exec(['-c', 'import sys; print(sys.version_info); print(sys.prefix)'], {}); + // 2.7.14 (v2.7.14:84471935ed, Sep 16 2017, 20:19:30) <> + // [MSC v.1500 32 bit (Intel)] + // C:\Python27 + if (!result.stdout) { + throw Error('Unable to determine Python interpreter version and system prefix.'); + } + const output = result.stdout.splitLines({ removeEmptyEntries: true, trim: true }); + if (!output || output.length < 2) { + throw Error('Unable to parse version and and system prefix from the Python interpreter output.'); + } + const majorMatches = output[0].match(/major=(\d*?),/); + const minorMatches = output[0].match(/minor=(\d*?),/); + if (!majorMatches || majorMatches.length < 2 || !minorMatches || minorMatches.length < 2) { + throw Error('Unable to parse interpreter version.'); + } + const prefix = output[output.length - 1]; + const hash = await this.getInterpreterHash(interpreterPath); + const searchPaths = await this.getSearchPaths(execService); + return new InterpreterData(DataVersion, interpreterPath, `${majorMatches[1]}.${minorMatches[1]}`, prefix, searchPaths, hash); + } + + private async getSearchPaths(execService: IPythonExecutionService): Promise { + const result = await execService.exec(['-c', 'import sys; print(sys.path);'], {}); + if (!result.stdout) { + throw Error('Unable to determine Python interpreter search paths.'); + } + // tslint:disable-next-line:no-unnecessary-local-variable + const paths = result.stdout.split(',') + .filter(p => this.isValidPath(p)) + .map(p => this.pathCleanup(p)); + return paths.join(';'); // PTVS uses ; on all platforms + } + + private pathCleanup(s: string): string { + s = s.trim(); + if (s[0] === '\'') { + s = s.substr(1); + } + if (s[s.length - 1] === ']') { + s = s.substr(0, s.length - 1); + } + if (s[s.length - 1] === '\'') { + s = s.substr(0, s.length - 1); + } + return s; + } + + private isValidPath(s: string): boolean { + return s.length > 0 && s[0] !== '['; + } + + private verifySearchPaths(currentPaths: string, interpreterPath: string, execService: IPythonExecutionService): void { + this.getSearchPaths(execService) + .then(async paths => { + if (paths !== currentPaths) { + this.context.globalState.update(interpreterPath, undefined); + const appShell = this.serviceContainer.get(IApplicationShell); + await appShell.showWarningMessage('Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly.'); + } + }).ignoreErrors(); + } +} diff --git a/src/client/activation/jedi.ts b/src/client/activation/jedi.ts new file mode 100644 index 000000000000..86629009162a --- /dev/null +++ b/src/client/activation/jedi.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { DocumentFilter, languages } from 'vscode'; +import { PYTHON } from '../common/constants'; +import { IConfigurationService, IExtensionContext, ILogger } from '../common/types'; +import { IShebangCodeLensProvider } from '../interpreter/contracts'; +import { IServiceContainer, IServiceManager } from '../ioc/types'; +import { JediFactory } from '../languageServices/jediProxyFactory'; +import { PythonCompletionItemProvider } from '../providers/completionProvider'; +import { PythonDefinitionProvider } from '../providers/definitionProvider'; +import { PythonHoverProvider } from '../providers/hoverProvider'; +import { activateGoToObjectDefinitionProvider } from '../providers/objectDefinitionProvider'; +import { PythonReferenceProvider } from '../providers/referenceProvider'; +import { PythonRenameProvider } from '../providers/renameProvider'; +import { PythonSignatureProvider } from '../providers/signatureProvider'; +import { PythonSymbolProvider } from '../providers/symbolProvider'; +import { IUnitTestManagementService } from '../unittests/types'; +import { WorkspaceSymbols } from '../workspaceSymbols/main'; +import { IExtensionActivator } from './types'; + +@injectable() +export class JediExtensionActivator implements IExtensionActivator { + private readonly context: IExtensionContext; + private jediFactory?: JediFactory; + private readonly documentSelector: DocumentFilter[]; + constructor(@inject(IServiceManager) private serviceManager: IServiceManager) { + this.context = this.serviceManager.get(IExtensionContext); + this.documentSelector = PYTHON; + } + + public async activate(): Promise { + const context = this.context; + + const jediFactory = this.jediFactory = new JediFactory(context.asAbsolutePath('.'), this.serviceManager); + context.subscriptions.push(jediFactory); + context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory)); + + context.subscriptions.push(jediFactory); + context.subscriptions.push(languages.registerRenameProvider(this.documentSelector, new PythonRenameProvider(this.serviceManager))); + const definitionProvider = new PythonDefinitionProvider(jediFactory); + + context.subscriptions.push(languages.registerDefinitionProvider(this.documentSelector, definitionProvider)); + context.subscriptions.push(languages.registerHoverProvider(this.documentSelector, new PythonHoverProvider(jediFactory))); + context.subscriptions.push(languages.registerReferenceProvider(this.documentSelector, new PythonReferenceProvider(jediFactory))); + context.subscriptions.push(languages.registerCompletionItemProvider(this.documentSelector, new PythonCompletionItemProvider(jediFactory, this.serviceManager), '.')); + context.subscriptions.push(languages.registerCodeLensProvider(this.documentSelector, this.serviceManager.get(IShebangCodeLensProvider))); + + const serviceContainer = this.serviceManager.get(IServiceContainer); + context.subscriptions.push(new WorkspaceSymbols(serviceContainer)); + + const symbolProvider = new PythonSymbolProvider(serviceContainer, jediFactory); + context.subscriptions.push(languages.registerDocumentSymbolProvider(this.documentSelector, symbolProvider)); + + const pythonSettings = this.serviceManager.get(IConfigurationService).getSettings(); + if (pythonSettings.devOptions.indexOf('DISABLE_SIGNATURE') === -1) { + context.subscriptions.push(languages.registerSignatureHelpProvider(this.documentSelector, new PythonSignatureProvider(jediFactory), '(', ',')); + } + + const testManagementService = this.serviceManager.get(IUnitTestManagementService); + testManagementService.activate() + .then(() => testManagementService.activateCodeLenses(symbolProvider)) + .catch(ex => this.serviceManager.get(ILogger).logError('Failed to activate Unit Tests', ex)); + + return true; + } + + public async deactivate(): Promise { + if (this.jediFactory) { + this.jediFactory.dispose(); + } + } +} diff --git a/src/client/activation/languageServer.ts b/src/client/activation/languageServer.ts new file mode 100644 index 000000000000..8c416206a7b1 --- /dev/null +++ b/src/client/activation/languageServer.ts @@ -0,0 +1,345 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { CancellationToken, CompletionContext, OutputChannel, Position, + TextDocument, Uri } from 'vscode'; +import { Disposable, LanguageClient, LanguageClientOptions, + ProvideCompletionItemsSignature, ServerOptions } from 'vscode-languageclient'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; +import { PythonSettings } from '../common/configSettings'; +import { isTestExecution, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; +import { createDeferred, Deferred } from '../common/helpers'; +import { IFileSystem, IPlatformService } from '../common/platform/types'; +import { StopWatch } from '../common/stopWatch'; +import { BANNER_NAME_LS_SURVEY, IConfigurationService, IExtensionContext, ILogger, + IOutputChannel, IPythonExtensionBanner, IPythonSettings } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { + PYTHON_LANGUAGE_SERVER_DOWNLOADED, + PYTHON_LANGUAGE_SERVER_ENABLED, + PYTHON_LANGUAGE_SERVER_ERROR +} from '../telemetry/constants'; +import { getTelemetryReporter } from '../telemetry/telemetry'; +import { IUnitTestManagementService } from '../unittests/types'; +import { LanguageServerDownloader } from './downloader'; +import { InterpreterData, InterpreterDataService } from './interpreterDataService'; +import { PlatformData } from './platformData'; +import { ProgressReporting } from './progress'; +import { IExtensionActivator } from './types'; + +const PYTHON = 'python'; +const dotNetCommand = 'dotnet'; +const languageClientName = 'Python Tools'; +const languageServerFolder = 'languageServer'; +const loadExtensionCommand = 'python._loadLanguageServerExtension'; + +@injectable() +export class LanguageServerExtensionActivator implements IExtensionActivator { + private readonly configuration: IConfigurationService; + private readonly appShell: IApplicationShell; + private readonly output: OutputChannel; + private readonly fs: IFileSystem; + private readonly sw = new StopWatch(); + private readonly platformData: PlatformData; + private readonly startupCompleted: Deferred; + private readonly disposables: Disposable[] = []; + private readonly context: IExtensionContext; + private readonly workspace: IWorkspaceService; + private readonly root: Uri | undefined; + + private languageClient: LanguageClient | undefined; + private interpreterHash: string = ''; + private excludedFiles: string[] = []; + private typeshedPaths: string[] = []; + private loadExtensionArgs: {} | undefined; + private surveyBanner: IPythonExtensionBanner; + // tslint:disable-next-line:no-unused-variable + private progressReporting: ProgressReporting | undefined; + + constructor(@inject(IServiceContainer) private readonly services: IServiceContainer) { + this.context = this.services.get(IExtensionContext); + this.configuration = this.services.get(IConfigurationService); + this.appShell = this.services.get(IApplicationShell); + this.output = this.services.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + this.fs = this.services.get(IFileSystem); + this.platformData = new PlatformData(services.get(IPlatformService), this.fs); + this.workspace = this.services.get(IWorkspaceService); + + // Currently only a single root. Multi-root support is future. + this.root = this.workspace && this.workspace.hasWorkspaceFolders + ? this.workspace.workspaceFolders![0]!.uri : undefined; + + this.startupCompleted = createDeferred(); + const commandManager = this.services.get(ICommandManager); + + this.disposables.push(commandManager.registerCommand(loadExtensionCommand, + async (args) => { + if (this.languageClient) { + await this.startupCompleted.promise; + this.languageClient.sendRequest('python/loadExtension', args); + } else { + this.loadExtensionArgs = args; + } + } + )); + + this.surveyBanner = services.get(IPythonExtensionBanner, BANNER_NAME_LS_SURVEY); + + (this.configuration.getSettings() as PythonSettings).addListener('change', this.onSettingsChanged); + } + + public async activate(): Promise { + this.sw.reset(); + const clientOptions = await this.getAnalysisOptions(); + if (!clientOptions) { + return false; + } + + const testManagementService = this.services.get(IUnitTestManagementService); + testManagementService.activate() + .catch(ex => this.services.get(ILogger).logError('Failed to activate Unit Tests', ex)); + + return this.startLanguageServer(clientOptions); + } + + public async deactivate(): Promise { + if (this.languageClient) { + // Do not await on this + this.languageClient.stop(); + } + for (const d of this.disposables) { + d.dispose(); + } + (this.configuration.getSettings() as PythonSettings).removeListener('change', this.onSettingsChanged); + } + + private async startLanguageServer(clientOptions: LanguageClientOptions): Promise { + // Determine if we are running MSIL/Universal via dotnet or self-contained app. + + const reporter = getTelemetryReporter(); + reporter.sendTelemetryEvent(PYTHON_LANGUAGE_SERVER_ENABLED); + + const settings = this.configuration.getSettings(); + if (!settings.downloadLanguageServer) { + // Depends on .NET Runtime or SDK. Typically development-only case. + this.languageClient = this.createSimpleLanguageClient(clientOptions); + await this.startLanguageClient(); + return true; + } + + const mscorlib = path.join(this.context.extensionPath, languageServerFolder, 'mscorlib.dll'); + if (!await this.fs.fileExists(mscorlib)) { + const downloader = new LanguageServerDownloader(this.services, languageServerFolder); + await downloader.downloadLanguageServer(this.context); + reporter.sendTelemetryEvent(PYTHON_LANGUAGE_SERVER_DOWNLOADED); + } + + const serverModule = path.join(this.context.extensionPath, languageServerFolder, this.platformData.getEngineExecutableName()); + this.languageClient = this.createSelfContainedLanguageClient(serverModule, clientOptions); + try { + await this.startLanguageClient(); + return true; + } catch (ex) { + this.appShell.showErrorMessage(`Language server failed to start. Error ${ex}`); + reporter.sendTelemetryEvent(PYTHON_LANGUAGE_SERVER_ERROR, { error: 'Failed to start (platform)' }); + return false; + } + } + + private async startLanguageClient(): Promise { + this.context.subscriptions.push(this.languageClient!.start()); + await this.serverReady(); + this.progressReporting = new ProgressReporting(this.languageClient!); + } + + private async serverReady(): Promise { + while (!this.languageClient!.initializeResult) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + if (this.loadExtensionArgs) { + this.languageClient!.sendRequest('python/loadExtension', this.loadExtensionArgs); + } + + this.startupCompleted.resolve(); + } + + private createSimpleLanguageClient(clientOptions: LanguageClientOptions): LanguageClient { + const commandOptions = { stdio: 'pipe' }; + const serverModule = path.join(this.context.extensionPath, languageServerFolder, this.platformData.getEngineDllName()); + const serverOptions: ServerOptions = { + run: { command: dotNetCommand, args: [serverModule], options: commandOptions }, + debug: { command: dotNetCommand, args: [serverModule, '--debug'], options: commandOptions } + }; + return new LanguageClient(PYTHON, languageClientName, serverOptions, clientOptions); + } + + private createSelfContainedLanguageClient(serverModule: string, clientOptions: LanguageClientOptions): LanguageClient { + const options = { stdio: 'pipe' }; + const serverOptions: ServerOptions = { + run: { command: serverModule, rgs: [], options: options }, + debug: { command: serverModule, args: ['--debug'], options } + }; + return new LanguageClient(PYTHON, languageClientName, serverOptions, clientOptions); + } + + private async getAnalysisOptions(): Promise { + // tslint:disable-next-line:no-any + const properties = new Map(); + let interpreterData: InterpreterData | undefined; + let pythonPath = ''; + + try { + const interpreterDataService = new InterpreterDataService(this.context, this.services); + interpreterData = await interpreterDataService.getInterpreterData(); + } catch (ex) { + this.appShell.showWarningMessage('Unable to determine path to the Python interpreter. IntelliSense will be limited.'); + } + + this.interpreterHash = interpreterData ? interpreterData.hash : ''; + if (interpreterData) { + pythonPath = path.dirname(interpreterData.path); + // tslint:disable-next-line:no-string-literal + properties['InterpreterPath'] = interpreterData.path; + // tslint:disable-next-line:no-string-literal + properties['Version'] = interpreterData.version; + // tslint:disable-next-line:no-string-literal + properties['PrefixPath'] = interpreterData.prefix; + } + + // tslint:disable-next-line:no-string-literal + properties['DatabasePath'] = path.join(this.context.extensionPath, languageServerFolder); + + let searchPaths = interpreterData ? interpreterData.searchPaths.split(path.delimiter) : []; + const settings = this.configuration.getSettings(); + if (settings.autoComplete) { + const extraPaths = settings.autoComplete.extraPaths; + if (extraPaths && extraPaths.length > 0) { + searchPaths.push(...extraPaths); + } + } + + // Make sure paths do not contain multiple slashes so file URIs + // in VS Code (Node.js) and in the language server (.NET) match. + // Note: for the language server paths separator is always ; + searchPaths.push(pythonPath); + searchPaths = searchPaths.map(p => path.normalize(p)); + + const selector = [{ language: PYTHON, scheme: 'file' }]; + this.excludedFiles = this.getExcludedFiles(); + this.typeshedPaths = this.getTypeshedPaths(settings); + + const traceLogging = (settings.analysis && settings.analysis.traceLogging) ? settings.analysis.traceLogging : false; + + // Options to control the language client + return { + // Register the server for Python documents + documentSelector: selector, + synchronize: { + configurationSection: PYTHON + }, + outputChannel: this.output, + initializationOptions: { + interpreter: { + properties + }, + displayOptions: { + preferredFormat: 'markdown', + trimDocumentationLines: false, + maxDocumentationLineLength: 0, + trimDocumentationText: false, + maxDocumentationTextLength: 0 + }, + searchPaths, + typeStubSearchPaths: this.typeshedPaths, + excludeFiles: this.excludedFiles, + testEnvironment: isTestExecution(), + analysisUpdates: true, + traceLogging + }, + middleware: { + provideCompletionItem: (document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature) => { + if (this.surveyBanner) { + this.surveyBanner.showBanner().ignoreErrors(); + } + return next(document, position, context, token); + } + } + }; + } + + private getExcludedFiles(): string[] { + const list: string[] = ['**/Lib/**', '**/site-packages/**']; + this.getVsCodeExcludeSection('search.exclude', list); + this.getVsCodeExcludeSection('files.exclude', list); + this.getVsCodeExcludeSection('files.watcherExclude', list); + this.getPythonExcludeSection('linting.ignorePatterns', list); + this.getPythonExcludeSection('workspaceSymbols.exclusionPattern', list); + return list; + } + + private getVsCodeExcludeSection(setting: string, list: string[]): void { + const states = this.workspace.getConfiguration(setting, this.root); + if (states) { + Object.keys(states) + .filter(k => (k.indexOf('*') >= 0 || k.indexOf('/') >= 0) && states[k]) + .forEach(p => list.push(p)); + } + } + + private getPythonExcludeSection(setting: string, list: string[]): void { + const pythonSettings = this.configuration.getSettings(this.root); + const paths = pythonSettings && pythonSettings.linting ? pythonSettings.linting.ignorePatterns : undefined; + if (paths && Array.isArray(paths)) { + paths + .filter(p => p && p.length > 0) + .forEach(p => list.push(p)); + } + } + + private getTypeshedPaths(settings: IPythonSettings): string[] { + return settings.analysis.typeshedPaths && settings.analysis.typeshedPaths.length > 0 + ? settings.analysis.typeshedPaths + : [path.join(this.context.extensionPath, 'typeshed')]; + } + + private async onSettingsChanged(): Promise { + const ids = new InterpreterDataService(this.context, this.services); + const idata = await ids.getInterpreterData(); + if (!idata || idata.hash !== this.interpreterHash) { + this.interpreterHash = idata ? idata.hash : ''; + await this.restartLanguageServer(); + return; + } + + const excludedFiles = this.getExcludedFiles(); + await this.restartLanguageServerIfArrayChanged(this.excludedFiles, excludedFiles); + + const settings = this.configuration.getSettings(); + const typeshedPaths = this.getTypeshedPaths(settings); + await this.restartLanguageServerIfArrayChanged(this.typeshedPaths, typeshedPaths); + } + + private async restartLanguageServerIfArrayChanged(oldArray: string[], newArray: string[]): Promise { + if (newArray.length !== oldArray.length) { + await this.restartLanguageServer(); + return; + } + + for (let i = 0; i < oldArray.length; i += 1) { + if (oldArray[i] !== newArray[i]) { + await this.restartLanguageServer(); + return; + } + } + } + + private async restartLanguageServer(): Promise { + if (!this.context) { + return; + } + await this.deactivate(); + await this.activate(); + } +} diff --git a/src/client/activation/languageServerHashes.ts b/src/client/activation/languageServerHashes.ts new file mode 100644 index 000000000000..d8d856064363 --- /dev/null +++ b/src/client/activation/languageServerHashes.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// This file will be replaced by a generated one during the release build +// with actual hashes of the uploaded packages. +// Values are for test purposes only +export const language_server_win_x86_sha512 = 'win-x86'; +export const language_server_win_x64_sha512 = 'win-x64'; +export const language_server_osx_x64_sha512 = 'osx-x64'; +export const language_server_linux_x64_sha512 = 'linux-x64'; diff --git a/src/client/activation/platformData.ts b/src/client/activation/platformData.ts new file mode 100644 index 000000000000..39cce79cc82f --- /dev/null +++ b/src/client/activation/platformData.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IFileSystem, IPlatformService } from '../common/platform/types'; +import { + language_server_linux_x64_sha512, + language_server_osx_x64_sha512, + language_server_win_x64_sha512, + language_server_win_x86_sha512 +} from './languageServerHashes'; + +export enum PlatformName { + Windows32Bit = 'win-x86', + Windows64Bit = 'win-x64', + Mac64Bit = 'osx-x64', + Linux64Bit = 'linux-x64' +} + +export enum PlatformLSExecutables { + Windows = 'Microsoft.Python.LanguageServer.exe', + MacOS = 'Microsoft.Python.LanguageServer', + Linux = 'Microsoft.Python.LanguageServer' +} + +export class PlatformData { + constructor(private platform: IPlatformService, fs: IFileSystem) { } + public async getPlatformName(): Promise { + if (this.platform.isWindows) { + return this.platform.is64bit ? PlatformName.Windows64Bit : PlatformName.Windows32Bit; + } + if (this.platform.isMac) { + return PlatformName.Mac64Bit; + } + if (this.platform.isLinux) { + if (!this.platform.is64bit) { + throw new Error('Microsoft Python Language Server does not support 32-bit Linux.'); + } + return PlatformName.Linux64Bit; + } + throw new Error('Unknown OS platform.'); + } + + public getEngineDllName(): string { + return 'Microsoft.Python.LanguageServer.dll'; + } + + public getEngineExecutableName(): string { + if (this.platform.isWindows) { + return PlatformLSExecutables.Windows; + } else if (this.platform.isLinux) { + return PlatformLSExecutables.Linux; + } else if (this.platform.isMac) { + return PlatformLSExecutables.MacOS; + } else { + return 'unknown-platform'; + } + } + + public async getExpectedHash(): Promise { + if (this.platform.isWindows) { + return this.platform.is64bit ? language_server_win_x64_sha512 : language_server_win_x86_sha512; + } + if (this.platform.isMac) { + return language_server_osx_x64_sha512; + } + if (this.platform.isLinux && this.platform.is64bit) { + return language_server_linux_x64_sha512; + } + throw new Error('Unknown platform.'); + } +} diff --git a/src/client/activation/progress.ts b/src/client/activation/progress.ts new file mode 100644 index 000000000000..a9c7fa916fcc --- /dev/null +++ b/src/client/activation/progress.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Progress, ProgressLocation, window } from 'vscode'; +import { Disposable, LanguageClient } from 'vscode-languageclient'; +import { createDeferred, Deferred } from '../common/helpers'; + +export class ProgressReporting { + private statusBarMessage: Disposable | undefined; + private progress: Progress<{ message?: string; increment?: number }> | undefined; + private progressDeferred: Deferred | undefined; + + constructor(private readonly languageClient: LanguageClient) { + this.languageClient.onNotification('python/setStatusBarMessage', (m: string) => { + if (this.statusBarMessage) { + this.statusBarMessage.dispose(); + } + this.statusBarMessage = window.setStatusBarMessage(m); + }); + + this.languageClient.onNotification('python/beginProgress', async _ => { + this.progressDeferred = createDeferred(); + window.withProgress({ + location: ProgressLocation.Window, + title: '' + }, progress => { + this.progress = progress; + return this.progressDeferred!.promise; + }); + }); + + this.languageClient.onNotification('python/reportProgress', (m: string) => { + if (!this.progress) { + return; + } + this.progress.report({ message: m }); + }); + + this.languageClient.onNotification('python/endProgress', _ => { + if (this.progressDeferred) { + this.progressDeferred.resolve(); + this.progressDeferred = undefined; + } + }); + } +} diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts new file mode 100644 index 000000000000..23dffa6cd365 --- /dev/null +++ b/src/client/activation/serviceRegistry.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { BANNER_NAME_LS_SURVEY, BANNER_NAME_PROPOSE_LS, IPythonExtensionBanner } from '../common/types'; +import { IServiceManager } from '../ioc/types'; +import { LanguageServerSurveyBanner } from '../languageServices/languageServerSurveyBanner'; +import { ProposeLanguageServerBanner } from '../languageServices/proposeLanguageServerBanner'; +import { ExtensionActivationService } from './activationService'; +import { JediExtensionActivator } from './jedi'; +import { LanguageServerExtensionActivator } from './languageServer'; +import { ExtensionActivators, IExtensionActivationService, IExtensionActivator } from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(IExtensionActivationService, ExtensionActivationService); + serviceManager.add(IExtensionActivator, JediExtensionActivator, ExtensionActivators.Jedi); + serviceManager.add(IExtensionActivator, LanguageServerExtensionActivator, ExtensionActivators.DotNet); + serviceManager.addSingleton(IPythonExtensionBanner, LanguageServerSurveyBanner, BANNER_NAME_LS_SURVEY); + serviceManager.addSingleton(IPythonExtensionBanner, ProposeLanguageServerBanner, BANNER_NAME_PROPOSE_LS); +} diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts new file mode 100644 index 000000000000..714f51378251 --- /dev/null +++ b/src/client/activation/types.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export const IExtensionActivationService = Symbol('IExtensionActivationService'); +export interface IExtensionActivationService { + activate(): Promise; +} + +export enum ExtensionActivators { + Jedi = 'Jedi', + DotNet = 'DotNet' +} + +export const IExtensionActivator = Symbol('IExtensionActivator'); +export interface IExtensionActivator { + activate(): Promise; + deactivate(): Promise; +} diff --git a/src/client/application/diagnostics/applicationDiagnostics.ts b/src/client/application/diagnostics/applicationDiagnostics.ts new file mode 100644 index 000000000000..5956cf5d6e5e --- /dev/null +++ b/src/client/application/diagnostics/applicationDiagnostics.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { DiagnosticSeverity } from 'vscode'; +import { STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; +import { ILogger, IOutputChannel } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { IApplicationDiagnostics } from '../types'; +import { EnvironmentPathVariableDiagnosticsServiceId } from './checks/envPathVariable'; +import { IDiagnostic, IDiagnosticsService } from './types'; + +@injectable() +export class ApplicationDiagnostics implements IApplicationDiagnostics { + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { } + public async performPreStartupHealthCheck(): Promise { + const envHealthCheck = this.serviceContainer.get(IDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId); + const diagnostics = await envHealthCheck.diagnose(); + this.log(diagnostics); + if (diagnostics.length > 0) { + await envHealthCheck.handle(diagnostics); + } + } + private log(diagnostics: IDiagnostic[]): void { + const logger = this.serviceContainer.get(ILogger); + const outputChannel = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + diagnostics.forEach(item => { + const message = `Diagnostic Code: ${item.code}, Message: ${item.message}`; + switch (item.severity) { + case DiagnosticSeverity.Error: { + logger.logError(message); + outputChannel.appendLine(message); + break; + } + case DiagnosticSeverity.Warning: { + logger.logWarning(message); + outputChannel.appendLine(message); + break; + } + default: { + logger.logInformation(message); + } + } + }); + } +} diff --git a/src/client/application/diagnostics/base.ts b/src/client/application/diagnostics/base.ts new file mode 100644 index 000000000000..bafb8e84561c --- /dev/null +++ b/src/client/application/diagnostics/base.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable, unmanaged } from 'inversify'; +import { DiagnosticSeverity } from 'vscode'; +import { IServiceContainer } from '../../ioc/types'; +import { DiagnosticScope, IDiagnostic, IDiagnosticFilterService, IDiagnosticsService } from './types'; + +@injectable() +export abstract class BaseDiagnostic implements IDiagnostic { + constructor(public readonly code: string, public readonly message: string, + public readonly severity: DiagnosticSeverity, public readonly scope: DiagnosticScope) { } +} + +@injectable() +export abstract class BaseDiagnosticsService implements IDiagnosticsService { + protected readonly filterService: IDiagnosticFilterService; + constructor(@unmanaged() private readonly supportedDiagnosticCodes: string[], + @unmanaged() protected serviceContainer: IServiceContainer) { + this.filterService = serviceContainer.get(IDiagnosticFilterService); + } + public abstract diagnose(): Promise; + public abstract handle(diagnostics: IDiagnostic[]): Promise; + public async canHandle(diagnostic: IDiagnostic): Promise { + return this.supportedDiagnosticCodes.filter(item => item === diagnostic.code).length > 0; + } +} diff --git a/src/client/application/diagnostics/checks/envPathVariable.ts b/src/client/application/diagnostics/checks/envPathVariable.ts new file mode 100644 index 000000000000..bccb3a29c042 --- /dev/null +++ b/src/client/application/diagnostics/checks/envPathVariable.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { DiagnosticSeverity } from 'vscode'; +import { IApplicationEnvironment } from '../../../common/application/types'; +import '../../../common/extensions'; +import { IPlatformService } from '../../../common/platform/types'; +import { ICurrentProcess, IPathUtils } from '../../../common/types'; +import { IServiceContainer } from '../../../ioc/types'; +import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; +import { IDiagnosticsCommandFactory } from '../commands/types'; +import { DiagnosticCodes } from '../constants'; +import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; +import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; + +const InvalidEnvPathVariableMessage = 'The environment variable \'{0}\' seems to have some paths containing the \'"\' character.' + + ' The existence of such a character is known to have caused the {1} extension to not load. If the extension fails to load please modify your paths to remove this \'"\' character.'; + +export class InvalidEnvironmentPathVariableDiagnostic extends BaseDiagnostic { + constructor(message) { + super(DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic, + message, DiagnosticSeverity.Warning, DiagnosticScope.Global); + } +} + +export const EnvironmentPathVariableDiagnosticsServiceId = 'EnvironmentPathVariableDiagnosticsServiceId'; + +@injectable() +export class EnvironmentPathVariableDiagnosticsService extends BaseDiagnosticsService { + protected readonly messageService: IDiagnosticHandlerService; + private readonly platform: IPlatformService; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super([DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic], serviceContainer); + this.platform = this.serviceContainer.get(IPlatformService); + this.messageService = serviceContainer.get>(IDiagnosticHandlerService, DiagnosticCommandPromptHandlerServiceId); + } + public async diagnose(): Promise { + if (this.platform.isWindows && + this.doesPathVariableHaveInvalidEntries()) { + const env = this.serviceContainer.get(IApplicationEnvironment); + const message = InvalidEnvPathVariableMessage + .format(this.platform.pathVariableName, env.extensionName); + return [new InvalidEnvironmentPathVariableDiagnostic(message)]; + } else { + return []; + } + } + public async handle(diagnostics: IDiagnostic[]): Promise { + // This class can only handle one type of diagnostic, hence just use first item in list. + if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { + return; + } + const diagnostic = diagnostics[0]; + if (await this.filterService.shouldIgnoreDiagnostic(diagnostic.code)) { + return; + } + const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); + const options = [ + { + prompt: 'Ignore' + }, + { + prompt: 'Always Ignore', + command: commandFactory.createCommand(diagnostic, { type: 'ignore', options: DiagnosticScope.Global }) + }, + { + prompt: 'More Info', + command: commandFactory.createCommand(diagnostic, { type: 'launch', options: 'https://aka.ms/Niq35h' }) + } + ]; + + await this.messageService.handle(diagnostic, { commandPrompts: options }); + } + private doesPathVariableHaveInvalidEntries() { + const currentProc = this.serviceContainer.get(ICurrentProcess); + const pathValue = currentProc.env[this.platform.pathVariableName]; + const pathSeparator = this.serviceContainer.get(IPathUtils).delimiter; + const paths = pathValue.split(pathSeparator); + return paths.filter(item => item.indexOf('"') >= 0).length > 0; + } +} diff --git a/src/client/application/diagnostics/commands/base.ts b/src/client/application/diagnostics/commands/base.ts new file mode 100644 index 000000000000..8bbb4cc5f1e4 --- /dev/null +++ b/src/client/application/diagnostics/commands/base.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IDiagnostic, IDiagnosticCommand } from '../types'; + +export abstract class BaseDiagnosticCommand implements IDiagnosticCommand { + constructor(public readonly diagnostic: IDiagnostic) { + } + public abstract invoke(): Promise; +} diff --git a/src/client/application/diagnostics/commands/factory.ts b/src/client/application/diagnostics/commands/factory.ts new file mode 100644 index 000000000000..47d2e63aa0d8 --- /dev/null +++ b/src/client/application/diagnostics/commands/factory.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IServiceContainer } from '../../../ioc/types'; +import { IDiagnostic, IDiagnosticCommand } from '../types'; +import { IgnoreDiagnosticCommand } from './ignore'; +import { LaunchBrowserCommand } from './launchBrowser'; +import { CommandOptions, IDiagnosticsCommandFactory } from './types'; + +@injectable() +export class DiagnosticsCommandFactory implements IDiagnosticsCommandFactory { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } + public createCommand(diagnostic: IDiagnostic, options: CommandOptions): IDiagnosticCommand { + const commandType = options.type; + switch (options.type) { + case 'ignore': { + return new IgnoreDiagnosticCommand(diagnostic, this.serviceContainer, options.options); + } + case 'launch': { + return new LaunchBrowserCommand(diagnostic, this.serviceContainer, options.options); + } + default: { + throw new Error(`Unknown Diagnostic command commandType '${commandType}'`); + } + } + } +} diff --git a/src/client/application/diagnostics/commands/ignore.ts b/src/client/application/diagnostics/commands/ignore.ts new file mode 100644 index 000000000000..18a47bba07ab --- /dev/null +++ b/src/client/application/diagnostics/commands/ignore.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IServiceContainer } from '../../../ioc/types'; +import { DiagnosticScope, IDiagnostic, IDiagnosticFilterService } from '../types'; +import { BaseDiagnosticCommand } from './base'; + +export class IgnoreDiagnosticCommand extends BaseDiagnosticCommand { + constructor(diagnostic: IDiagnostic, private serviceContainer: IServiceContainer, private readonly scope: DiagnosticScope) { + super(diagnostic); + } + public invoke(): Promise { + const filter = this.serviceContainer.get(IDiagnosticFilterService); + return filter.ignoreDiagnostic(this.diagnostic.code, this.scope); + } +} diff --git a/src/client/application/diagnostics/commands/launchBrowser.ts b/src/client/application/diagnostics/commands/launchBrowser.ts new file mode 100644 index 000000000000..55a78a3c0971 --- /dev/null +++ b/src/client/application/diagnostics/commands/launchBrowser.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IBrowserService } from '../../../common/types'; +import { IServiceContainer } from '../../../ioc/types'; +import { IDiagnostic } from '../types'; +import { BaseDiagnosticCommand } from './base'; + +export class LaunchBrowserCommand extends BaseDiagnosticCommand { + constructor(diagnostic: IDiagnostic, private serviceContainer: IServiceContainer, private url: string) { + super(diagnostic); + } + public async invoke(): Promise { + const browser = this.serviceContainer.get(IBrowserService); + return browser.launch(this.url); + } +} diff --git a/src/client/application/diagnostics/commands/types.ts b/src/client/application/diagnostics/commands/types.ts new file mode 100644 index 000000000000..b6f95e2bc769 --- /dev/null +++ b/src/client/application/diagnostics/commands/types.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { DiagnosticScope, IDiagnostic, IDiagnosticCommand } from '../types'; + +export type CommandOption = { type: Type; options: Option }; +export type LaunchBrowserOption = CommandOption<'launch', string>; +export type IgnoreDiagnostOption = CommandOption<'ignore', DiagnosticScope>; +export type CommandOptions = LaunchBrowserOption | IgnoreDiagnostOption; + +export const IDiagnosticsCommandFactory = Symbol('IDiagnosticsCommandFactory'); + +export interface IDiagnosticsCommandFactory { + createCommand(diagnostic: IDiagnostic, options: CommandOptions): IDiagnosticCommand; +} diff --git a/src/client/application/diagnostics/constants.ts b/src/client/application/diagnostics/constants.ts new file mode 100644 index 000000000000..88431d4a855b --- /dev/null +++ b/src/client/application/diagnostics/constants.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export enum DiagnosticCodes { + InvalidEnvironmentPathVariableDiagnostic = 'InvalidEnvironmentPathVariableDiagnostic' +} diff --git a/src/client/application/diagnostics/filter.ts b/src/client/application/diagnostics/filter.ts new file mode 100644 index 000000000000..908c02c61136 --- /dev/null +++ b/src/client/application/diagnostics/filter.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IPersistentStateFactory } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { DiagnosticScope, IDiagnosticFilterService } from './types'; + +export enum FilterKeys { + GlobalDiagnosticFilter = 'GLOBAL_DIAGNOSTICS_FILTER', + WorkspaceDiagnosticFilter = 'WORKSPACE_DIAGNOSTICS_FILTER' +} + +@injectable() +export class DiagnosticFilterService implements IDiagnosticFilterService { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + } + public async shouldIgnoreDiagnostic(code: string): Promise { + const factory = this.serviceContainer.get(IPersistentStateFactory); + const globalState = factory.createGlobalPersistentState(FilterKeys.GlobalDiagnosticFilter, []); + const workspaceState = factory.createWorkspacePersistentState(FilterKeys.WorkspaceDiagnosticFilter, []); + return globalState.value.indexOf(code) >= 0 || + workspaceState.value.indexOf(code) >= 0; + } + public async ignoreDiagnostic(code: string, scope: DiagnosticScope): Promise { + const factory = this.serviceContainer.get(IPersistentStateFactory); + const state = scope === DiagnosticScope.Global ? + factory.createGlobalPersistentState(FilterKeys.GlobalDiagnosticFilter, []) : + factory.createWorkspacePersistentState(FilterKeys.WorkspaceDiagnosticFilter, []); + + const currentValue = state.value.slice(); + await state.updateValue(currentValue.concat(code)); + } +} diff --git a/src/client/application/diagnostics/promptHandler.ts b/src/client/application/diagnostics/promptHandler.ts new file mode 100644 index 000000000000..939f7074ec49 --- /dev/null +++ b/src/client/application/diagnostics/promptHandler.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { DiagnosticSeverity } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { IServiceContainer } from '../../ioc/types'; +import { IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerService } from './types'; + +export type MessageCommandPrompt = { + commandPrompts: { + prompt: string; + command?: IDiagnosticCommand; + }[]; + message?: string; +}; + +export const DiagnosticCommandPromptHandlerServiceId = 'DiagnosticCommandPromptHandlerServiceId'; + +@injectable() +export class DiagnosticCommandPromptHandlerService implements IDiagnosticHandlerService { + private readonly appShell: IApplicationShell; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.appShell = serviceContainer.get(IApplicationShell); + } + public async handle(diagnostic: IDiagnostic, options: MessageCommandPrompt = { commandPrompts: [] }): Promise { + const prompts = options.commandPrompts.map(option => option.prompt); + const response = await this.displayMessage(options.message ? options.message : diagnostic.message, diagnostic.severity, prompts); + if (!response) { + return; + } + const selectedOption = options.commandPrompts.find(option => option.prompt === response); + if (selectedOption && selectedOption.command) { + await selectedOption.command.invoke(); + } + } + private async displayMessage(message: string, severity: DiagnosticSeverity, prompts: string[]): Promise { + switch (severity) { + case DiagnosticSeverity.Error: { + return this.appShell.showErrorMessage(message, ...prompts); + } + case DiagnosticSeverity.Warning: { + return this.appShell.showWarningMessage(message, ...prompts); + } + default: { + return this.appShell.showInformationMessage(message, ...prompts); + } + } + } +} diff --git a/src/client/application/diagnostics/serviceRegistry.ts b/src/client/application/diagnostics/serviceRegistry.ts new file mode 100644 index 000000000000..6554a6ddf37b --- /dev/null +++ b/src/client/application/diagnostics/serviceRegistry.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IServiceManager } from '../../ioc/types'; +import { EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId } from './checks/envPathVariable'; +import { DiagnosticsCommandFactory } from './commands/factory'; +import { IDiagnosticsCommandFactory } from './commands/types'; +import { DiagnosticFilterService } from './filter'; +import { DiagnosticCommandPromptHandlerService, DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from './promptHandler'; +import { IDiagnosticFilterService, IDiagnosticHandlerService, IDiagnosticsService } from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(IDiagnosticFilterService, DiagnosticFilterService); + serviceManager.addSingleton>(IDiagnosticHandlerService, DiagnosticCommandPromptHandlerService, DiagnosticCommandPromptHandlerServiceId); + serviceManager.addSingleton(IDiagnosticsService, EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId); + serviceManager.addSingleton(IDiagnosticsCommandFactory, DiagnosticsCommandFactory); +} diff --git a/src/client/application/diagnostics/types.ts b/src/client/application/diagnostics/types.ts new file mode 100644 index 000000000000..41f5940c8c63 --- /dev/null +++ b/src/client/application/diagnostics/types.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { DiagnosticSeverity } from 'vscode'; + +export enum DiagnosticScope { + Global = 'Global', + WorkspaceFolder = 'WorkspaceFolder' +} + +export interface IDiagnostic { + readonly code: string; + readonly message: string; + readonly severity: DiagnosticSeverity; + readonly scope: DiagnosticScope; +} + +export const IDiagnosticsService = Symbol('IDiagnosticsService'); + +export interface IDiagnosticsService { + diagnose(): Promise; + canHandle(diagnostic: IDiagnostic): Promise; + handle(diagnostics: IDiagnostic[]): Promise; +} + +export const IDiagnosticFilterService = Symbol('IDiagnosticFilterService'); + +export interface IDiagnosticFilterService { + shouldIgnoreDiagnostic(code: string): Promise; + ignoreDiagnostic(code: string, scope: DiagnosticScope): Promise; +} + +export const IDiagnosticHandlerService = Symbol('IDiagnosticHandlerService'); + +export interface IDiagnosticHandlerService { + handle(diagnostic: IDiagnostic, options?: T): Promise; +} + +export interface IDiagnosticCommand { + readonly diagnostic: IDiagnostic; + invoke(): Promise; +} diff --git a/src/client/application/serviceRegistry.ts b/src/client/application/serviceRegistry.ts new file mode 100644 index 000000000000..7e4b561b4995 --- /dev/null +++ b/src/client/application/serviceRegistry.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IServiceManager } from '../ioc/types'; +import { ApplicationDiagnostics } from './diagnostics/applicationDiagnostics'; +import { registerTypes as diagnosticsRegisterTypes } from './diagnostics/serviceRegistry'; +import { IApplicationDiagnostics } from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(IApplicationDiagnostics, ApplicationDiagnostics); + diagnosticsRegisterTypes(serviceManager); +} diff --git a/src/client/application/types.ts b/src/client/application/types.ts new file mode 100644 index 000000000000..7ceaf387f94d --- /dev/null +++ b/src/client/application/types.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export const IApplicationDiagnostics = Symbol('IApplicationDiagnostics'); + +export interface IApplicationDiagnostics { + /** + * Perform pre-extension activation health checks. + * E.g. validate user environment, etc. + * @returns {Promise} + * @memberof IApplicationDiagnostics + */ + performPreStartupHealthCheck(): Promise; +} diff --git a/src/client/common/application/applicationEnvironment.ts b/src/client/common/application/applicationEnvironment.ts new file mode 100644 index 000000000000..a26eb2eede00 --- /dev/null +++ b/src/client/common/application/applicationEnvironment.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../constants'; +import { IApplicationEnvironment } from './types'; + +@injectable() +export class ApplicationEnvironment implements IApplicationEnvironment { + public get appName(): string { + return vscode.env.appName; + } + public get appRoot(): string { + return vscode.env.appRoot; + } + public get language(): string { + return vscode.env.language; + } + public get sessionId(): string { + return vscode.env.sessionId; + } + public get machineId(): string { + return vscode.env.machineId; + } + public get extensionName(): string { + // tslint:disable-next-line:non-literal-require + return require(path.join(EXTENSION_ROOT_DIR, 'package.json')).displayName; + } +} diff --git a/src/client/common/application/applicationShell.ts b/src/client/common/application/applicationShell.ts index ac3330651b55..e956becd2204 100644 --- a/src/client/common/application/applicationShell.ts +++ b/src/client/common/application/applicationShell.ts @@ -6,50 +6,49 @@ const opn = require('opn'); import { injectable } from 'inversify'; -import * as vscode from 'vscode'; -import { Disposable, StatusBarAlignment, StatusBarItem, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; +import { CancellationToken, Disposable, InputBoxOptions, MessageItem, MessageOptions, OpenDialogOptions, QuickPickItem, QuickPickOptions, SaveDialogOptions, StatusBarAlignment, StatusBarItem, Uri, window, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; import { IApplicationShell } from './types'; @injectable() export class ApplicationShell implements IApplicationShell { public showInformationMessage(message: string, ...items: string[]): Thenable; - public showInformationMessage(message: string, options: vscode.MessageOptions, ...items: string[]): Thenable; - public showInformationMessage(message: string, ...items: T[]): Thenable; - public showInformationMessage(message: string, options: vscode.MessageOptions, ...items: T[]): Thenable; + public showInformationMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; + public showInformationMessage(message: string, ...items: T[]): Thenable; + public showInformationMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; public showInformationMessage(message: string, options?: any, ...items: any[]): Thenable { - return vscode.window.showInformationMessage(message, options, ...items); + return window.showInformationMessage(message, options, ...items); } public showWarningMessage(message: string, ...items: string[]): Thenable; - public showWarningMessage(message: string, options: vscode.MessageOptions, ...items: string[]): Thenable; - public showWarningMessage(message: string, ...items: T[]): Thenable; - public showWarningMessage(message: string, options: vscode.MessageOptions, ...items: T[]): Thenable; + public showWarningMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; + public showWarningMessage(message: string, ...items: T[]): Thenable; + public showWarningMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; public showWarningMessage(message: any, options?: any, ...items: any[]) { - return vscode.window.showWarningMessage(message, options, ...items); + return window.showWarningMessage(message, options, ...items); } public showErrorMessage(message: string, ...items: string[]): Thenable; - public showErrorMessage(message: string, options: vscode.MessageOptions, ...items: string[]): Thenable; - public showErrorMessage(message: string, ...items: T[]): Thenable; - public showErrorMessage(message: string, options: vscode.MessageOptions, ...items: T[]): Thenable; + public showErrorMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; + public showErrorMessage(message: string, ...items: T[]): Thenable; + public showErrorMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; public showErrorMessage(message: any, options?: any, ...items: any[]) { - return vscode.window.showErrorMessage(message, options, ...items); + return window.showErrorMessage(message, options, ...items); } - public showQuickPick(items: string[] | Thenable, options?: vscode.QuickPickOptions, token?: vscode.CancellationToken): Thenable; - public showQuickPick(items: T[] | Thenable, options?: vscode.QuickPickOptions, token?: vscode.CancellationToken): Thenable; - public showQuickPick(items: any, options?: any, token?: any) { - return vscode.window.showQuickPick(items, options, token); + public showQuickPick(items: string[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; + public showQuickPick(items: T[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; + public showQuickPick(items: any, options?: any, token?: any): Thenable { + return window.showQuickPick(items, options, token); } - public showOpenDialog(options: vscode.OpenDialogOptions): Thenable { - return vscode.window.showOpenDialog(options); + public showOpenDialog(options: OpenDialogOptions): Thenable { + return window.showOpenDialog(options); } - public showSaveDialog(options: vscode.SaveDialogOptions): Thenable { - return vscode.window.showSaveDialog(options); + public showSaveDialog(options: SaveDialogOptions): Thenable { + return window.showSaveDialog(options); } - public showInputBox(options?: vscode.InputBoxOptions, token?: vscode.CancellationToken): Thenable { - return vscode.window.showInputBox(options, token); + public showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable { + return window.showInputBox(options, token); } public openUrl(url: string): void { opn(url); @@ -59,14 +58,14 @@ export class ApplicationShell implements IApplicationShell { public setStatusBarMessage(text: string, hideWhenDone: Thenable): Disposable; public setStatusBarMessage(text: string): Disposable; public setStatusBarMessage(text: string, arg?: any): Disposable { - return vscode.window.setStatusBarMessage(text, arg); + return window.setStatusBarMessage(text, arg); } public createStatusBarItem(alignment?: StatusBarAlignment, priority?: number): StatusBarItem { - return vscode.window.createStatusBarItem(alignment, priority); + return window.createStatusBarItem(alignment, priority); } public showWorkspaceFolderPick(options?: WorkspaceFolderPickOptions): Thenable { - return vscode.window.showWorkspaceFolderPick(options); + return window.showWorkspaceFolderPick(options); } } diff --git a/src/client/common/application/debugService.ts b/src/client/common/application/debugService.ts new file mode 100644 index 000000000000..33d8cad5e1c7 --- /dev/null +++ b/src/client/common/application/debugService.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { debug, DebugConfiguration, DebugSession, Event, WorkspaceFolder } from 'vscode'; +import { IDebugService } from './types'; + +@injectable() +export class DebugService implements IDebugService { + public get onDidStartDebugSession(): Event { + return debug.onDidStartDebugSession; + } + public get onDidTerminateDebugSession(): Event { + return debug.onDidTerminateDebugSession; + } + public startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration): Thenable { + return debug.startDebugging(folder, nameOrConfiguration); + } +} diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 1808e7c220aa..3943d9cb7504 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -4,12 +4,13 @@ // tslint:disable:no-any unified-signatures -import * as vscode from 'vscode'; -import { CancellationToken, ConfigurationChangeEvent, Disposable, Event, FileSystemWatcher, GlobPattern, TextDocument, TextDocumentShowOptions, WorkspaceConfiguration, WorkspaceFolderPickOptions } from 'vscode'; -import { TextEditor, TextEditorEdit, TextEditorOptionsChangeEvent, TextEditorSelectionChangeEvent, TextEditorViewColumnChangeEvent } from 'vscode'; -import { StatusBarAlignment, StatusBarItem } from 'vscode'; -import { Uri, ViewColumn, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; -import { Terminal, TerminalOptions } from 'vscode'; +import { + CancellationToken, ConfigurationChangeEvent, DebugConfiguration, DebugSession, Disposable, Event, FileSystemWatcher, GlobPattern, InputBoxOptions, MessageItem, + MessageOptions, OpenDialogOptions, QuickPickItem, QuickPickOptions, SaveDialogOptions, + StatusBarAlignment, StatusBarItem, Terminal, TerminalOptions, TextDocument, TextDocumentShowOptions, TextEditor, + TextEditorEdit, TextEditorOptionsChangeEvent, TextEditorSelectionChangeEvent, TextEditorViewColumnChangeEvent, Uri, ViewColumn, WorkspaceConfiguration, WorkspaceFolder, + WorkspaceFolderPickOptions, WorkspaceFoldersChangeEvent +} from 'vscode'; export const IApplicationShell = Symbol('IApplicationShell'); export interface IApplicationShell { @@ -24,7 +25,7 @@ export interface IApplicationShell { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - showInformationMessage(message: string, options: vscode.MessageOptions, ...items: string[]): Thenable; + showInformationMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; /** * Show an information message. @@ -35,7 +36,7 @@ export interface IApplicationShell { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - showInformationMessage(message: string, ...items: T[]): Thenable; + showInformationMessage(message: string, ...items: T[]): Thenable; /** * Show an information message. @@ -47,7 +48,7 @@ export interface IApplicationShell { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - showInformationMessage(message: string, options: vscode.MessageOptions, ...items: T[]): Thenable; + showInformationMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; /** * Show a warning message. @@ -70,7 +71,7 @@ export interface IApplicationShell { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - showWarningMessage(message: string, options: vscode.MessageOptions, ...items: string[]): Thenable; + showWarningMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; /** * Show a warning message. @@ -81,7 +82,7 @@ export interface IApplicationShell { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - showWarningMessage(message: string, ...items: T[]): Thenable; + showWarningMessage(message: string, ...items: T[]): Thenable; /** * Show a warning message. @@ -93,7 +94,7 @@ export interface IApplicationShell { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - showWarningMessage(message: string, options: vscode.MessageOptions, ...items: T[]): Thenable; + showWarningMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; /** * Show an error message. @@ -116,7 +117,7 @@ export interface IApplicationShell { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - showErrorMessage(message: string, options: vscode.MessageOptions, ...items: string[]): Thenable; + showErrorMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; /** * Show an error message. @@ -127,7 +128,7 @@ export interface IApplicationShell { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - showErrorMessage(message: string, ...items: T[]): Thenable; + showErrorMessage(message: string, ...items: T[]): Thenable; /** * Show an error message. @@ -139,7 +140,7 @@ export interface IApplicationShell { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - showErrorMessage(message: string, options: vscode.MessageOptions, ...items: T[]): Thenable; + showErrorMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; /** * Shows a selection list. @@ -149,7 +150,7 @@ export interface IApplicationShell { * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selection or `undefined`. */ - showQuickPick(items: string[] | Thenable, options?: vscode.QuickPickOptions, token?: vscode.CancellationToken): Thenable; + showQuickPick(items: string[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; /** * Shows a selection list. @@ -159,7 +160,7 @@ export interface IApplicationShell { * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selected item or `undefined`. */ - showQuickPick(items: T[] | Thenable, options?: vscode.QuickPickOptions, token?: vscode.CancellationToken): Thenable; + showQuickPick(items: T[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; /** * Shows a file open dialog to the user which allows to select a file @@ -168,7 +169,7 @@ export interface IApplicationShell { * @param options Options that control the dialog. * @returns A promise that resolves to the selected resources or `undefined`. */ - showOpenDialog(options: vscode.OpenDialogOptions): Thenable; + showOpenDialog(options: OpenDialogOptions): Thenable; /** * Shows a file save dialog to the user which allows to select a file @@ -177,7 +178,7 @@ export interface IApplicationShell { * @param options Options that control the dialog. * @returns A promise that resolves to the selected resource or `undefined`. */ - showSaveDialog(options: vscode.SaveDialogOptions): Thenable; + showSaveDialog(options: SaveDialogOptions): Thenable; /** * Opens an input box to ask the user for input. @@ -190,7 +191,7 @@ export interface IApplicationShell { * @param token A token that can be used to signal cancellation. * @return A promise that resolves to a string the user provided or to `undefined` in case of dismissal. */ - showInputBox(options?: vscode.InputBoxOptions, token?: vscode.CancellationToken): Thenable; + showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable; /** * Opens URL in a default browser. @@ -406,7 +407,7 @@ export interface IDocumentManager { showTextDocument(uri: Uri, options?: TextDocumentShowOptions): Thenable; } -export const IWorkspaceService = Symbol('IWorkspace'); +export const IWorkspaceService = Symbol('IWorkspaceService'); export interface IWorkspaceService { /** @@ -434,6 +435,12 @@ export interface IWorkspaceService { * An event that is emitted when the [configuration](#WorkspaceConfiguration) changed. */ readonly onDidChangeConfiguration: Event; + /** + * Whether a workspace folder exists + * @type {boolean} + * @memberof IWorkspaceService + */ + readonly hasWorkspaceFolders: boolean; /** * Returns the [workspace folder](#WorkspaceFolder) that contains a given uri. @@ -524,3 +531,74 @@ export interface ITerminalManager { */ createTerminal(options: TerminalOptions): Terminal; } + +export const IDebugService = Symbol('IDebugManager'); + +export interface IDebugService { + /** + * An [event](#Event) which fires when a new [debug session](#DebugSession) has been started. + */ + onDidStartDebugSession: Event; + + /** + * An [event](#Event) which fires when a [debug session](#DebugSession) has terminated. + */ + onDidTerminateDebugSession: Event; + /** + * Start debugging by using either a named launch or named compound configuration, + * or by directly passing a [DebugConfiguration](#DebugConfiguration). + * The named configurations are looked up in '.vscode/launch.json' found in the given folder. + * Before debugging starts, all unsaved files are saved and the launch configurations are brought up-to-date. + * Folder specific variables used in the configuration (e.g. '${workspaceFolder}') are resolved against the given folder. + * @param folder The [workspace folder](#WorkspaceFolder) for looking up named configurations and resolving variables or `undefined` for a non-folder setup. + * @param nameOrConfiguration Either the name of a debug or compound configuration or a [DebugConfiguration](#DebugConfiguration) object. + * @return A thenable that resolves when debugging could be successfully started. + */ + startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration): Thenable; +} + +export const IApplicationEnvironment = Symbol('IApplicationEnvironment'); +export interface IApplicationEnvironment { + /** + * The application name of the editor, like 'VS Code'. + * + * @readonly + */ + appName: string; + + /** + * The extension name. + * + * @readonly + */ + extensionName: string; + + /** + * The application root folder from which the editor is running. + * + * @readonly + */ + appRoot: string; + + /** + * Represents the preferred user-language, like `de-CH`, `fr`, or `en-US`. + * + * @readonly + */ + language: string; + + /** + * A unique identifier for the computer. + * + * @readonly + */ + machineId: string; + + /** + * A unique identifier for the current session. + * Changes each time the editor is started. + * + * @readonly + */ + sessionId: string; +} diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts index 7475ab77c94f..08bd950fe655 100644 --- a/src/client/common/application/workspace.ts +++ b/src/client/common/application/workspace.ts @@ -20,6 +20,9 @@ export class WorkspaceService implements IWorkspaceService { public get onDidChangeWorkspaceFolders(): vscode.Event { return vscode.workspace.onDidChangeWorkspaceFolders; } + public get hasWorkspaceFolders() { + return Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0; + } public getConfiguration(section?: string, resource?: vscode.Uri): vscode.WorkspaceConfiguration { return vscode.workspace.getConfiguration(section, resource); } diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 9da482ded147..26311e9657be 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -1,376 +1,400 @@ -'use strict'; - -import * as child_process from 'child_process'; -import { EventEmitter } from 'events'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { ConfigurationTarget, Uri } from 'vscode'; -import { isTestExecution } from './constants'; -import { - IAutoCompeteSettings, - IFormattingSettings, - ILintingSettings, - IPythonSettings, - ISortImportSettings, - ITerminalSettings, - IUnitTestSettings, - IWorkspaceSymbolSettings -} from './types'; -import { SystemVariables } from './variables/systemVariables'; - -// tslint:disable-next-line:no-require-imports no-var-requires -const untildify = require('untildify'); - -export const IS_WINDOWS = /^win/.test(process.platform); - -// tslint:disable-next-line:completed-docs -export class PythonSettings extends EventEmitter implements IPythonSettings { - private static pythonSettings: Map = new Map(); - - public jediPath: string; - public jediMemoryLimit: number; - public envFile: string; - public disablePromptForFeatures: string[]; - public venvPath: string; - public venvFolders: string[]; - public devOptions: string[]; - public linting: ILintingSettings; - public formatting: IFormattingSettings; - public autoComplete: IAutoCompeteSettings; - public unitTest: IUnitTestSettings; - public terminal: ITerminalSettings; - public sortImports: ISortImportSettings; - public workspaceSymbols: IWorkspaceSymbolSettings; - public disableInstallationChecks: boolean; - public globalModuleInstallation: boolean; - - 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); - this.disposables.push(vscode.workspace.onDidChangeConfiguration(() => { - this.initializeSettings(); - })); - - this.initializeSettings(); - } - // tslint:disable-next-line:function-name - public static getInstance(resource?: Uri): PythonSettings { - const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource).uri; - const workspaceFolderKey = workspaceFolderUri ? workspaceFolderUri.fsPath : ''; - - if (!PythonSettings.pythonSettings.has(workspaceFolderKey)) { - const settings = new PythonSettings(workspaceFolderUri); - PythonSettings.pythonSettings.set(workspaceFolderKey, settings); - } - // tslint:disable-next-line:no-non-null-assertion - return PythonSettings.pythonSettings.get(workspaceFolderKey)!; - } - - public static getSettingsUriAndTarget(resource?: Uri): { uri: Uri | undefined, target: ConfigurationTarget } { - const workspaceFolder = resource ? vscode.workspace.getWorkspaceFolder(resource) : 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; - } - - const target = workspaceFolderUri ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Global; - return { uri: workspaceFolderUri, target }; - } - - // tslint:disable-next-line:function-name - public static dispose() { - if (!isTestExecution()) { - 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 = []; - } - - // tslint:disable-next-line:cyclomatic-complexity max-func-body-length - private initializeSettings() { - 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, workspaceRoot); - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - this.venvPath = systemVariables.resolveAny(pythonSettings.get('venvPath'))!; - this.venvFolders = systemVariables.resolveAny(pythonSettings.get('venvFolders'))!; - // 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), workspaceRoot); - } else { - this.jediPath = ''; - } - this.jediMemoryLimit = pythonSettings.get('jediMemoryLimit')!; - - // 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 : []; - // 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 { - this.linting = lintingSettings; - } - - this.disableInstallationChecks = pythonSettings.get('disableInstallationCheck') === true; - this.globalModuleInstallation = pythonSettings.get('globalModuleInstallation') === true; - - // 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 { - this.sortImports = sortImportSettings; - } - // Support for travis. - this.sortImports = this.sortImports ? this.sortImports : { path: '', args: [] }; - // Support for travis. - this.linting = this.linting ? this.linting : { - enabled: false, - ignorePatterns: [], - flake8Args: [], flake8Enabled: false, flake8Path: 'flake', - lintOnSave: false, maxNumberOfProblems: 100, - mypyArgs: [], mypyEnabled: false, mypyPath: 'mypy', - pep8Args: [], pep8Enabled: false, pep8Path: 'pep8', - pylamaArgs: [], pylamaEnabled: false, pylamaPath: 'pylama', - prospectorArgs: [], prospectorEnabled: false, prospectorPath: 'prospector', - pydocstyleArgs: [], pydocstyleEnabled: false, pydocstylePath: 'pydocstyle', - pylintArgs: [], pylintEnabled: false, pylintPath: 'pylint', - pylintCategorySeverity: { - convention: vscode.DiagnosticSeverity.Hint, - error: vscode.DiagnosticSeverity.Error, - fatal: vscode.DiagnosticSeverity.Error, - refactor: vscode.DiagnosticSeverity.Hint, - warning: vscode.DiagnosticSeverity.Warning - }, - pep8CategorySeverity: { - E: vscode.DiagnosticSeverity.Error, - W: vscode.DiagnosticSeverity.Warning - }, - flake8CategorySeverity: { - E: vscode.DiagnosticSeverity.Error, - W: vscode.DiagnosticSeverity.Warning, - // Per http://flake8.pycqa.org/en/latest/glossary.html#term-error-code - // 'F' does not mean 'fatal as in PyLint but rather 'pyflakes' such as - // unused imports, variables, etc. - F: vscode.DiagnosticSeverity.Warning - }, - mypyCategorySeverity: { - error: vscode.DiagnosticSeverity.Error, - note: vscode.DiagnosticSeverity.Hint - }, - pylintUseMinimalCheckers: false - }; - this.linting.pylintPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylintPath), workspaceRoot); - this.linting.flake8Path = getAbsolutePath(systemVariables.resolveAny(this.linting.flake8Path), workspaceRoot); - this.linting.pep8Path = getAbsolutePath(systemVariables.resolveAny(this.linting.pep8Path), workspaceRoot); - this.linting.pylamaPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylamaPath), workspaceRoot); - this.linting.prospectorPath = getAbsolutePath(systemVariables.resolveAny(this.linting.prospectorPath), workspaceRoot); - this.linting.pydocstylePath = getAbsolutePath(systemVariables.resolveAny(this.linting.pydocstylePath), workspaceRoot); - this.linting.mypyPath = getAbsolutePath(systemVariables.resolveAny(this.linting.mypyPath), workspaceRoot); - - // 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 { - this.formatting = formattingSettings; - } - // Support for travis. - this.formatting = this.formatting ? this.formatting : { - autopep8Args: [], autopep8Path: 'autopep8', - provider: 'autopep8', - yapfArgs: [], yapfPath: 'yapf' - }; - this.formatting.autopep8Path = getAbsolutePath(systemVariables.resolveAny(this.formatting.autopep8Path), workspaceRoot); - this.formatting.yapfPath = getAbsolutePath(systemVariables.resolveAny(this.formatting.yapfPath), workspaceRoot); - - // 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 { - this.autoComplete = autoCompleteSettings; - } - // Support for travis. - this.autoComplete = this.autoComplete ? this.autoComplete : { - extraPaths: [], - addBrackets: false, - preloadModules: [] - }; - - // 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 { - this.workspaceSymbols = workspaceSymbolsSettings; - } - // Support for travis. - this.workspaceSymbols = this.workspaceSymbols ? this.workspaceSymbols : { - ctagsPath: 'ctags', - enabled: true, - exclusionPatterns: [], - rebuildOnFileSave: true, - rebuildOnStart: true, - tagFilePath: path.join(workspaceRoot, 'tags') - }; - this.workspaceSymbols.tagFilePath = getAbsolutePath(systemVariables.resolveAny(this.workspaceSymbols.tagFilePath), workspaceRoot); - - // 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 { - this.unitTest = unitTestSettings; - if (isTestExecution() && !this.unitTest) { - // tslint:disable-next-line:prefer-type-cast - this.unitTest = { - nosetestArgs: [], pyTestArgs: [], unittestArgs: [], - promptToConfigure: true, debugPort: 3000, - nosetestsEnabled: false, pyTestEnabled: false, unittestEnabled: false, - nosetestPath: 'nosetests', pyTestPath: 'pytest' - } as IUnitTestSettings; - } - } - - // Support for travis. - this.unitTest = this.unitTest ? this.unitTest : { - promptToConfigure: true, - debugPort: 3000, - nosetestArgs: [], nosetestPath: 'nosetest', nosetestsEnabled: false, - pyTestArgs: [], pyTestEnabled: false, pyTestPath: 'pytest', - unittestArgs: [], unittestEnabled: false - }; - this.unitTest.pyTestPath = getAbsolutePath(systemVariables.resolveAny(this.unitTest.pyTestPath), workspaceRoot); - this.unitTest.nosetestPath = getAbsolutePath(systemVariables.resolveAny(this.unitTest.nosetestPath), workspaceRoot); - if (this.unitTest.cwd) { - this.unitTest.cwd = getAbsolutePath(systemVariables.resolveAny(this.unitTest.cwd), workspaceRoot); - } - - // 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)); - - // 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 { - this.terminal = terminalSettings; - if (isTestExecution() && !this.terminal) { - // tslint:disable-next-line:prefer-type-cast - this.terminal = {} as ITerminalSettings; - } - } - // Support for travis. - this.terminal = this.terminal ? this.terminal : { - executeInFileDir: true, - launchArgs: [], - activateEnvironment: true - }; - - // 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); - } - - public get pythonPath(): string { - return this._pythonPath; - } - public set pythonPath(value: string) { - if (this._pythonPath === value) { - return; - } - // 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) { - this._pythonPath = value; - } - } -} - -function getAbsolutePath(pathToCheck: string, rootDir: string): string { - // tslint:disable-next-line:prefer-type-cast no-unsafe-any - pathToCheck = untildify(pathToCheck) as string; - if (isTestExecution() && !pathToCheck) { return rootDir; } - if (pathToCheck.indexOf(path.sep) === -1) { - return pathToCheck; - } - return path.isAbsolute(pathToCheck) ? pathToCheck : path.resolve(rootDir, pathToCheck); -} - -function getPythonExecutable(pythonPath: string): string { - // tslint:disable-next-line:prefer-type-cast no-unsafe-any - pythonPath = untildify(pythonPath) as string; - - // If only 'python'. - if (pythonPath === 'python' || - pythonPath.indexOf(path.sep) === -1 || - path.basename(pythonPath) === path.dirname(pythonPath)) { - return pythonPath; - } - - if (isValidPythonPath(pythonPath)) { - return pythonPath; - } - // 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'. - if (IS_WINDOWS) { - 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 { - if (isValidPythonPath(path.join(pythonPath, executableName))) { - return path.join(pythonPath, executableName); - } - if (isValidPythonPath(path.join(pythonPath, 'bin', executableName))) { - return path.join(pythonPath, 'bin', executableName); - } - } - } - - return pythonPath; -} - -function isValidPythonPath(pythonPath: string): boolean { - try { - const output = child_process.execFileSync(pythonPath, ['-c', 'print(1234)'], { encoding: 'utf8' }); - return output.startsWith('1234'); - } catch (ex) { - return false; - } -} +'use strict'; + +import * as child_process from 'child_process'; +import { EventEmitter } from 'events'; +import * as path from 'path'; +import { ConfigurationTarget, DiagnosticSeverity, Disposable, Uri, workspace } from 'vscode'; +import { sendTelemetryEvent } from '../telemetry'; +import { COMPLETION_ADD_BRACKETS, FORMAT_ON_TYPE } from '../telemetry/constants'; +import { isTestExecution } from './constants'; +import { + IAnalysisSettings, + IAutoCompleteSettings, + IFormattingSettings, + ILintingSettings, + IPythonSettings, + ISortImportSettings, + ITerminalSettings, + IUnitTestSettings, + IWorkspaceSymbolSettings +} from './types'; +import { SystemVariables } from './variables/systemVariables'; + +// tslint:disable-next-line:no-require-imports no-var-requires +const untildify = require('untildify'); + +export const IS_WINDOWS = /^win/.test(process.platform); + +// tslint:disable-next-line:completed-docs +export class PythonSettings extends EventEmitter implements IPythonSettings { + private static pythonSettings: Map = new Map(); + public downloadLanguageServer = true; + public jediEnabled = true; + public jediPath = ''; + public jediMemoryLimit = 1024; + public envFile = ''; + public venvPath = ''; + public venvFolders: string[] = []; + public devOptions: string[] = []; + public linting!: ILintingSettings; + public formatting!: IFormattingSettings; + public autoComplete!: IAutoCompleteSettings; + public unitTest!: IUnitTestSettings; + public terminal!: ITerminalSettings; + public sortImports!: ISortImportSettings; + public workspaceSymbols!: IWorkspaceSymbolSettings; + public disableInstallationChecks = false; + public globalModuleInstallation = false; + public analysis!: IAnalysisSettings; + + private workspaceRoot: Uri; + private disposables: Disposable[] = []; + // tslint:disable-next-line:variable-name + private _pythonPath = ''; + + constructor(workspaceFolder?: Uri) { + super(); + this.workspaceRoot = workspaceFolder ? workspaceFolder : Uri.file(__dirname); + this.disposables.push(workspace.onDidChangeConfiguration(() => { + this.initializeSettings(); + + // 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); + })); + + this.initializeSettings(); + } + // tslint:disable-next-line:function-name + public static getInstance(resource?: Uri): PythonSettings { + const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource).uri; + const workspaceFolderKey = workspaceFolderUri ? workspaceFolderUri.fsPath : ''; + + if (!PythonSettings.pythonSettings.has(workspaceFolderKey)) { + const settings = new PythonSettings(workspaceFolderUri); + PythonSettings.pythonSettings.set(workspaceFolderKey, settings); + const formatOnType = workspace.getConfiguration('editor', resource ? resource : null).get('formatOnType', false); + sendTelemetryEvent(COMPLETION_ADD_BRACKETS, undefined, { enabled: settings.autoComplete.addBrackets }); + sendTelemetryEvent(FORMAT_ON_TYPE, undefined, { enabled: formatOnType }); + } + // tslint:disable-next-line:no-non-null-assertion + return PythonSettings.pythonSettings.get(workspaceFolderKey)!; + } + + // tslint:disable-next-line:type-literal-delimiter + public static getSettingsUriAndTarget(resource?: Uri): { uri: Uri | undefined, target: ConfigurationTarget } { + const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; + let workspaceFolderUri: Uri | undefined = workspaceFolder ? workspaceFolder.uri : undefined; + + if (!workspaceFolderUri && Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + workspaceFolderUri = workspace.workspaceFolders[0].uri; + } + + const target = workspaceFolderUri ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Global; + return { uri: workspaceFolderUri, target }; + } + + // tslint:disable-next-line:function-name + public static dispose() { + if (!isTestExecution()) { + 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 = []; + } + // tslint:disable-next-line:cyclomatic-complexity max-func-body-length + private initializeSettings() { + const workspaceRoot = this.workspaceRoot.fsPath; + const systemVariables: SystemVariables = new SystemVariables(this.workspaceRoot ? this.workspaceRoot.fsPath : undefined); + const pythonSettings = 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, workspaceRoot); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + this.venvPath = systemVariables.resolveAny(pythonSettings.get('venvPath'))!; + this.venvFolders = systemVariables.resolveAny(pythonSettings.get('venvFolders'))!; + + this.downloadLanguageServer = systemVariables.resolveAny(pythonSettings.get('downloadLanguageServer', true))!; + this.jediEnabled = systemVariables.resolveAny(pythonSettings.get('jediEnabled', true))!; + if (this.jediEnabled) { + // 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), workspaceRoot); + } else { + this.jediPath = ''; + } + this.jediMemoryLimit = pythonSettings.get('jediMemoryLimit')!; + } + + // 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 : []; + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const lintingSettings = systemVariables.resolveAny(pythonSettings.get('linting'))!; + if (this.linting) { + Object.assign(this.linting, lintingSettings); + } else { + this.linting = lintingSettings; + } + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const analysisSettings = systemVariables.resolveAny(pythonSettings.get('analysis'))!; + if (this.analysis) { + Object.assign(this.analysis, analysisSettings); + } else { + this.analysis = analysisSettings; + } + + this.disableInstallationChecks = pythonSettings.get('disableInstallationCheck') === true; + this.globalModuleInstallation = pythonSettings.get('globalModuleInstallation') === true; + + // 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 { + this.sortImports = sortImportSettings; + } + // Support for travis. + this.sortImports = this.sortImports ? this.sortImports : { path: '', args: [] }; + // Support for travis. + this.linting = this.linting ? this.linting : { + enabled: false, + ignorePatterns: [], + flake8Args: [], flake8Enabled: false, flake8Path: 'flake', + lintOnSave: false, maxNumberOfProblems: 100, + mypyArgs: [], mypyEnabled: false, mypyPath: 'mypy', + pep8Args: [], pep8Enabled: false, pep8Path: 'pep8', + pylamaArgs: [], pylamaEnabled: false, pylamaPath: 'pylama', + prospectorArgs: [], prospectorEnabled: false, prospectorPath: 'prospector', + pydocstyleArgs: [], pydocstyleEnabled: false, pydocstylePath: 'pydocstyle', + pylintArgs: [], pylintEnabled: false, pylintPath: 'pylint', + pylintCategorySeverity: { + convention: DiagnosticSeverity.Hint, + error: DiagnosticSeverity.Error, + fatal: DiagnosticSeverity.Error, + refactor: DiagnosticSeverity.Hint, + warning: DiagnosticSeverity.Warning + }, + pep8CategorySeverity: { + E: DiagnosticSeverity.Error, + W: DiagnosticSeverity.Warning + }, + flake8CategorySeverity: { + E: DiagnosticSeverity.Error, + W: DiagnosticSeverity.Warning, + // Per http://flake8.pycqa.org/en/latest/glossary.html#term-error-code + // 'F' does not mean 'fatal as in PyLint but rather 'pyflakes' such as + // unused imports, variables, etc. + F: DiagnosticSeverity.Warning + }, + mypyCategorySeverity: { + error: DiagnosticSeverity.Error, + note: DiagnosticSeverity.Hint + }, + pylintUseMinimalCheckers: false + }; + this.linting.pylintPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylintPath), workspaceRoot); + this.linting.flake8Path = getAbsolutePath(systemVariables.resolveAny(this.linting.flake8Path), workspaceRoot); + this.linting.pep8Path = getAbsolutePath(systemVariables.resolveAny(this.linting.pep8Path), workspaceRoot); + this.linting.pylamaPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylamaPath), workspaceRoot); + this.linting.prospectorPath = getAbsolutePath(systemVariables.resolveAny(this.linting.prospectorPath), workspaceRoot); + this.linting.pydocstylePath = getAbsolutePath(systemVariables.resolveAny(this.linting.pydocstylePath), workspaceRoot); + this.linting.mypyPath = getAbsolutePath(systemVariables.resolveAny(this.linting.mypyPath), workspaceRoot); + + // 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 { + this.formatting = formattingSettings; + } + // Support for travis. + this.formatting = this.formatting ? this.formatting : { + autopep8Args: [], autopep8Path: 'autopep8', + provider: 'autopep8', + blackArgs: [], blackPath: 'black', + yapfArgs: [], yapfPath: 'yapf' + }; + this.formatting.autopep8Path = getAbsolutePath(systemVariables.resolveAny(this.formatting.autopep8Path), workspaceRoot); + this.formatting.yapfPath = getAbsolutePath(systemVariables.resolveAny(this.formatting.yapfPath), workspaceRoot); + + // 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 { + this.autoComplete = autoCompleteSettings; + } + // Support for travis. + this.autoComplete = this.autoComplete ? this.autoComplete : { + extraPaths: [], + addBrackets: false, + preloadModules: [], + showAdvancedMembers: false, + typeshedPaths: [] + }; + + // 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 { + this.workspaceSymbols = workspaceSymbolsSettings; + } + // Support for travis. + this.workspaceSymbols = this.workspaceSymbols ? this.workspaceSymbols : { + ctagsPath: 'ctags', + enabled: true, + exclusionPatterns: [], + rebuildOnFileSave: true, + rebuildOnStart: true, + tagFilePath: path.join(workspaceRoot, 'tags') + }; + this.workspaceSymbols.tagFilePath = getAbsolutePath(systemVariables.resolveAny(this.workspaceSymbols.tagFilePath), workspaceRoot); + + // 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 { + this.unitTest = unitTestSettings; + if (isTestExecution() && !this.unitTest) { + // tslint:disable-next-line:prefer-type-cast + // tslint:disable-next-line:no-object-literal-type-assertion + this.unitTest = { + nosetestArgs: [], pyTestArgs: [], unittestArgs: [], + promptToConfigure: true, debugPort: 3000, + nosetestsEnabled: false, pyTestEnabled: false, unittestEnabled: false, + nosetestPath: 'nosetests', pyTestPath: 'pytest', autoTestDiscoverOnSaveEnabled: true + } as IUnitTestSettings; + } + } + + // Support for travis. + this.unitTest = this.unitTest ? this.unitTest : { + promptToConfigure: true, + debugPort: 3000, + nosetestArgs: [], nosetestPath: 'nosetest', nosetestsEnabled: false, + pyTestArgs: [], pyTestEnabled: false, pyTestPath: 'pytest', + unittestArgs: [], unittestEnabled: false, autoTestDiscoverOnSaveEnabled: true + }; + this.unitTest.pyTestPath = getAbsolutePath(systemVariables.resolveAny(this.unitTest.pyTestPath), workspaceRoot); + this.unitTest.nosetestPath = getAbsolutePath(systemVariables.resolveAny(this.unitTest.nosetestPath), workspaceRoot); + if (this.unitTest.cwd) { + this.unitTest.cwd = getAbsolutePath(systemVariables.resolveAny(this.unitTest.cwd), workspaceRoot); + } + + // 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)); + + // 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 { + this.terminal = terminalSettings; + if (isTestExecution() && !this.terminal) { + // tslint:disable-next-line:prefer-type-cast + // tslint:disable-next-line:no-object-literal-type-assertion + this.terminal = {} as ITerminalSettings; + } + } + // Support for travis. + this.terminal = this.terminal ? this.terminal : { + executeInFileDir: true, + launchArgs: [], + activateEnvironment: true + }; + } + + public get pythonPath(): string { + return this._pythonPath; + } + public set pythonPath(value: string) { + if (this._pythonPath === value) { + return; + } + // 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) { + this._pythonPath = value; + } + } +} + +function getAbsolutePath(pathToCheck: string, rootDir: string): string { + // tslint:disable-next-line:prefer-type-cast no-unsafe-any + pathToCheck = untildify(pathToCheck) as string; + if (isTestExecution() && !pathToCheck) { return rootDir; } + if (pathToCheck.indexOf(path.sep) === -1) { + return pathToCheck; + } + return path.isAbsolute(pathToCheck) ? pathToCheck : path.resolve(rootDir, pathToCheck); +} + +function getPythonExecutable(pythonPath: string): string { + // tslint:disable-next-line:prefer-type-cast no-unsafe-any + pythonPath = untildify(pythonPath) as string; + + // If only 'python'. + if (pythonPath === 'python' || + pythonPath.indexOf(path.sep) === -1 || + path.basename(pythonPath) === path.dirname(pythonPath)) { + return pythonPath; + } + + if (isValidPythonPath(pythonPath)) { + return pythonPath; + } + // 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'. + if (IS_WINDOWS) { + 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 { + if (isValidPythonPath(path.join(pythonPath, executableName))) { + return path.join(pythonPath, executableName); + } + if (isValidPythonPath(path.join(pythonPath, 'bin', executableName))) { + return path.join(pythonPath, 'bin', executableName); + } + } + } + + return pythonPath; +} + +function isValidPythonPath(pythonPath: string): boolean { + try { + const output = child_process.execFileSync(pythonPath, ['-c', 'print(1234)'], { encoding: 'utf8' }); + return output.startsWith('1234'); + } catch (ex) { + return false; + } +} diff --git a/src/client/common/configuration/service.ts b/src/client/common/configuration/service.ts index effbba72efed..fe84a3de9d5c 100644 --- a/src/client/common/configuration/service.ts +++ b/src/client/common/configuration/service.ts @@ -11,6 +11,7 @@ export class ConfigurationService implements IConfigurationService { public getSettings(resource?: Uri): IPythonSettings { return PythonSettings.getInstance(resource); } + public async updateSettingAsync(setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise { const settingsInfo = PythonSettings.getSettingsUriAndTarget(resource); diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index 2995452cb2f7..bebb4777d065 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -1,5 +1,10 @@ import * as path from 'path'; -export const PythonLanguage = { language: 'python' }; + +export const PYTHON_LANGUAGE = 'python'; +export const PYTHON = [ + { scheme: 'file', language: PYTHON_LANGUAGE }, + { scheme: 'untitled', language: PYTHON_LANGUAGE } +]; export namespace Commands { export const Set_Interpreter = 'python.setInterpreter'; @@ -66,8 +71,10 @@ export namespace LinterErrors { export const STANDARD_OUTPUT_CHANNEL = 'STANDARD_OUTPUT_CHANNEL'; export function isTestExecution(): boolean { - // tslint:disable-next-line:interface-name no-string-literal - return process.env['VSC_PYTHON_CI_TEST'] === '1'; + return process.env.VSC_PYTHON_CI_TEST === '1'; +} +export function isLanguageServerTest(): boolean { + return process.env.VSC_PYTHON_LANGUAGE_SERVER === '1'; } export const EXTENSION_ROOT_DIR = path.join(__dirname, '..', '..', '..'); diff --git a/src/client/common/enumUtils.ts b/src/client/common/enumUtils.ts index 59736d799f80..a2f1a9b833e1 100644 --- a/src/client/common/enumUtils.ts +++ b/src/client/common/enumUtils.ts @@ -1,14 +1,15 @@ +// tslint:disable:no-any no-unnecessary-class export class EnumEx { - static getNamesAndValues(e: any) { - return EnumEx.getNames(e).map(n => ({ name: n, value: e[n] as T })); + public static getNamesAndValues(e: any): { name: string; value: T }[] { + return EnumEx.getNames(e).map(n => ({ name: n, value: e[n] })); } - static getNames(e: any) { - return EnumEx.getObjValues(e).filter(v => typeof v === "string") as string[]; + public static getNames(e: any) { + return EnumEx.getObjValues(e).filter(v => typeof v === 'string') as string[]; } - static getValues(e: any) { - return EnumEx.getObjValues(e).filter(v => typeof v === "number") as T[]; + public static getValues(e: any) { + return EnumEx.getObjValues(e).filter(v => typeof v === 'number') as any as T[]; } private static getObjValues(e: any): (number | string)[] { diff --git a/src/client/common/extensions.ts b/src/client/common/extensions.ts index 96e329532df9..7dac59593aa4 100644 --- a/src/client/common/extensions.ts +++ b/src/client/common/extensions.ts @@ -15,7 +15,7 @@ declare interface String { * By default lines are trimmed and empty lines are removed. * @param {SplitLinesOptions=} splitOptions - Options used for splitting the string. */ - splitLines(splitOptions?: { trim: boolean, removeEmptyEntries?: boolean }): string[]; + splitLines(splitOptions?: { trim: boolean; removeEmptyEntries?: boolean }): string[]; /** * Appropriately formats a string so it can be used as an argument for a command in a shell. * E.g. if an argument contains a space, then it will be enclosed within double quotes. @@ -26,6 +26,11 @@ declare interface String { * E.g. if an argument contains a space, then it will be enclosed within double quotes. */ fileToCommandArgument(): string; + /** + * String.format() implementation. + * Tokens such as {0}, {1} will be replaced with corresponding positional arguments. + */ + format(...args: string[]): string; } /** @@ -33,10 +38,10 @@ declare interface String { * By default lines are trimmed and empty lines are removed. * @param {SplitLinesOptions=} splitOptions - Options used for splitting the string. */ -String.prototype.splitLines = function (this: string, splitOptions: { trim: boolean, removeEmptyEntries: boolean } = { removeEmptyEntries: true, trim: true }): string[] { +String.prototype.splitLines = function (this: string, splitOptions: { trim: boolean; removeEmptyEntries: boolean } = { removeEmptyEntries: true, trim: true }): string[] { let lines = this.split(/\r?\n/g); if (splitOptions && splitOptions.trim) { - lines = lines.filter(line => line.trim()); + lines = lines.map(line => line.trim()); } if (splitOptions && splitOptions.removeEmptyEntries) { lines = lines.filter(line => line.length > 0); @@ -82,3 +87,10 @@ Promise.prototype.ignoreErrors = function (this: Promise) { // tslint:disable-next-line:no-empty this.catch(() => { }); }; + +if (!String.prototype.format) { + String.prototype.format = function (this: string) { + const args = arguments; + return this.replace(/{(\d+)}/g, (match, number) => args[number] === undefined ? match : args[number]); + }; +} diff --git a/src/client/common/helpers.ts b/src/client/common/helpers.ts index 82d59e9308c4..ce1824291bd7 100644 --- a/src/client/common/helpers.ts +++ b/src/client/common/helpers.ts @@ -29,9 +29,9 @@ export interface Deferred { } class DeferredImpl implements Deferred { - private _resolve: (value?: T | PromiseLike) => void; + private _resolve!: (value?: T | PromiseLike) => void; // tslint:disable-next-line:no-any - private _reject: (reason?: any) => void; + private _reject!: (reason?: any) => void; private _resolved: boolean = false; private _rejected: boolean = false; private _promise: Promise; @@ -70,14 +70,14 @@ export function createDeferred(scope: any = null): Deferred { return new DeferredImpl(scope); } -export function createTemporaryFile(extension: string, temporaryDirectory?: string): Promise<{ filePath: string, cleanupCallback: Function }> { +export function createTemporaryFile(extension: string, temporaryDirectory?: string): Promise<{ filePath: string; cleanupCallback: Function }> { // tslint:disable-next-line:no-any const options: any = { postfix: extension }; if (temporaryDirectory) { options.dir = temporaryDirectory; } - return new Promise<{ filePath: string, cleanupCallback: Function }>((resolve, reject) => { + return new Promise<{ filePath: string; cleanupCallback: Function }>((resolve, reject) => { tmp.file(options, (err, tmpFile, fd, cleanupCallback) => { if (err) { return reject(err); diff --git a/src/client/common/installer/channelManager.ts b/src/client/common/installer/channelManager.ts index 3e3747f3d412..7c5780d9152e 100644 --- a/src/client/common/installer/channelManager.ts +++ b/src/client/common/installer/channelManager.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { QuickPickItem, Uri } from 'vscode'; +import { Uri } from 'vscode'; import { IInterpreterService, InterpreterType } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { IApplicationShell } from '../application/types'; @@ -34,9 +34,9 @@ export class InstallationChannelManager implements IInstallationChannelManager { label: `Install using ${installer.displayName}`, description: '', installer - } as QuickPickItem & { installer: IModuleInstaller }; + }; }); - const selection = await appShell.showQuickPick(options, { matchOnDescription: true, matchOnDetail: true, placeHolder }); + const selection = await appShell.showQuickPick(options, { matchOnDescription: true, matchOnDetail: true, placeHolder }); return selection ? selection.installer : undefined; } diff --git a/src/client/common/installer/condaInstaller.ts b/src/client/common/installer/condaInstaller.ts index a4802817b8d9..83d519be9ac0 100644 --- a/src/client/common/installer/condaInstaller.ts +++ b/src/client/common/installer/condaInstaller.ts @@ -30,8 +30,8 @@ export class CondaInstaller extends ModuleInstaller implements IModuleInstaller * @returns {Promise} Whether conda is supported as a module installer or not. */ public async isSupported(resource?: Uri): Promise { - if (this.isCondaAvailable !== undefined) { - return this.isCondaAvailable!; + if (this.isCondaAvailable === false) { + return false; } const condaLocator = this.serviceContainer.get(ICondaService); this.isCondaAvailable = await condaLocator.isCondaAvailable(); @@ -61,8 +61,7 @@ export class CondaInstaller extends ModuleInstaller implements IModuleInstaller args.push(moduleName); return { args, - execPath: condaFile, - moduleName: '' + execPath: condaFile }; } private async isCurrentEnvironmentACondaEnvironment(resource?: Uri): Promise { diff --git a/src/client/common/installer/installer.ts b/src/client/common/installer/installer.ts deleted file mode 100644 index f01113fa3ab0..000000000000 --- a/src/client/common/installer/installer.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { inject, injectable, named } from 'inversify'; -import * as os from 'os'; -import * as path from 'path'; -import { ConfigurationTarget, Uri, window, workspace } from 'vscode'; -import * as vscode from 'vscode'; -import { IFormatterHelper } from '../../formatters/types'; -import { IServiceContainer } from '../../ioc/types'; -import { ILinterManager } from '../../linters/types'; -import { ITestsHelper } from '../../unittests/common/types'; -import { PythonSettings } from '../configSettings'; -import { STANDARD_OUTPUT_CHANNEL } from '../constants'; -import { IPlatformService } from '../platform/types'; -import { IProcessService, IPythonExecutionFactory } from '../process/types'; -import { ITerminalServiceFactory } from '../terminal/types'; -import { IInstaller, ILogger, InstallerResponse, IOutputChannel, ModuleNamePurpose, Product } from '../types'; -import { IInstallationChannelManager } from './types'; - -export { Product } from '../types'; - -const CTagsInsllationScript = os.platform() === 'darwin' ? 'brew install ctags' : 'sudo apt-get install exuberant-ctags'; - -// tslint:disable-next-line:variable-name -const ProductNames = new Map(); -ProductNames.set(Product.autopep8, 'autopep8'); -ProductNames.set(Product.flake8, 'flake8'); -ProductNames.set(Product.mypy, 'mypy'); -ProductNames.set(Product.nosetest, 'nosetest'); -ProductNames.set(Product.pep8, 'pep8'); -ProductNames.set(Product.pylama, 'pylama'); -ProductNames.set(Product.prospector, 'prospector'); -ProductNames.set(Product.pydocstyle, 'pydocstyle'); -ProductNames.set(Product.pylint, 'pylint'); -ProductNames.set(Product.pytest, 'pytest'); -ProductNames.set(Product.yapf, 'yapf'); -ProductNames.set(Product.rope, 'rope'); - -// 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, - TestFramework, - RefactoringLibrary, - WorkspaceSymbols -} - -const ProductTypeNames = new Map(); -ProductTypeNames.set(ProductType.Formatter, 'Formatter'); -ProductTypeNames.set(ProductType.Linter, 'Linter'); -ProductTypeNames.set(ProductType.RefactoringLibrary, 'Refactoring library'); -ProductTypeNames.set(ProductType.TestFramework, 'Test Framework'); -ProductTypeNames.set(ProductType.WorkspaceSymbols, 'Workspace Symbols'); - -const ProductTypes = new Map(); -ProductTypes.set(Product.flake8, ProductType.Linter); -ProductTypes.set(Product.mypy, ProductType.Linter); -ProductTypes.set(Product.pep8, ProductType.Linter); -ProductTypes.set(Product.prospector, ProductType.Linter); -ProductTypes.set(Product.pydocstyle, ProductType.Linter); -ProductTypes.set(Product.pylama, ProductType.Linter); -ProductTypes.set(Product.pylint, ProductType.Linter); -ProductTypes.set(Product.ctags, ProductType.WorkspaceSymbols); -ProductTypes.set(Product.nosetest, ProductType.TestFramework); -ProductTypes.set(Product.pytest, ProductType.TestFramework); -ProductTypes.set(Product.unittest, ProductType.TestFramework); -ProductTypes.set(Product.autopep8, ProductType.Formatter); -ProductTypes.set(Product.yapf, ProductType.Formatter); -ProductTypes.set(Product.rope, ProductType.RefactoringLibrary); - -@injectable() -export class Installer implements IInstaller { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private outputChannel: vscode.OutputChannel) { - } - // tslint:disable-next-line:no-empty - public dispose() { } - public async promptToInstall(product: Product, resource?: Uri): Promise { - const productType = ProductTypes.get(product)!; - const productTypeName = ProductTypeNames.get(productType)!; - const productName = ProductNames.get(product)!; - - const installOption = ProductInstallationPrompt.has(product) ? ProductInstallationPrompt.get(product)! : `Install ${productName}`; - const alternateFormatter = product === Product.autopep8 ? 'yapf' : 'autopep8'; - const useOtherFormatter = `Use '${alternateFormatter}' formatter`; - const options: string[] = []; - options.push(installOption); - if (productType === ProductType.Formatter) { - options.push(...[useOtherFormatter]); - } - const item = await window.showErrorMessage(`${productTypeName} ${productName} is not installed`, ...options); - if (!item) { - return InstallerResponse.Ignore; - } - switch (item) { - case installOption: { - return this.install(product, resource); - } - case useOtherFormatter: { - return this.updateSetting('formatting.provider', alternateFormatter, resource) - .then(() => InstallerResponse.Installed); - } - default: { - throw new Error('Invalid selection'); - } - } - } - public translateProductToModuleName(product: Product, purpose: ModuleNamePurpose): string { - switch (product) { - case Product.mypy: return 'mypy'; - case Product.nosetest: { - return purpose === ModuleNamePurpose.install ? 'nose' : 'nosetests'; - } - case Product.pylama: return 'pylama'; - case Product.prospector: return 'prospector'; - case Product.pylint: return 'pylint'; - case Product.pytest: return 'pytest'; - case Product.autopep8: return 'autopep8'; - case Product.pep8: return 'pep8'; - case Product.pydocstyle: return 'pydocstyle'; - case Product.yapf: return 'yapf'; - case Product.flake8: return 'flake8'; - case Product.unittest: return 'unittest'; - case Product.rope: return 'rope'; - default: { - throw new Error(`Product ${product} cannot be installed as a Python Module.`); - } - } - } - public async install(product: Product, resource?: Uri): Promise { - if (product === Product.unittest) { - return InstallerResponse.Installed; - } - if (product === Product.ctags) { - return this.installCTags(); - } - - const channels = this.serviceContainer.get(IInstallationChannelManager); - const installer = await channels.getInstallationChannel(product, resource); - if (!installer) { - return InstallerResponse.Ignore; - } - - const moduleName = this.translateProductToModuleName(product, ModuleNamePurpose.install); - const logger = this.serviceContainer.get(ILogger); - await installer.installModule(moduleName, resource) - .catch(logger.logError.bind(logger, `Error in installing the module '${moduleName}'`)); - - return this.isInstalled(product) - .then(isInstalled => isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore); - } - public async isInstalled(product: Product, resource?: Uri): Promise { - if (product === Product.unittest) { - return true; - } - let moduleName: string | undefined; - try { - moduleName = this.translateProductToModuleName(product, ModuleNamePurpose.run); - // tslint:disable-next-line:no-empty - } catch { } - - // User may have customized the module name or provided the fully qualifieid path. - const executableName = this.getExecutableNameFromSettings(product, resource); - - const isModule = typeof moduleName === 'string' && moduleName.length > 0 && path.basename(executableName) === executableName; - // Prospector is an exception, it can be installed as a module, but not run as one. - if (product !== Product.prospector && isModule) { - const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create(resource); - return pythonProcess.isModuleInstalled(executableName); - } else { - const process = this.serviceContainer.get(IProcessService); - const prospectorPath = PythonSettings.getInstance(resource).linting.prospectorPath; - return process.exec(prospectorPath, ['--version'], { mergeStdOutErr: true }) - .then(() => true) - .catch(() => false); - } - } - private installCTags() { - if (this.serviceContainer.get(IPlatformService).isWindows) { - 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 { - const terminalServiceFactory = this.serviceContainer.get(ITerminalServiceFactory); - const terminalService = terminalServiceFactory.getTerminalService(); - const logger = this.serviceContainer.get(ILogger); - terminalService.sendCommand(CTagsInsllationScript, []) - .catch(logger.logError.bind(logger, `Failed to install ctags. Script sent '${CTagsInsllationScript}'.`)); - } - return InstallerResponse.Ignore; - } - - // 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); - } - } - private getExecutableNameFromSettings(product: Product, resource?: Uri): string { - const settings = PythonSettings.getInstance(resource); - const productType = ProductTypes.get(product)!; - switch (productType) { - case ProductType.WorkspaceSymbols: return settings.workspaceSymbols.ctagsPath; - case ProductType.TestFramework: { - const testHelper = this.serviceContainer.get(ITestsHelper); - const settingsPropNames = testHelper.getSettingsPropertyNames(product); - if (!settingsPropNames.pathName) { - // E.g. in the case of UnitTests we don't allow customizing the paths. - return this.translateProductToModuleName(product, ModuleNamePurpose.run); - } - return settings.unitTest[settingsPropNames.pathName] as string; - } - case ProductType.Formatter: { - const formatHelper = this.serviceContainer.get(IFormatterHelper); - const settingsPropNames = formatHelper.getSettingsPropertyNames(product); - return settings.formatting[settingsPropNames.pathName] as string; - } - case ProductType.RefactoringLibrary: return this.translateProductToModuleName(product, ModuleNamePurpose.run); - case ProductType.Linter: { - const linterManager = this.serviceContainer.get(ILinterManager); - return linterManager.getLinterInfo(product).pathName(resource); - } - default: { - throw new Error(`Unrecognized Product '${product}'`); - } - } - } -} diff --git a/src/client/common/installer/moduleInstaller.ts b/src/client/common/installer/moduleInstaller.ts index 7f6b028b1a93..5acfab18c9a1 100644 --- a/src/client/common/installer/moduleInstaller.ts +++ b/src/client/common/installer/moduleInstaller.ts @@ -8,33 +8,30 @@ import * as fs from 'fs'; import { injectable } from 'inversify'; import * as path from 'path'; import * as vscode from 'vscode'; -import { IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE, InterpreterType } from '../../interpreter/contracts'; +import { IInterpreterService, InterpreterType } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { PythonSettings } from '../configSettings'; import { STANDARD_OUTPUT_CHANNEL } from '../constants'; -import { IFileSystem } from '../platform/types'; +import { noop } from '../core.utils'; import { ITerminalServiceFactory } from '../terminal/types'; -import { ExecutionInfo, IOutputChannel } from '../types'; +import { ExecutionInfo, IConfigurationService, IOutputChannel } from '../types'; @injectable() export abstract class ModuleInstaller { constructor(protected serviceContainer: IServiceContainer) { } public async installModule(name: string, resource?: vscode.Uri): Promise { const executionInfo = await this.getExecutionInfo(name, resource); - const terminalService = this.serviceContainer.get(ITerminalServiceFactory).getTerminalService(); + const terminalService = this.serviceContainer.get(ITerminalServiceFactory).getTerminalService(resource); + const executionInfoArgs = await this.processInstallArgs(executionInfo.args, resource); if (executionInfo.moduleName) { - const settings = PythonSettings.getInstance(resource); - const args = ['-m', 'pip'].concat(executionInfo.args); - const pythonPath = settings.pythonPath; + const configService = this.serviceContainer.get(IConfigurationService); + const settings = configService.getSettings(resource); + const args = ['-m', executionInfo.moduleName].concat(executionInfoArgs); - const locator = this.serviceContainer.get(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); - const fileSystem = this.serviceContainer.get(IFileSystem); - const interpreters = await locator.getInterpreters(resource); + const pythonPath = settings.pythonPath; - const currentInterpreter = interpreters.length > 1 - ? interpreters.filter(x => fileSystem.arePathsSame(x.path, pythonPath))[0] - : interpreters[0]; + const interpreterService = this.serviceContainer.get(IInterpreterService); + const currentInterpreter = await interpreterService.getActiveInterpreter(resource); if (!currentInterpreter || currentInterpreter.type !== InterpreterType.Unknown) { await terminalService.sendCommand(pythonPath, args); @@ -48,19 +45,35 @@ export abstract class ModuleInstaller { await terminalService.sendCommand(pythonPath, args.concat(['--user'])); } } else { - await terminalService.sendCommand(executionInfo.execPath!, executionInfo.args); + await terminalService.sendCommand(executionInfo.execPath!, executionInfoArgs); } } public abstract isSupported(resource?: vscode.Uri): Promise; protected abstract getExecutionInfo(moduleName: string, resource?: vscode.Uri): Promise; + private async processInstallArgs(args: string[], resource?: vscode.Uri): Promise { + const indexOfPylint = args.findIndex(arg => arg.toUpperCase() === 'PYLINT'); + if (indexOfPylint === -1) { + return args; + } + // If installing pylint on python 2.x, then use pylint~=1.9.0 + const interpreterService = this.serviceContainer.get(IInterpreterService); + const currentInterpreter = await interpreterService.getActiveInterpreter(resource); + if (currentInterpreter && currentInterpreter.version_info && currentInterpreter.version_info[0] === 2) { + const newArgs = [...args]; + // This command could be sent to the terminal, hence '<' needs to be escaped for UNIX. + newArgs[indexOfPylint] = '"pylint<2.0.0"'; + return newArgs; + } + return args; + } private async isPathWritableAsync(directoryPath: string): Promise { const filePath = `${directoryPath}${path.sep}___vscpTest___`; return new Promise(resolve => { fs.open(filePath, fs.constants.O_CREAT | fs.constants.O_RDWR, (error, fd) => { if (!error) { fs.close(fd, (e) => { - fs.unlink(filePath); + fs.unlink(filePath, noop); }); } return resolve(!error); diff --git a/src/client/common/installer/pipEnvInstaller.ts b/src/client/common/installer/pipEnvInstaller.ts index cbde0ea3335d..2833aa218324 100644 --- a/src/client/common/installer/pipEnvInstaller.ts +++ b/src/client/common/installer/pipEnvInstaller.ts @@ -5,13 +5,14 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; import { IInterpreterLocatorService, PIPENV_SERVICE } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { ITerminalServiceFactory } from '../terminal/types'; +import { ExecutionInfo } from '../types'; +import { ModuleInstaller } from './moduleInstaller'; import { IModuleInstaller } from './types'; -const pipenvName = 'pipenv'; +export const pipenvName = 'pipenv'; @injectable() -export class PipEnvInstaller implements IModuleInstaller { +export class PipEnvInstaller extends ModuleInstaller implements IModuleInstaller { private readonly pipenv: IInterpreterLocatorService; public get displayName() { @@ -21,17 +22,18 @@ export class PipEnvInstaller implements IModuleInstaller { return 10; } - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); this.pipenv = this.serviceContainer.get(IInterpreterLocatorService, PIPENV_SERVICE); } - - public installModule(name: string): Promise { - const terminalService = this.serviceContainer.get(ITerminalServiceFactory).getTerminalService(); - return terminalService.sendCommand(pipenvName, ['install', name]); - } - public async isSupported(resource?: Uri): Promise { const interpreters = await this.pipenv.getInterpreters(resource); return interpreters && interpreters.length > 0; } + protected async getExecutionInfo(moduleName: string, resource?: Uri): Promise { + return { + args: ['install', moduleName, '--dev'], + execPath: pipenvName + }; + } } diff --git a/src/client/common/installer/pipInstaller.ts b/src/client/common/installer/pipInstaller.ts index bfc7ff4deadd..16f886f29e93 100644 --- a/src/client/common/installer/pipInstaller.ts +++ b/src/client/common/installer/pipInstaller.ts @@ -2,8 +2,9 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Uri, workspace } from 'vscode'; +import { Uri } from 'vscode'; import { IServiceContainer } from '../../ioc/types'; +import { IWorkspaceService } from '../application/types'; import { IPythonExecutionFactory } from '../process/types'; import { ExecutionInfo } from '../types'; import { ModuleInstaller } from './moduleInstaller'; @@ -25,20 +26,20 @@ export class PipInstaller extends ModuleInstaller implements IModuleInstaller { } protected async getExecutionInfo(moduleName: string, resource?: Uri): Promise { const proxyArgs: string[] = []; - const proxy = workspace.getConfiguration('http').get('proxy', ''); + const workspaceService = this.serviceContainer.get(IWorkspaceService); + const proxy = workspaceService.getConfiguration('http').get('proxy', ''); if (proxy.length > 0) { proxyArgs.push('--proxy'); proxyArgs.push(proxy); } return { args: [...proxyArgs, 'install', '-U', moduleName], - execPath: '', moduleName: 'pip' }; } private isPipAvailable(resource?: Uri): Promise { const pythonExecutionFactory = this.serviceContainer.get(IPythonExecutionFactory); - return pythonExecutionFactory.create(resource) + return pythonExecutionFactory.create({ resource }) .then(proc => proc.isModuleInstalled('pip')) .catch(() => false); } diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index c404906d6cb5..38f8707f78ca 100644 --- a/src/client/common/installer/productInstaller.ts +++ b/src/client/common/installer/productInstaller.ts @@ -1,44 +1,54 @@ +// tslint:disable:max-classes-per-file max-classes-per-file + import { inject, injectable, named } from 'inversify'; import * as os from 'os'; -import * as path from 'path'; import { OutputChannel, Uri } from 'vscode'; -import * as vscode from 'vscode'; -import { IFormatterHelper } from '../../formatters/types'; +import '../../common/extensions'; import { IServiceContainer } from '../../ioc/types'; import { ILinterManager } from '../../linters/types'; -import { ITestsHelper } from '../../unittests/common/types'; -import { IApplicationShell } from '../application/types'; +import { IApplicationShell, IWorkspaceService } from '../application/types'; import { STANDARD_OUTPUT_CHANNEL } from '../constants'; import { IPlatformService } from '../platform/types'; -import { IProcessService, IPythonExecutionFactory } from '../process/types'; +import { IProcessServiceFactory, IPythonExecutionFactory } from '../process/types'; import { ITerminalServiceFactory } from '../terminal/types'; -import { IConfigurationService, IInstaller, ILogger, InstallerResponse, IOutputChannel, ModuleNamePurpose, Product } from '../types'; +import { IConfigurationService, IInstaller, ILogger, InstallerResponse, IOutputChannel, ModuleNamePurpose, Product, ProductType } from '../types'; import { ProductNames } from './productNames'; -import { IInstallationChannelManager } from './types'; +import { IInstallationChannelManager, IProductPathService, IProductService } from './types'; export { Product } from '../types'; const CTagsInsllationScript = os.platform() === 'darwin' ? 'brew install ctags' : 'sudo apt-get install exuberant-ctags'; -enum ProductType { - Linter, - Formatter, - TestFramework, - RefactoringLibrary, - WorkspaceSymbols -} - -// tslint:disable-next-line:max-classes-per-file -abstract class BaseInstaller { - protected appShell: IApplicationShell; - protected configService: IConfigurationService; +export abstract class BaseInstaller { + private static readonly PromptPromises = new Map>(); + protected readonly appShell: IApplicationShell; + protected readonly configService: IConfigurationService; + private readonly workspaceService: IWorkspaceService; + private readonly productService: IProductService; constructor(protected serviceContainer: IServiceContainer, protected outputChannel: OutputChannel) { this.appShell = serviceContainer.get(IApplicationShell); this.configService = serviceContainer.get(IConfigurationService); + this.workspaceService = serviceContainer.get(IWorkspaceService); + this.productService = serviceContainer.get(IProductService); } - public abstract promptToInstall(product: Product, resource?: Uri): Promise; + public promptToInstall(product: Product, resource?: Uri): Promise { + // If this method gets called twice, while previous promise has not been resolved, then return that same promise. + // E.g. previous promise is not resolved as a message has been displayed to the user, so no point displaying + // another message. + const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; + const key = `${product}${workspaceFolder ? workspaceFolder.uri.fsPath : ''}`; + if (BaseInstaller.PromptPromises.has(key)) { + return BaseInstaller.PromptPromises.get(key)!; + } + const promise = this.promptToInstallImplementation(product, resource); + BaseInstaller.PromptPromises.set(key, promise); + promise.then(() => BaseInstaller.PromptPromises.delete(key)).ignoreErrors(); + promise.catch(() => BaseInstaller.PromptPromises.delete(key)).ignoreErrors(); + + return promise; + } public async install(product: Product, resource?: Uri): Promise { if (product === Product.unittest) { @@ -56,49 +66,46 @@ abstract class BaseInstaller { await installer.installModule(moduleName, resource) .catch(logger.logError.bind(logger, `Error in installing the module '${moduleName}'`)); - return this.isInstalled(product) + return this.isInstalled(product, resource) .then(isInstalled => isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore); } public async isInstalled(product: Product, resource?: Uri): Promise { - let moduleName: string | undefined; - try { - moduleName = translateProductToModule(product, ModuleNamePurpose.run); - // tslint:disable-next-line:no-empty - } catch { } - - // User may have customized the module name or provided the fully qualifieid path. + if (product === Product.unittest) { + return true; + } + // User may have customized the module name or provided the fully qualified path. const executableName = this.getExecutableNameFromSettings(product, resource); - const isModule = typeof moduleName === 'string' && moduleName.length > 0 && path.basename(executableName) === executableName; - // Prospector is an exception, it can be installed as a module, but not run as one. - if (product !== Product.prospector && isModule) { - const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create(resource); + const isModule = this.isExecutableAModule(product, resource); + if (isModule) { + const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource }); return pythonProcess.isModuleInstalled(executableName); } else { - const process = this.serviceContainer.get(IProcessService); - const prospectorPath = this.configService.getSettings(resource).linting.prospectorPath; - return process.exec(prospectorPath, ['--version'], { mergeStdOutErr: true }) + const process = await this.serviceContainer.get(IProcessServiceFactory).create(resource); + return process.exec(executableName, ['--version'], { mergeStdOutErr: true }) .then(() => true) .catch(() => false); } } - + protected abstract promptToInstallImplementation(product: Product, resource?: Uri): Promise; protected getExecutableNameFromSettings(product: Product, resource?: Uri): string { - throw new Error('getExecutableNameFromSettings is not supported on this object'); + const productType = this.productService.getProductType(product); + const productPathService = this.serviceContainer.get(IProductPathService, productType); + return productPathService.getExecutableNameFromSettings(product, resource); + } + protected isExecutableAModule(product: Product, resource?: Uri): Boolean { + const productType = this.productService.getProductType(product); + const productPathService = this.serviceContainer.get(IProductPathService, productType); + return productPathService.isExecutableAModule(product, resource); } } -class CTagsInstaller extends BaseInstaller { +export class CTagsInstaller extends BaseInstaller { constructor(serviceContainer: IServiceContainer, outputChannel: OutputChannel) { super(serviceContainer, outputChannel); } - public async promptToInstall(product: Product, resource?: Uri): Promise { - const item = await this.appShell.showErrorMessage('Install CTags to enable Python workspace symbols?', 'Yes', 'No'); - return item === 'Yes' ? this.install(product, resource) : InstallerResponse.Ignore; - } - public async install(product: Product, resource?: Uri): Promise { if (this.serviceContainer.get(IPlatformService).isWindows) { this.outputChannel.appendLine('Install Universal Ctags Win32 to enable support for Workspace Symbols'); @@ -108,57 +115,74 @@ class CTagsInstaller extends BaseInstaller { 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 { - const terminalService = this.serviceContainer.get(ITerminalServiceFactory).getTerminalService(); + const terminalService = this.serviceContainer.get(ITerminalServiceFactory).getTerminalService(resource); const logger = this.serviceContainer.get(ILogger); terminalService.sendCommand(CTagsInsllationScript, []) .catch(logger.logError.bind(logger, `Failed to install ctags. Script sent '${CTagsInsllationScript}'.`)); } return InstallerResponse.Ignore; } - - protected getExecutableNameFromSettings(product: Product, resource?: Uri): string { - const settings = this.configService.getSettings(resource); - return settings.workspaceSymbols.ctagsPath; + protected async promptToInstallImplementation(product: Product, resource?: Uri): Promise { + const item = await this.appShell.showErrorMessage('Install CTags to enable Python workspace symbols?', 'Yes', 'No'); + return item === 'Yes' ? this.install(product, resource) : InstallerResponse.Ignore; } } -class FormatterInstaller extends BaseInstaller { - public async promptToInstall(product: Product, resource?: Uri): Promise { +export class FormatterInstaller extends BaseInstaller { + protected async promptToInstallImplementation(product: Product, resource?: Uri): Promise { + // Hard-coded on purpose because the UI won't necessarily work having + // another formatter. + const formatters = [Product.autopep8, Product.black, Product.yapf]; + const formatterNames = formatters.map((formatter) => ProductNames.get(formatter)!); const productName = ProductNames.get(product)!; + formatterNames.splice(formatterNames.indexOf(productName), 1); + const useOptions = formatterNames.map((name) => `Use ${name}`); + const yesChoice = 'Yes'; + + const options = [...useOptions]; + let message = `Formatter ${productName} is not installed. Install?`; + if (this.isExecutableAModule(product, resource)) { + options.splice(0, 0, yesChoice); + } else { + const executable = this.getExecutableNameFromSettings(product, resource); + message = `Path to the ${productName} formatter is invalid (${executable})`; + } - const installThis = `Install ${productName}`; - const alternateFormatter = product === Product.autopep8 ? 'yapf' : 'autopep8'; - const useOtherFormatter = `Use '${alternateFormatter}' formatter`; - const item = await this.appShell.showErrorMessage(`Formatter ${productName} is not installed.`, installThis, useOtherFormatter); - - if (item === installThis) { + const item = await this.appShell.showErrorMessage(message, ...options); + if (item === yesChoice) { return this.install(product, resource); + } else if (typeof item === 'string') { + for (const formatter of formatters) { + const formatterName = ProductNames.get(formatter)!; + + if (item.endsWith(formatterName)) { + await this.configService.updateSettingAsync('formatting.provider', formatterName, resource); + return this.install(formatter, resource); + } + } } - if (item === useOtherFormatter) { - await this.configService.updateSettingAsync('formatting.provider', alternateFormatter, resource); - return InstallerResponse.Installed; - } - return InstallerResponse.Ignore; - } - protected getExecutableNameFromSettings(product: Product, resource?: Uri): string { - const settings = this.configService.getSettings(resource); - const formatHelper = this.serviceContainer.get(IFormatterHelper); - const settingsPropNames = formatHelper.getSettingsPropertyNames(product); - return settings.formatting[settingsPropNames.pathName] as string; + return InstallerResponse.Ignore; } } -// tslint:disable-next-line:max-classes-per-file -class LinterInstaller extends BaseInstaller { - public async promptToInstall(product: Product, resource?: Uri): Promise { +export class LinterInstaller extends BaseInstaller { + protected async promptToInstallImplementation(product: Product, resource?: Uri): Promise { const productName = ProductNames.get(product)!; const install = 'Install'; const disableAllLinting = 'Disable linting'; const disableThisLinter = `Disable ${productName}`; - const response = await this.appShell - .showErrorMessage(`Linter ${productName} is not installed.`, install, disableThisLinter, disableAllLinting); + const options = [disableThisLinter, disableAllLinting]; + let message = `Linter ${productName} is not installed.`; + if (this.isExecutableAModule(product, resource)) { + options.splice(0, 0, install); + } else { + const executable = this.getExecutableNameFromSettings(product, resource); + message = `Path to the ${productName} linter is invalid (${executable})`; + } + + const response = await this.appShell.showErrorMessage(message, ...options); if (response === install) { return this.install(product, resource); } @@ -172,65 +196,41 @@ class LinterInstaller extends BaseInstaller { } return InstallerResponse.Ignore; } - protected getExecutableNameFromSettings(product: Product, resource?: Uri): string { - const linterManager = this.serviceContainer.get(ILinterManager); - return linterManager.getLinterInfo(product).pathName(resource); - } } -// tslint:disable-next-line:max-classes-per-file -class TestFrameworkInstaller extends BaseInstaller { - public async promptToInstall(product: Product, resource?: Uri): Promise { +export class TestFrameworkInstaller extends BaseInstaller { + protected async promptToInstallImplementation(product: Product, resource?: Uri): Promise { const productName = ProductNames.get(product)!; - const item = await this.appShell.showErrorMessage(`Test framework ${productName} is not installed. Install?`, 'Yes', 'No'); - return item === 'Yes' ? this.install(product, resource) : InstallerResponse.Ignore; - } - protected getExecutableNameFromSettings(product: Product, resource?: Uri): string { - const testHelper = this.serviceContainer.get(ITestsHelper); - const settingsPropNames = testHelper.getSettingsPropertyNames(product); - if (!settingsPropNames.pathName) { - // E.g. in the case of UnitTests we don't allow customizing the paths. - return translateProductToModule(product, ModuleNamePurpose.run); + const options: string[] = []; + let message = `Test framework ${productName} is not installed. Install?`; + if (this.isExecutableAModule(product, resource)) { + options.push(...['Yes', 'No']); + } else { + const executable = this.getExecutableNameFromSettings(product, resource); + message = `Path to the ${productName} test framework is invalid (${executable})`; } - const settings = this.configService.getSettings(resource); - return settings.unitTest[settingsPropNames.pathName] as string; + + const item = await this.appShell.showErrorMessage(message, ...options); + return item === 'Yes' ? this.install(product, resource) : InstallerResponse.Ignore; } } -// tslint:disable-next-line:max-classes-per-file -class RefactoringLibraryInstaller extends BaseInstaller { - public async promptToInstall(product: Product, resource?: Uri): Promise { +export class RefactoringLibraryInstaller extends BaseInstaller { + protected async promptToInstallImplementation(product: Product, resource?: Uri): Promise { const productName = ProductNames.get(product)!; const item = await this.appShell.showErrorMessage(`Refactoring library ${productName} is not installed. Install?`, 'Yes', 'No'); return item === 'Yes' ? this.install(product, resource) : InstallerResponse.Ignore; } - protected getExecutableNameFromSettings(product: Product, resource?: Uri): string { - return translateProductToModule(product, ModuleNamePurpose.run); - } } -// tslint:disable-next-line:max-classes-per-file @injectable() export class ProductInstaller implements IInstaller { - private ProductTypes = new Map(); + private readonly productService: IProductService; constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private outputChannel: vscode.OutputChannel) { - this.ProductTypes.set(Product.flake8, ProductType.Linter); - this.ProductTypes.set(Product.mypy, ProductType.Linter); - this.ProductTypes.set(Product.pep8, ProductType.Linter); - this.ProductTypes.set(Product.prospector, ProductType.Linter); - this.ProductTypes.set(Product.pydocstyle, ProductType.Linter); - this.ProductTypes.set(Product.pylama, ProductType.Linter); - this.ProductTypes.set(Product.pylint, ProductType.Linter); - this.ProductTypes.set(Product.ctags, ProductType.WorkspaceSymbols); - this.ProductTypes.set(Product.nosetest, ProductType.TestFramework); - this.ProductTypes.set(Product.pytest, ProductType.TestFramework); - this.ProductTypes.set(Product.unittest, ProductType.TestFramework); - this.ProductTypes.set(Product.autopep8, ProductType.Formatter); - this.ProductTypes.set(Product.yapf, ProductType.Formatter); - this.ProductTypes.set(Product.rope, ProductType.RefactoringLibrary); + @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private outputChannel: OutputChannel) { + this.productService = serviceContainer.get(IProductService); } // tslint:disable-next-line:no-empty @@ -247,9 +247,8 @@ export class ProductInstaller implements IInstaller { public translateProductToModuleName(product: Product, purpose: ModuleNamePurpose): string { return translateProductToModule(product, purpose); } - private createInstaller(product: Product): BaseInstaller { - const productType = this.ProductTypes.get(product)!; + const productType = this.productService.getProductType(product); switch (productType) { case ProductType.Formatter: return new FormatterInstaller(this.serviceContainer, this.outputChannel); @@ -279,6 +278,7 @@ function translateProductToModule(product: Product, purpose: ModuleNamePurpose): case Product.pylint: return 'pylint'; case Product.pytest: return 'pytest'; case Product.autopep8: return 'autopep8'; + case Product.black: return 'black'; case Product.pep8: return 'pep8'; case Product.pydocstyle: return 'pydocstyle'; case Product.yapf: return 'yapf'; diff --git a/src/client/common/installer/productNames.ts b/src/client/common/installer/productNames.ts index 9371f540c778..dc58605cc57d 100644 --- a/src/client/common/installer/productNames.ts +++ b/src/client/common/installer/productNames.ts @@ -6,6 +6,7 @@ import { Product } from '../types'; // tslint:disable-next-line:variable-name export const ProductNames = new Map(); ProductNames.set(Product.autopep8, 'autopep8'); +ProductNames.set(Product.black, 'black'); ProductNames.set(Product.flake8, 'flake8'); ProductNames.set(Product.mypy, 'mypy'); ProductNames.set(Product.nosetest, 'nosetest'); diff --git a/src/client/common/installer/productPath.ts b/src/client/common/installer/productPath.ts new file mode 100644 index 000000000000..9e4500a7eabd --- /dev/null +++ b/src/client/common/installer/productPath.ts @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-classes-per-file + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IFormatterHelper } from '../../formatters/types'; +import { IServiceContainer } from '../../ioc/types'; +import { ILinterManager } from '../../linters/types'; +import { ITestsHelper } from '../../unittests/common/types'; +import { IConfigurationService, IInstaller, ModuleNamePurpose, Product } from '../types'; +import { IProductPathService } from './types'; + +@injectable() +abstract class BaseProductPathsService implements IProductPathService { + protected readonly configService: IConfigurationService; + protected readonly productInstaller: IInstaller; + constructor(@inject(IServiceContainer) protected serviceContainer: IServiceContainer) { + this.configService = serviceContainer.get(IConfigurationService); + this.productInstaller = serviceContainer.get(IInstaller); + } + public abstract getExecutableNameFromSettings(product: Product, resource?: Uri): string; + public isExecutableAModule(product: Product, resource?: Uri): Boolean { + let moduleName: string | undefined; + try { + moduleName = this.productInstaller.translateProductToModuleName(product, ModuleNamePurpose.run); + // tslint:disable-next-line:no-empty + } catch { } + + // User may have customized the module name or provided the fully qualifieid path. + const executableName = this.getExecutableNameFromSettings(product, resource); + + return typeof moduleName === 'string' && moduleName.length > 0 && path.basename(executableName) === executableName; + } +} + +@injectable() +export class CTagsProductPathService extends BaseProductPathsService { + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + public getExecutableNameFromSettings(_: Product, resource?: Uri): string { + const settings = this.configService.getSettings(resource); + return settings.workspaceSymbols.ctagsPath; + } +} + +@injectable() +export class FormatterProductPathService extends BaseProductPathsService { + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + public getExecutableNameFromSettings(product: Product, resource?: Uri): string { + const settings = this.configService.getSettings(resource); + const formatHelper = this.serviceContainer.get(IFormatterHelper); + const settingsPropNames = formatHelper.getSettingsPropertyNames(product); + return settings.formatting[settingsPropNames.pathName] as string; + } +} + +@injectable() +export class LinterProductPathService extends BaseProductPathsService { + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + public getExecutableNameFromSettings(product: Product, resource?: Uri): string { + const linterManager = this.serviceContainer.get(ILinterManager); + return linterManager.getLinterInfo(product).pathName(resource); + } +} + +@injectable() +export class TestFrameworkProductPathService extends BaseProductPathsService { + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + public getExecutableNameFromSettings(product: Product, resource?: Uri): string { + const testHelper = this.serviceContainer.get(ITestsHelper); + const settingsPropNames = testHelper.getSettingsPropertyNames(product); + if (!settingsPropNames.pathName) { + // E.g. in the case of UnitTests we don't allow customizing the paths. + return this.productInstaller.translateProductToModuleName(product, ModuleNamePurpose.run); + } + const settings = this.configService.getSettings(resource); + return settings.unitTest[settingsPropNames.pathName] as string; + } +} + +@injectable() +export class RefactoringLibraryProductPathService extends BaseProductPathsService { + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + public getExecutableNameFromSettings(product: Product, _?: Uri): string { + return this.productInstaller.translateProductToModuleName(product, ModuleNamePurpose.run); + } +} diff --git a/src/client/common/installer/productService.ts b/src/client/common/installer/productService.ts new file mode 100644 index 000000000000..78ce5bbec847 --- /dev/null +++ b/src/client/common/installer/productService.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { Product, ProductType } from '../types'; +import { IProductService } from './types'; + +@injectable() +export class ProductService implements IProductService { + private ProductTypes = new Map(); + + constructor() { + this.ProductTypes.set(Product.flake8, ProductType.Linter); + this.ProductTypes.set(Product.mypy, ProductType.Linter); + this.ProductTypes.set(Product.pep8, ProductType.Linter); + this.ProductTypes.set(Product.prospector, ProductType.Linter); + this.ProductTypes.set(Product.pydocstyle, ProductType.Linter); + this.ProductTypes.set(Product.pylama, ProductType.Linter); + this.ProductTypes.set(Product.pylint, ProductType.Linter); + this.ProductTypes.set(Product.ctags, ProductType.WorkspaceSymbols); + this.ProductTypes.set(Product.nosetest, ProductType.TestFramework); + this.ProductTypes.set(Product.pytest, ProductType.TestFramework); + this.ProductTypes.set(Product.unittest, ProductType.TestFramework); + this.ProductTypes.set(Product.autopep8, ProductType.Formatter); + this.ProductTypes.set(Product.black, ProductType.Formatter); + this.ProductTypes.set(Product.yapf, ProductType.Formatter); + this.ProductTypes.set(Product.rope, ProductType.RefactoringLibrary); + } + public getProductType(product: Product): ProductType { + return this.ProductTypes.get(product)!; + } +} diff --git a/src/client/common/installer/serviceRegistry.ts b/src/client/common/installer/serviceRegistry.ts index 9f50aa77fba3..3cbd2cacaea6 100644 --- a/src/client/common/installer/serviceRegistry.ts +++ b/src/client/common/installer/serviceRegistry.ts @@ -3,15 +3,25 @@ 'use strict'; import { IServiceManager } from '../../ioc/types'; +import { ProductType } from '../types'; import { InstallationChannelManager } from './channelManager'; import { CondaInstaller } from './condaInstaller'; import { PipEnvInstaller } from './pipEnvInstaller'; import { PipInstaller } from './pipInstaller'; -import { IInstallationChannelManager, IModuleInstaller } from './types'; +import { CTagsProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from './productPath'; +import { ProductService } from './productService'; +import { IInstallationChannelManager, IModuleInstaller, IProductPathService, IProductService } from './types'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IModuleInstaller, CondaInstaller); serviceManager.addSingleton(IModuleInstaller, PipInstaller); serviceManager.addSingleton(IModuleInstaller, PipEnvInstaller); serviceManager.addSingleton(IInstallationChannelManager, InstallationChannelManager); + + serviceManager.addSingleton(IProductService, ProductService); + serviceManager.addSingleton(IProductPathService, CTagsProductPathService, ProductType.WorkspaceSymbols); + serviceManager.addSingleton(IProductPathService, FormatterProductPathService, ProductType.Formatter); + serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); + serviceManager.addSingleton(IProductPathService, TestFrameworkProductPathService, ProductType.TestFramework); + serviceManager.addSingleton(IProductPathService, RefactoringLibraryProductPathService, ProductType.RefactoringLibrary); } diff --git a/src/client/common/installer/types.ts b/src/client/common/installer/types.ts index d84cf7bb64d2..c0521ee9386e 100644 --- a/src/client/common/installer/types.ts +++ b/src/client/common/installer/types.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { Uri } from 'vscode'; -import { Product } from '../types'; +import { Product, ProductType } from '../types'; export const IModuleInstaller = Symbol('IModuleInstaller'); export interface IModuleInstaller { @@ -23,3 +23,12 @@ export interface IInstallationChannelManager { getInstallationChannels(resource?: Uri): Promise; showNoInstallersMessage(): void; } +export const IProductService = Symbol('IProductService'); +export interface IProductService { + getProductType(product: Product): ProductType; +} +export const IProductPathService = Symbol('IProductPathService'); +export interface IProductPathService { + getExecutableNameFromSettings(product: Product, resource?: Uri): string; + isExecutableAModule(product: Product, resource?: Uri): Boolean; +} diff --git a/src/client/common/logger.ts b/src/client/common/logger.ts index e15281d68294..e07590c96625 100644 --- a/src/client/common/logger.ts +++ b/src/client/common/logger.ts @@ -1,3 +1,5 @@ +// tslint:disable:no-console + import { injectable } from 'inversify'; import { ILogger } from './types'; @@ -19,6 +21,13 @@ export class Logger implements ILogger { console.warn(`${PREFIX}${message}`); } } + public logInformation(message: string, ex?: Error) { + if (ex) { + console.info(`${PREFIX}${message}`, ex); + } else { + console.info(`${PREFIX}${message}`); + } + } } // tslint:disable-next-line:no-any export function error(title: string = '', message: any) { diff --git a/src/client/common/net/browser.ts b/src/client/common/net/browser.ts index 528f2c146f2b..cb458b7c3735 100644 --- a/src/client/common/net/browser.ts +++ b/src/client/common/net/browser.ts @@ -2,7 +2,9 @@ // Licensed under the MIT License. import * as child_process from 'child_process'; +import { injectable } from 'inversify'; import * as os from 'os'; +import { IBrowserService } from '../types'; export function launch(url: string) { let openCommand: string | undefined; @@ -19,3 +21,10 @@ export function launch(url: string) { } child_process.spawn(openCommand, [url]); } + +@injectable() +export class BrowserService implements IBrowserService { + public launch(url: string): void{ + launch(url); + } +} diff --git a/src/client/common/net/socket/socketServer.ts b/src/client/common/net/socket/socketServer.ts index 24e7b2713740..74099fd7cffe 100644 --- a/src/client/common/net/socket/socketServer.ts +++ b/src/client/common/net/socket/socketServer.ts @@ -28,22 +28,22 @@ export class SocketServer extends EventEmitter implements ISocketServer { this.socketServer = undefined; } - public Start(options: { port?: number, host?: string } = {}): Promise { + public Start(options: { port?: number; host?: string } = {}): Promise { const def = createDeferred(); this.socketServer = net.createServer(this.connectionListener.bind(this)); const port = typeof options.port === 'number' ? options.port! : 0; const host = typeof options.host === 'string' ? options.host! : 'localhost'; - this.socketServer!.listen({ port, host }, () => { - def.resolve(this.socketServer!.address().port); - }); - this.socketServer!.on('error', ex => { console.error('Error in Socket Server', ex); const msg = `Failed to start the socket server. (Error: ${ex.message})`; def.reject(msg); }); + this.socketServer!.listen({ port, host }, () => { + def.resolve(this.socketServer!.address().port); + }); + return def.promise; } diff --git a/src/client/common/persistentState.ts b/src/client/common/persistentState.ts index a882db7cbc1f..8042c7f52d2a 100644 --- a/src/client/common/persistentState.ts +++ b/src/client/common/persistentState.ts @@ -8,25 +8,38 @@ import { Memento } from 'vscode'; import { GLOBAL_MEMENTO, IMemento, IPersistentState, IPersistentStateFactory, WORKSPACE_MEMENTO } from './types'; class PersistentState implements IPersistentState{ - constructor(private storage: Memento, private key: string, private defaultValue: T) { } + constructor(private storage: Memento, private key: string, private defaultValue?: T, private expiryDurationMs?: number) { } public get value(): T { - return this.storage.get(this.key, this.defaultValue); + if (this.expiryDurationMs) { + const cachedData = this.storage.get<{ data?: T; expiry?: number }>(this.key, { data: this.defaultValue! }); + if (!cachedData || !cachedData.expiry || cachedData.expiry < Date.now()) { + return this.defaultValue!; + } else { + return cachedData.data!; + } + } else { + return this.storage.get(this.key, this.defaultValue!); + } } public async updateValue(newValue: T): Promise { - await this.storage.update(this.key, newValue); + if (this.expiryDurationMs) { + await this.storage.update(this.key, { data: newValue, expiry: Date.now() + this.expiryDurationMs }); + } else { + await this.storage.update(this.key, newValue); + } } } @injectable() export class PersistentStateFactory implements IPersistentStateFactory { - constructor( @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, + constructor(@inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, @inject(IMemento) @named(WORKSPACE_MEMENTO) private workspaceState: Memento) { } - public createGlobalPersistentState(key: string, defaultValue: T): IPersistentState { - return new PersistentState(this.globalState, key, defaultValue); + public createGlobalPersistentState(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState { + return new PersistentState(this.globalState, key, defaultValue, expiryDurationMs); } - public createWorkspacePersistentState(key: string, defaultValue: T): IPersistentState { - return new PersistentState(this.workspaceState, key, defaultValue); + public createWorkspacePersistentState(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState { + return new PersistentState(this.workspaceState, key, defaultValue, expiryDurationMs); } } diff --git a/src/client/common/platform/fileSystem.ts b/src/client/common/platform/fileSystem.ts index 89ec9aac3e9e..ede57f3290e0 100644 --- a/src/client/common/platform/fileSystem.ts +++ b/src/client/common/platform/fileSystem.ts @@ -2,10 +2,14 @@ // Licensed under the MIT License. 'use strict'; +import { createHash } from 'crypto'; import * as fs from 'fs-extra'; +import * as glob from 'glob'; import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { IFileSystem, IPlatformService } from './types'; +import * as tmp from 'tmp'; +import { createDeferred } from '../helpers'; +import { IFileSystem, IPlatformService, TemporaryFile } from './types'; @injectable() export class FileSystem implements IFileSystem { @@ -15,7 +19,7 @@ export class FileSystem implements IFileSystem { return path.sep; } - public objectExistsAsync(filePath: string, statCheck: (s: fs.Stats) => boolean): Promise { + public objectExists(filePath: string, statCheck: (s: fs.Stats) => boolean): Promise { return new Promise(resolve => { fs.stat(filePath, (error, stats) => { if (error) { @@ -26,8 +30,11 @@ export class FileSystem implements IFileSystem { }); } - public fileExistsAsync(filePath: string): Promise { - return this.objectExistsAsync(filePath, (stats) => stats.isFile()); + public fileExists(filePath: string): Promise { + return this.objectExists(filePath, (stats) => stats.isFile()); + } + public fileExistsSync(filePath: string): boolean { + return fs.existsSync(filePath); } /** * Reads the contents of the file using utf8 and returns the string contents. @@ -39,15 +46,15 @@ export class FileSystem implements IFileSystem { return fs.readFile(filePath).then(buffer => buffer.toString()); } - public directoryExistsAsync(filePath: string): Promise { - return this.objectExistsAsync(filePath, (stats) => stats.isDirectory()); + public directoryExists(filePath: string): Promise { + return this.objectExists(filePath, (stats) => stats.isDirectory()); } - public createDirectoryAsync(directoryPath: string): Promise { + public createDirectory(directoryPath: string): Promise { return fs.mkdirp(directoryPath); } - public getSubDirectoriesAsync(rootDir: string): Promise { + public getSubDirectories(rootDir: string): Promise { return new Promise(resolve => { fs.readdir(rootDir, (error, files) => { if (error) { @@ -79,18 +86,71 @@ export class FileSystem implements IFileSystem { } public appendFileSync(filename: string, data: {}, encoding: string): void; - public appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: number; flag?: string; }): void; + public appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: number; flag?: string }): void; // tslint:disable-next-line:unified-signatures - public appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: string; flag?: string; }): void; + public appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: string; flag?: string }): void; public appendFileSync(filename: string, data: {}, optionsOrEncoding: {}): void { return fs.appendFileSync(filename, data, optionsOrEncoding); } - public getRealPathAsync(filePath: string): Promise { + public getRealPath(filePath: string): Promise { return new Promise(resolve => { fs.realpath(filePath, (err, realPath) => { resolve(err ? filePath : realPath); }); }); } + + public copyFile(src: string, dest: string): Promise { + const deferred = createDeferred(); + const rs = fs.createReadStream(src).on('error', (err) => { + deferred.reject(err); + }); + const ws = fs.createWriteStream(dest).on('error', (err) => { + deferred.reject(err); + }).on('close', () => { + deferred.resolve(); + }); + rs.pipe(ws); + return deferred.promise; + } + + public deleteFile(filename: string): Promise { + const deferred = createDeferred(); + fs.unlink(filename, err => err ? deferred.reject(err) : deferred.resolve()); + return deferred.promise; + } + public getFileHash(filePath: string): Promise { + return new Promise(resolve => { + fs.lstat(filePath, (err, stats) => { + if (err) { + resolve(); + } else { + const actual = createHash('sha512').update(`${stats.ctimeMs}-${stats.mtimeMs}`).digest('hex'); + resolve(actual); + } + }); + }); + } + public search(globPattern: string): Promise { + return new Promise((resolve, reject) => { + glob(globPattern, (ex, files) => { + if (ex) { + return reject(ex); + } + resolve(Array.isArray(files) ? files : []); + }); + }); + } + public createTemporaryFile(extension: string): Promise { + return new Promise((resolve, reject) => { + tmp.file({ postfix: extension }, (err, tmpFile, _, cleanupCallback) => { + if (err) { + return reject(err); + } + resolve({ filePath: tmpFile, dispose: cleanupCallback }); + }); + }); + + } } diff --git a/src/client/common/platform/pathUtils.ts b/src/client/common/platform/pathUtils.ts index 8743cd51d88a..319d523402fb 100644 --- a/src/client/common/platform/pathUtils.ts +++ b/src/client/common/platform/pathUtils.ts @@ -1,12 +1,20 @@ import { inject, injectable } from 'inversify'; +import * as path from 'path'; import { IPathUtils, IsWindows } from '../types'; import { NON_WINDOWS_PATH_VARIABLE_NAME, WINDOWS_PATH_VARIABLE_NAME } from './constants'; -// TO DO: Deprecate in favor of IPlatformService @injectable() export class PathUtils implements IPathUtils { - constructor( @inject(IsWindows) private isWindows: boolean) { } + constructor(@inject(IsWindows) private isWindows: boolean) { } + public get delimiter(): string { + return path.delimiter; + } + // TO DO: Deprecate in favor of IPlatformService public getPathVariableName() { return this.isWindows ? WINDOWS_PATH_VARIABLE_NAME : NON_WINDOWS_PATH_VARIABLE_NAME; } + public basename(pathValue: string, ext?: string): string { + return path.basename(pathValue, ext); + } + } diff --git a/src/client/common/platform/types.ts b/src/client/common/platform/types.ts index d87722efdd38..ec08fd7d284c 100644 --- a/src/client/common/platform/types.ts +++ b/src/client/common/platform/types.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import * as fs from 'fs'; +import { Disposable } from 'vscode'; export enum Architecture { Unknown = 1, @@ -28,19 +29,27 @@ export interface IPlatformService { virtualEnvBinName: 'bin' | 'scripts'; } +export type TemporaryFile = { filePath: string } & Disposable; + export const IFileSystem = Symbol('IFileSystem'); export interface IFileSystem { directorySeparatorChar: string; - objectExistsAsync(path: string, statCheck: (s: fs.Stats) => boolean): Promise; - fileExistsAsync(path: string): Promise; - directoryExistsAsync(path: string): Promise; - createDirectoryAsync(path: string): Promise; - getSubDirectoriesAsync(rootDir: string): Promise; + objectExists(path: string, statCheck: (s: fs.Stats) => boolean): Promise; + fileExists(path: string): Promise; + fileExistsSync(path: string): boolean; + directoryExists(path: string): Promise; + createDirectory(path: string): Promise; + getSubDirectories(rootDir: string): Promise; arePathsSame(path1: string, path2: string): boolean; readFile(filePath: string): Promise; appendFileSync(filename: string, data: {}, encoding: string): void; - appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: number; flag?: string; }): void; + appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: number; flag?: string }): void; // tslint:disable-next-line:unified-signatures - appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: string; flag?: string; }): void; - getRealPathAsync(path: string): Promise; + appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: string; flag?: string }): void; + getRealPath(path: string): Promise; + copyFile(src: string, dest: string): Promise; + deleteFile(filename: string): Promise; + getFileHash(filePath: string): Promise; + search(globPattern: string): Promise; + createTemporaryFile(extension: string): Promise; } diff --git a/src/client/common/process/currentProcess.ts b/src/client/common/process/currentProcess.ts index e7f5b5efe293..fc2bb97bb01d 100644 --- a/src/client/common/process/currentProcess.ts +++ b/src/client/common/process/currentProcess.ts @@ -1,3 +1,5 @@ +// tslint:disable:no-any + import { injectable } from 'inversify'; import { ICurrentProcess } from '../types'; import { EnvironmentVariables } from '../variables/types'; @@ -5,12 +7,11 @@ import { EnvironmentVariables } from '../variables/types'; @injectable() export class CurrentProcess implements ICurrentProcess { public on = (event: string | symbol, listener: Function): this => { - process.on(event, listener); - // tslint:disable-next-line:no-any + process.on(event as any, listener as any); return process as any; } public get env(): EnvironmentVariables { - return process.env; + return process.env as any as EnvironmentVariables; } public get argv(): string[] { return process.argv; diff --git a/src/client/common/process/proc.ts b/src/client/common/process/proc.ts index 5d8cefd935cb..ed511447db1f 100644 --- a/src/client/common/process/proc.ts +++ b/src/client/common/process/proc.ts @@ -1,23 +1,25 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// tslint:disable:no-any + import { spawn } from 'child_process'; -import { inject, injectable } from 'inversify'; import { Observable } from 'rxjs/Observable'; import { Disposable } from 'vscode'; import { createDeferred } from '../helpers'; +import { EnvironmentVariables } from '../variables/types'; import { DEFAULT_ENCODING } from './constants'; import { ExecutionResult, IBufferDecoder, IProcessService, ObservableExecutionResult, Output, SpawnOptions, StdErrError } from './types'; -@injectable() export class ProcessService implements IProcessService { - constructor(@inject(IBufferDecoder) private decoder: IBufferDecoder) { } + constructor(private readonly decoder: IBufferDecoder, private readonly env?: EnvironmentVariables) { } public execObservable(file: string, args: string[], options: SpawnOptions = {}): ObservableExecutionResult { const encoding = options.encoding = typeof options.encoding === 'string' && options.encoding.length > 0 ? options.encoding : DEFAULT_ENCODING; delete options.encoding; const spawnOptions = { ...options }; if (!spawnOptions.env || Object.keys(spawnOptions).length === 0) { - spawnOptions.env = { ...process.env }; + const env = this.env ? this.env : process.env; + spawnOptions.env = { ...env }; } // Always ensure we have unbuffered output. @@ -33,8 +35,8 @@ export class ProcessService implements IProcessService { const disposables: Disposable[] = []; const on = (ee: NodeJS.EventEmitter, name: string, fn: Function) => { - ee.on(name, fn); - disposables.push({ dispose: () => ee.removeListener(name, fn) }); + ee.on(name, fn as any); + disposables.push({ dispose: () => ee.removeListener(name, fn as any) }); }; if (options.token) { @@ -77,7 +79,8 @@ export class ProcessService implements IProcessService { delete options.encoding; const spawnOptions = { ...options }; if (!spawnOptions.env || Object.keys(spawnOptions).length === 0) { - spawnOptions.env = { ...process.env }; + const env = this.env ? this.env : process.env; + spawnOptions.env = { ...env }; } // Always ensure we have unbuffered output. @@ -90,8 +93,8 @@ export class ProcessService implements IProcessService { const disposables: Disposable[] = []; const on = (ee: NodeJS.EventEmitter, name: string, fn: Function) => { - ee.on(name, fn); - disposables.push({ dispose: () => ee.removeListener(name, fn) }); + ee.on(name, fn as any); + disposables.push({ dispose: () => ee.removeListener(name, fn as any) }); }; if (options.token) { diff --git a/src/client/common/process/processFactory.ts b/src/client/common/process/processFactory.ts new file mode 100644 index 000000000000..91440cf9bddd --- /dev/null +++ b/src/client/common/process/processFactory.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IServiceContainer } from '../../ioc/types'; +import { IEnvironmentVariablesProvider } from '../variables/types'; +import { ProcessService } from './proc'; +import { IBufferDecoder, IProcessService, IProcessServiceFactory } from './types'; + +@injectable() +export class ProcessServiceFactory implements IProcessServiceFactory { + private envVarsService: IEnvironmentVariablesProvider; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.envVarsService = serviceContainer.get(IEnvironmentVariablesProvider); + } + public async create(resource?: Uri): Promise { + const customEnvVars = await this.envVarsService.getEnvironmentVariables(resource); + const decoder = this.serviceContainer.get(IBufferDecoder); + return new ProcessService(decoder, customEnvVars); + } +} diff --git a/src/client/common/process/pythonExecutionFactory.ts b/src/client/common/process/pythonExecutionFactory.ts index 4866e6e504f8..3e9551905cec 100644 --- a/src/client/common/process/pythonExecutionFactory.ts +++ b/src/client/common/process/pythonExecutionFactory.ts @@ -4,20 +4,21 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; import { IServiceContainer } from '../../ioc/types'; -import { IEnvironmentVariablesProvider } from '../variables/types'; +import { IConfigurationService } from '../types'; import { PythonExecutionService } from './pythonProcess'; -import { IPythonExecutionFactory, IPythonExecutionService } from './types'; +import { ExecutionFactoryCreationOptions, IProcessServiceFactory, IPythonExecutionFactory, IPythonExecutionService } from './types'; @injectable() export class PythonExecutionFactory implements IPythonExecutionFactory { - private envVarsService: IEnvironmentVariablesProvider; + private readonly configService: IConfigurationService; + private processServiceFactory: IProcessServiceFactory; constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.envVarsService = serviceContainer.get(IEnvironmentVariablesProvider); + this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); + this.configService = serviceContainer.get(IConfigurationService); } - public async create(resource?: Uri): Promise { - return this.envVarsService.getEnvironmentVariables(resource) - .then(customEnvVars => { - return new PythonExecutionService(this.serviceContainer, customEnvVars, resource); - }); + public async create(options: ExecutionFactoryCreationOptions): Promise { + const pythonPath = options.pythonPath ? options.pythonPath : this.configService.getSettings(options.resource).pythonPath; + const processService = await this.processServiceFactory.create(options.resource); + return new PythonExecutionService(this.serviceContainer, processService, pythonPath); } } diff --git a/src/client/common/process/pythonProcess.ts b/src/client/common/process/pythonProcess.ts index 0d6622bed472..625a02f34e5b 100644 --- a/src/client/common/process/pythonProcess.ts +++ b/src/client/common/process/pythonProcess.ts @@ -2,72 +2,75 @@ // Licensed under the MIT License. import { injectable } from 'inversify'; +import * as path from 'path'; import { Uri } from 'vscode'; -import { IInterpreterVersionService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; +import { EXTENSION_ROOT_DIR } from '../constants'; import { ErrorUtils } from '../errors/errorUtils'; import { ModuleNotInstalledError } from '../errors/moduleNotInstalledError'; -import { IFileSystem } from '../platform/types'; -import { IConfigurationService } from '../types'; +import { Architecture, IFileSystem } from '../platform/types'; import { EnvironmentVariables } from '../variables/types'; -import { ExecutionResult, IProcessService, IPythonExecutionService, ObservableExecutionResult, SpawnOptions } from './types'; +import { ExecutionResult, InterpreterInfomation, IProcessService, IPythonExecutionService, ObservableExecutionResult, PythonVersionInfo, SpawnOptions } from './types'; @injectable() export class PythonExecutionService implements IPythonExecutionService { - private readonly procService: IProcessService; - private readonly configService: IConfigurationService; private readonly fileSystem: IFileSystem; - constructor(private serviceContainer: IServiceContainer, private envVars: EnvironmentVariables | undefined, private resource?: Uri) { - this.procService = serviceContainer.get(IProcessService); - this.configService = serviceContainer.get(IConfigurationService); + constructor(private serviceContainer: IServiceContainer, private readonly procService: IProcessService, private readonly pythonPath: string) { this.fileSystem = serviceContainer.get(IFileSystem); } - public async getVersion(): Promise { - const versionService = this.serviceContainer.get(IInterpreterVersionService); - return versionService.getVersion(this.pythonPath, ''); + public async getInterpreterInformation(): Promise { + const file = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'interpreterInfo.py'); + try { + const [version, jsonValue] = await Promise.all([ + this.procService.exec(this.pythonPath, ['--version'], { mergeStdOutErr: true }) + .then(output => output.stdout.trim()), + this.procService.exec(this.pythonPath, [file], { mergeStdOutErr: true }) + .then(output => output.stdout.trim()) + ]); + + const json = JSON.parse(jsonValue) as { versionInfo: PythonVersionInfo; sysPrefix: string; sysVersion: string; is64Bit: boolean }; + return { + architecture: json.is64Bit ? Architecture.x64 : Architecture.x86, + path: this.pythonPath, + version, + sysVersion: json.sysVersion, + version_info: json.versionInfo, + sysPrefix: json.sysPrefix + }; + } catch (ex) { + console.error(`Failed to get interpreter information for '${this.pythonPath}'`, ex); + } } public async getExecutablePath(): Promise { // If we've passed the python file, then return the file. // This is because on mac if using the interpreter /usr/bin/python2.7 we can get a different value for the path - if (await this.fileSystem.fileExistsAsync(this.pythonPath)) { + if (await this.fileSystem.fileExists(this.pythonPath)) { return this.pythonPath; } - return this.procService.exec(this.pythonPath, ['-c', 'import sys;print(sys.executable)'], { env: this.envVars, throwOnStdErr: true }) + return this.procService.exec(this.pythonPath, ['-c', 'import sys;print(sys.executable)'], { throwOnStdErr: true }) .then(output => output.stdout.trim()); } public async isModuleInstalled(moduleName: string): Promise { - return this.procService.exec(this.pythonPath, ['-c', `import ${moduleName}`], { env: this.envVars, throwOnStdErr: true }) + return this.procService.exec(this.pythonPath, ['-c', `import ${moduleName}`], { throwOnStdErr: true }) .then(() => true).catch(() => false); } public execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult { const opts: SpawnOptions = { ...options }; - if (this.envVars) { - opts.env = this.envVars; - } return this.procService.execObservable(this.pythonPath, args, opts); } public execModuleObservable(moduleName: string, args: string[], options: SpawnOptions): ObservableExecutionResult { const opts: SpawnOptions = { ...options }; - if (this.envVars) { - opts.env = this.envVars; - } return this.procService.execObservable(this.pythonPath, ['-m', moduleName, ...args], opts); } public async exec(args: string[], options: SpawnOptions): Promise> { const opts: SpawnOptions = { ...options }; - if (this.envVars) { - opts.env = this.envVars; - } return this.procService.exec(this.pythonPath, args, opts); } public async execModule(moduleName: string, args: string[], options: SpawnOptions): Promise> { const opts: SpawnOptions = { ...options }; - if (this.envVars) { - opts.env = this.envVars; - } const result = await this.procService.exec(this.pythonPath, ['-m', moduleName, ...args], opts); // If a module is not installed we'll have something in stderr. @@ -80,7 +83,4 @@ export class PythonExecutionService implements IPythonExecutionService { return result; } - private get pythonPath(): string { - return this.configService.getSettings(this.resource).pythonPath; - } } diff --git a/src/client/common/process/pythonToolService.ts b/src/client/common/process/pythonToolService.ts index 9419b005250b..d4b2ccaaa8bb 100644 --- a/src/client/common/process/pythonToolService.ts +++ b/src/client/common/process/pythonToolService.ts @@ -5,23 +5,21 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; import { IServiceContainer } from '../../ioc/types'; import { ExecutionInfo } from '../types'; -import { IEnvironmentVariablesProvider } from '../variables/types'; -import { ExecutionResult, IProcessService, IPythonExecutionFactory, IPythonToolExecutionService, ObservableExecutionResult, SpawnOptions } from './types'; +import { ExecutionResult, IProcessServiceFactory, IPythonExecutionFactory, IPythonToolExecutionService, ObservableExecutionResult, SpawnOptions } from './types'; @injectable() export class PythonToolExecutionService implements IPythonToolExecutionService { - constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer) { } + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } public async execObservable(executionInfo: ExecutionInfo, options: SpawnOptions, resource: Uri): Promise> { if (options.env) { throw new Error('Environment variables are not supported'); } if (executionInfo.moduleName && executionInfo.moduleName.length > 0) { - const pythonExecutionService = await this.serviceContainer.get(IPythonExecutionFactory).create(resource); + const pythonExecutionService = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource }); return pythonExecutionService.execModuleObservable(executionInfo.moduleName, executionInfo.args, options); } else { - const env = await this.serviceContainer.get(IEnvironmentVariablesProvider).getEnvironmentVariables(resource); - const processService = this.serviceContainer.get(IProcessService); - return processService.execObservable(executionInfo.execPath!, executionInfo.args, { ...options, env }); + const processService = await this.serviceContainer.get(IProcessServiceFactory).create(resource); + return processService.execObservable(executionInfo.execPath!, executionInfo.args, { ...options }); } } public async exec(executionInfo: ExecutionInfo, options: SpawnOptions, resource: Uri): Promise> { @@ -29,12 +27,11 @@ export class PythonToolExecutionService implements IPythonToolExecutionService { throw new Error('Environment variables are not supported'); } if (executionInfo.moduleName && executionInfo.moduleName.length > 0) { - const pythonExecutionService = await this.serviceContainer.get(IPythonExecutionFactory).create(resource); + const pythonExecutionService = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource }); return pythonExecutionService.execModule(executionInfo.moduleName!, executionInfo.args, options); } else { - const env = await this.serviceContainer.get(IEnvironmentVariablesProvider).getEnvironmentVariables(resource); - const processService = this.serviceContainer.get(IProcessService); - return processService.exec(executionInfo.execPath!, executionInfo.args, { ...options, env }); + const processService = await this.serviceContainer.get(IProcessServiceFactory).create(resource); + return processService.exec(executionInfo.execPath!, executionInfo.args, { ...options }); } } } diff --git a/src/client/common/process/serviceRegistry.ts b/src/client/common/process/serviceRegistry.ts index dd4242fec55e..27684a20cc32 100644 --- a/src/client/common/process/serviceRegistry.ts +++ b/src/client/common/process/serviceRegistry.ts @@ -3,14 +3,14 @@ import { IServiceManager } from '../../ioc/types'; import { BufferDecoder } from './decoder'; -import { ProcessService } from './proc'; +import { ProcessServiceFactory } from './processFactory'; import { PythonExecutionFactory } from './pythonExecutionFactory'; import { PythonToolExecutionService } from './pythonToolService'; -import { IBufferDecoder, IProcessService, IPythonExecutionFactory, IPythonToolExecutionService } from './types'; +import { IBufferDecoder, IProcessServiceFactory, IPythonExecutionFactory, IPythonToolExecutionService } from './types'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IBufferDecoder, BufferDecoder); - serviceManager.addSingleton(IProcessService, ProcessService); + serviceManager.addSingleton(IProcessServiceFactory, ProcessServiceFactory); serviceManager.addSingleton(IPythonExecutionFactory, PythonExecutionFactory); serviceManager.addSingleton(IPythonToolExecutionService, PythonToolExecutionService); } diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index c4a79d6435a6..96862c8ca26d 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -4,6 +4,7 @@ import { ChildProcess, SpawnOptions as ChildProcessSpawnOptions } from 'child_process'; import { Observable } from 'rxjs/Observable'; import { CancellationToken, Uri } from 'vscode'; +import { Architecture } from '../platform/types'; import { ExecutionInfo } from '../types'; import { EnvironmentVariables } from '../variables/types'; @@ -34,22 +35,40 @@ export type ExecutionResult = { stderr?: T; }; -export const IProcessService = Symbol('IProcessService'); - export interface IProcessService { execObservable(file: string, args: string[], options?: SpawnOptions): ObservableExecutionResult; exec(file: string, args: string[], options?: SpawnOptions): Promise>; } -export const IPythonExecutionFactory = Symbol('IPythonExecutionFactory'); +export const IProcessServiceFactory = Symbol('IProcessServiceFactory'); -export interface IPythonExecutionFactory { - create(resource?: Uri): Promise; +export interface IProcessServiceFactory { + create(resource?: Uri): Promise; } +export const IPythonExecutionFactory = Symbol('IPythonExecutionFactory'); +export type ExecutionFactoryCreationOptions = { + resource?: Uri; + pythonPath?: string; +}; +export interface IPythonExecutionFactory { + create(options: ExecutionFactoryCreationOptions): Promise; +} +export type ReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; +// tslint:disable-next-line:interface-name +export type PythonVersionInfo = [number, number, number, ReleaseLevel]; +export type InterpreterInfomation = { + path: string; + version: string; + sysVersion: string; + architecture: Architecture; + version_info: PythonVersionInfo; + sysPrefix: string; +}; export const IPythonExecutionService = Symbol('IPythonExecutionService'); export interface IPythonExecutionService { + getInterpreterInformation(): Promise; getExecutablePath(): Promise; isModuleInstalled(moduleName: string): Promise; diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index b800774eeed4..42aa4e6b7d43 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -2,15 +2,18 @@ // Licensed under the MIT License. import { IServiceManager } from '../ioc/types'; +import { ApplicationEnvironment } from './application/applicationEnvironment'; import { ApplicationShell } from './application/applicationShell'; import { CommandManager } from './application/commandManager'; +import { DebugService } from './application/debugService'; import { DocumentManager } from './application/documentManager'; import { TerminalManager } from './application/terminalManager'; -import { IApplicationShell, ICommandManager, IDocumentManager, ITerminalManager, IWorkspaceService } from './application/types'; +import { IApplicationEnvironment, IApplicationShell, ICommandManager, IDebugService, IDocumentManager, ITerminalManager, IWorkspaceService } from './application/types'; import { WorkspaceService } from './application/workspace'; import { ConfigurationService } from './configuration/service'; import { ProductInstaller } from './installer/productInstaller'; import { Logger } from './logger'; +import { BrowserService } from './net/browser'; import { PersistentStateFactory } from './persistentState'; import { IS_64_BIT, IS_WINDOWS } from './platform/constants'; import { PathUtils } from './platform/pathUtils'; @@ -20,7 +23,7 @@ import { CommandPromptAndPowerShell } from './terminal/environmentActivationProv import { TerminalServiceFactory } from './terminal/factory'; import { TerminalHelper } from './terminal/helper'; import { ITerminalActivationCommandProvider, ITerminalHelper, ITerminalServiceFactory } from './terminal/types'; -import { IConfigurationService, ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, Is64Bit, IsWindows } from './types'; +import { IBrowserService, IConfigurationService, ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, Is64Bit, IsWindows } from './types'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); @@ -38,6 +41,9 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IWorkspaceService, WorkspaceService); serviceManager.addSingleton(IDocumentManager, DocumentManager); serviceManager.addSingleton(ITerminalManager, TerminalManager); + serviceManager.addSingleton(IDebugService, DebugService); + serviceManager.addSingleton(IApplicationEnvironment, ApplicationEnvironment); + serviceManager.addSingleton(IBrowserService, BrowserService); serviceManager.addSingleton(ITerminalHelper, TerminalHelper); serviceManager.addSingleton(ITerminalActivationCommandProvider, Bash, 'bashCShellFish'); diff --git a/src/client/telemetry/stopWatch.ts b/src/client/common/stopWatch.ts similarity index 80% rename from src/client/telemetry/stopWatch.ts rename to src/client/common/stopWatch.ts index 3e2a2132e94d..a72b3f3ae349 100644 --- a/src/client/telemetry/stopWatch.ts +++ b/src/client/common/stopWatch.ts @@ -8,4 +8,7 @@ export class StopWatch { public get elapsedTime() { return Date.now() - this.started; } + public reset(){ + this.started = Date.now(); + } } diff --git a/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts index 5ba858d624f4..ee45d5948c93 100644 --- a/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts @@ -4,12 +4,10 @@ import { injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; -import { PythonInterpreter } from '../../../interpreter/contracts'; import { IServiceContainer } from '../../../ioc/types'; import { IFileSystem } from '../../platform/types'; import { IConfigurationService } from '../../types'; -import { TerminalShellType } from '../types'; -import { ITerminalActivationCommandProvider } from '../types'; +import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; @injectable() export abstract class BaseActivationCommandProvider implements ITerminalActivationCommandProvider { @@ -25,7 +23,7 @@ export abstract class BaseActivationCommandProvider implements ITerminalActivati for (const scriptFileName of scriptFileNames) { // Generate scripts are found in the same directory as the interpreter. const scriptFile = path.join(path.dirname(pythonPath), scriptFileName); - const found = await fs.fileExistsAsync(scriptFile); + const found = await fs.fileExists(scriptFile); if (found) { return scriptFile; } diff --git a/src/client/common/terminal/environmentActivationProviders/bash.ts b/src/client/common/terminal/environmentActivationProviders/bash.ts index afa26f153ccd..3a1780df539f 100644 --- a/src/client/common/terminal/environmentActivationProviders/bash.ts +++ b/src/client/common/terminal/environmentActivationProviders/bash.ts @@ -10,12 +10,17 @@ import { BaseActivationCommandProvider } from './baseActivationProvider'; @injectable() export class Bash extends BaseActivationCommandProvider { - constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer) { + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super(serviceContainer); } public isShellSupported(targetShell: TerminalShellType): boolean { return targetShell === TerminalShellType.bash || + targetShell === TerminalShellType.gitbash || + targetShell === TerminalShellType.wsl || + targetShell === TerminalShellType.ksh || + targetShell === TerminalShellType.zsh || targetShell === TerminalShellType.cshell || + targetShell === TerminalShellType.tcshell || targetShell === TerminalShellType.fish; } public async getActivationCommands(resource: Uri | undefined, targetShell: TerminalShellType): Promise { @@ -28,9 +33,14 @@ export class Bash extends BaseActivationCommandProvider { private getScriptsInOrderOfPreference(targetShell: TerminalShellType): string[] { switch (targetShell) { + case TerminalShellType.wsl: + case TerminalShellType.ksh: + case TerminalShellType.zsh: + case TerminalShellType.gitbash: case TerminalShellType.bash: { return ['activate.sh', 'activate']; } + case TerminalShellType.tcshell: case TerminalShellType.cshell: { return ['activate.csh']; } diff --git a/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts b/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts index b7d8f24d86b9..f9c8ca77311b 100644 --- a/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts +++ b/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts @@ -12,7 +12,7 @@ import { BaseActivationCommandProvider } from './baseActivationProvider'; @injectable() export class CommandPromptAndPowerShell extends BaseActivationCommandProvider { - constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer) { + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super(serviceContainer); } public isShellSupported(targetShell: TerminalShellType): boolean { @@ -41,7 +41,7 @@ export class CommandPromptAndPowerShell extends BaseActivationCommandProvider { const powershellExe = targetShell === TerminalShellType.powershell ? 'powershell' : 'pwsh'; const activationCmd = scriptFile.fileToCommandArgument(); return [ - `& cmd /k "${activationCmd} & ${powershellExe}"` + `& cmd /k "${activationCmd.replace(/"/g, '""')} & ${powershellExe}"` ]; } else { // Powershell on non-windows os, we cannot execute the batch file. diff --git a/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts index 51cb12e88e7e..e3f73deecf90 100644 --- a/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts @@ -8,8 +8,7 @@ import { IServiceContainer } from '../../../ioc/types'; import '../../extensions'; import { IPlatformService } from '../../platform/types'; import { IConfigurationService } from '../../types'; -import { TerminalShellType } from '../types'; -import { ITerminalActivationCommandProvider } from '../types'; +import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; @injectable() export class CondaActivationCommandProvider implements ITerminalActivationCommandProvider { @@ -29,8 +28,15 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman const isWindows = this.serviceContainer.get(IPlatformService).isWindows; if (targetShell === TerminalShellType.powershell || targetShell === TerminalShellType.powershellCore) { + if (!isWindows) { + return; + } // https://github.com/conda/conda/issues/626 - return; + // On windows, the solution is to go into cmd, then run the batch (.bat) file and go back into powershell. + const powershellExe = targetShell === TerminalShellType.powershell ? 'powershell' : 'pwsh'; + return [ + `& cmd /k "activate ${envInfo.name.toCommandArgument().replace(/"/g, '""')} & ${powershellExe}"` + ]; } else if (targetShell === TerminalShellType.fish) { // https://github.com/conda/conda/blob/be8c08c083f4d5e05b06bd2689d2cd0d410c2ffe/shell/etc/fish/conf.d/conda.fish#L18-L28 return [`conda activate ${envInfo.name.toCommandArgument()}`]; diff --git a/src/client/common/terminal/helper.ts b/src/client/common/terminal/helper.ts index bc2f9c78a237..af0f50484d38 100644 --- a/src/client/common/terminal/helper.ts +++ b/src/client/common/terminal/helper.ts @@ -14,23 +14,33 @@ import { ITerminalActivationCommandProvider, ITerminalHelper, TerminalShellType // Types of shells can be found here: // 1. https://wiki.ubuntu.com/ChangingShells -const IS_BASH = /(bash.exe$|wsl.exe$|bash$|zsh$|ksh$)/i; +const IS_GITBASH = /(gitbash.exe$)/i; +const IS_BASH = /(bash.exe$|bash$)/i; +const IS_WSL = /(wsl.exe$)/i; +const IS_ZSH = /(zsh$)/i; +const IS_KSH = /(ksh$)/i; const IS_COMMAND = /cmd.exe$/i; const IS_POWERSHELL = /(powershell.exe$|powershell$)/i; const IS_POWERSHELL_CORE = /(pwsh.exe$|pwsh$)/i; const IS_FISH = /(fish$)/i; const IS_CSHELL = /(csh$)/i; +const IS_TCSHELL = /(tcsh$)/i; @injectable() export class TerminalHelper implements ITerminalHelper { private readonly detectableShells: Map; - constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer) { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { this.detectableShells = new Map(); this.detectableShells.set(TerminalShellType.powershell, IS_POWERSHELL); + this.detectableShells.set(TerminalShellType.gitbash, IS_GITBASH); this.detectableShells.set(TerminalShellType.bash, IS_BASH); + this.detectableShells.set(TerminalShellType.wsl, IS_WSL); + this.detectableShells.set(TerminalShellType.zsh, IS_ZSH); + this.detectableShells.set(TerminalShellType.ksh, IS_KSH); this.detectableShells.set(TerminalShellType.commandPrompt, IS_COMMAND); this.detectableShells.set(TerminalShellType.fish, IS_FISH); + this.detectableShells.set(TerminalShellType.tcshell, IS_TCSHELL); this.detectableShells.set(TerminalShellType.cshell, IS_CSHELL); this.detectableShells.set(TerminalShellType.powershellCore, IS_POWERSHELL_CORE); } diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index 5ec43970a75a..3927578f9adc 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -3,22 +3,27 @@ import { inject, injectable } from 'inversify'; import { Disposable, Event, EventEmitter, Terminal, Uri } from 'vscode'; +import '../../common/extensions'; +import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; +import { captureTelemetry } from '../../telemetry'; +import { TERMINAL_CREATE } from '../../telemetry/constants'; import { ITerminalManager } from '../application/types'; -import { IDisposableRegistry } from '../types'; +import { sleep } from '../core.utils'; +import { IConfigurationService, IDisposableRegistry } from '../types'; import { ITerminalHelper, ITerminalService, TerminalShellType } from './types'; @injectable() export class TerminalService implements ITerminalService, Disposable { private terminal?: Terminal; - private terminalShellType: TerminalShellType; + private terminalShellType!: TerminalShellType; private terminalClosed = new EventEmitter(); private terminalManager: ITerminalManager; private terminalHelper: ITerminalHelper; public get onDidCloseTerminal(): Event { return this.terminalClosed.event; } - constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, private resource?: Uri, private title: string = 'Python') { @@ -44,11 +49,11 @@ export class TerminalService implements ITerminalService, Disposable { this.terminal!.show(true); this.terminal!.sendText(text); } - public async show(): Promise { - await this.ensureTerminal(); - this.terminal!.show(true); + public async show(preserveFocus: boolean = true): Promise { + await this.ensureTerminal(preserveFocus); + this.terminal!.show(preserveFocus); } - private async ensureTerminal(): Promise { + private async ensureTerminal(preserveFocus: boolean = true): Promise { if (this.terminal) { return; } @@ -62,16 +67,19 @@ export class TerminalService implements ITerminalService, Disposable { const activationCommamnds = await this.terminalHelper.getEnvironmentActivationCommands(this.terminalShellType, this.resource); if (activationCommamnds) { for (const command of activationCommamnds!) { - this.terminal!.show(true); + this.terminal!.show(preserveFocus); this.terminal!.sendText(command); // Give the command some time to complete. // Its been observed that sending commands too early will strip some text off. - await new Promise(resolve => setTimeout(resolve, 500)); + const delay = (this.terminalShellType === TerminalShellType.powershell || TerminalShellType.powershellCore) ? 1000 : 500; + await sleep(delay); } } - this.terminal!.show(true); + this.terminal!.show(preserveFocus); + + this.sendTelemetry().ignoreErrors(); } private terminalCloseHandler(terminal: Terminal) { if (terminal === this.terminal) { @@ -79,4 +87,12 @@ export class TerminalService implements ITerminalService, Disposable { this.terminal = undefined; } } + + private async sendTelemetry() { + const pythonPath = this.serviceContainer.get(IConfigurationService).getSettings(this.resource).pythonPath; + const interpreterInfo = await this.serviceContainer.get(IInterpreterService).getInterpreterDetails(pythonPath); + const pythonVersion = interpreterInfo.version_info ? interpreterInfo.version_info.join('.') : undefined; + const interpreterType = interpreterInfo.type; + captureTelemetry(TERMINAL_CREATE, { terminal: this.terminalShellType, pythonVersion, interpreterType }); + } } diff --git a/src/client/common/terminal/types.ts b/src/client/common/terminal/types.ts index 5a2914fd2ab0..a13569d6094f 100644 --- a/src/client/common/terminal/types.ts +++ b/src/client/common/terminal/types.ts @@ -3,23 +3,27 @@ // Licensed under the MIT License. import { Event, Terminal, Uri } from 'vscode'; -import { PythonInterpreter } from '../../interpreter/contracts'; export enum TerminalShellType { - powershell = 1, - powershellCore = 2, - commandPrompt = 3, - bash = 4, - fish = 5, - cshell = 6, - other = 7 + powershell = 'powershell', + powershellCore = 'powershellCore', + commandPrompt = 'commandPrompt', + gitbash = 'gitbash', + bash = 'bash', + zsh = 'zsh', + ksh = 'ksh', + fish = 'fish', + cshell = 'cshell', + tcshell = 'tshell', + wsl = 'wsl', + other = 'other' } export interface ITerminalService { readonly onDidCloseTerminal: Event; sendCommand(command: string, args: string[]): Promise; sendText(text: string): Promise; - show(): Promise; + show(preserveFocus?: boolean): Promise; } export const ITerminalServiceFactory = Symbol('ITerminalServiceFactory'); diff --git a/src/client/common/types.ts b/src/client/common/types.ts index a53d15370fc1..83dee6718c57 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -3,14 +3,16 @@ // Licensed under the MIT License. import { Socket } from 'net'; -import { ConfigurationTarget, DiagnosticSeverity, Disposable, Uri } from 'vscode'; +import { ConfigurationTarget, DiagnosticSeverity, Disposable, ExtensionContext, OutputChannel, Uri } from 'vscode'; import { EnvironmentVariables } from './variables/types'; export const IOutputChannel = Symbol('IOutputChannel'); +export interface IOutputChannel extends OutputChannel { } export const IDocumentSymbolProvider = Symbol('IDocumentSymbolProvider'); export const IsWindows = Symbol('IS_WINDOWS'); export const Is64Bit = Symbol('Is64Bit'); export const IDisposableRegistry = Symbol('IDiposableRegistry'); +export type IDisposableRegistry = Disposable[]; export const IMemento = Symbol('IGlobalMemento'); export const GLOBAL_MEMENTO = Symbol('IGlobalMemento'); export const WORKSPACE_MEMENTO = Symbol('IWorkspaceMemento'); @@ -23,8 +25,8 @@ export interface IPersistentState { export const IPersistentStateFactory = Symbol('IPersistentStateFactory'); export interface IPersistentStateFactory { - createGlobalPersistentState(key: string, defaultValue: T): IPersistentState; - createWorkspacePersistentState(key: string, defaultValue: T): IPersistentState; + createGlobalPersistentState(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState; + createWorkspacePersistentState(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState; } export type ExecutionInfo = { @@ -34,11 +36,18 @@ export type ExecutionInfo = { product?: Product; }; +export enum LogLevel { + Information = 'Information', + Error = 'Error', + Warning = 'Warning' +} + export const ILogger = Symbol('ILogger'); export interface ILogger { logError(message: string, error?: Error); logWarning(message: string, error?: Error); + logInformation(message: string, error?: Error); } export enum InstallerResponse { @@ -47,6 +56,14 @@ export enum InstallerResponse { Ignore } +export enum ProductType { + Linter = 'Linter', + Formatter = 'Formatter', + TestFramework = 'TestFramework', + RefactoringLibrary = 'RefactoringLibrary', + WorkspaceSymbols = 'WorkspaceSymbols' +} + export enum Product { pytest = 1, nosetest = 2, @@ -62,7 +79,8 @@ export enum Product { unittest = 12, ctags = 13, rope = 14, - isort = 15 + isort = 15, + black = 16 } export enum ModuleNamePurpose { @@ -82,7 +100,9 @@ export interface IInstaller { export const IPathUtils = Symbol('IPathUtils'); export interface IPathUtils { + readonly delimiter: string; getPathVariableName(): 'Path' | 'PATH'; + basename(pathValue: string, ext?: string): string; } export const ICurrentProcess = Symbol('ICurrentProcess'); @@ -98,20 +118,22 @@ export interface IPythonSettings { readonly pythonPath: string; readonly venvPath: string; readonly venvFolders: string[]; + readonly downloadLanguageServer: boolean; + readonly jediEnabled: boolean; readonly jediPath: string; readonly jediMemoryLimit: number; readonly devOptions: string[]; readonly linting: ILintingSettings; readonly formatting: IFormattingSettings; readonly unitTest: IUnitTestSettings; - readonly autoComplete: IAutoCompeteSettings; + readonly autoComplete: IAutoCompleteSettings; readonly terminal: ITerminalSettings; readonly sortImports: ISortImportSettings; readonly workspaceSymbols: IWorkspaceSymbolSettings; readonly envFile: string; - readonly disablePromptForFeatures: string[]; readonly disableInstallationChecks: boolean; readonly globalModuleInstallation: boolean; + readonly analysis: IAnalysisSettings; } export interface ISortImportSettings { readonly path: string; @@ -121,7 +143,6 @@ export interface ISortImportSettings { export interface IUnitTestSettings { readonly promptToConfigure: boolean; readonly debugPort: number; - readonly debugHost?: string; readonly nosetestsEnabled: boolean; nosetestPath: string; nosetestArgs: string[]; @@ -131,6 +152,8 @@ export interface IUnitTestSettings { readonly unittestEnabled: boolean; unittestArgs: string[]; cwd?: string; + readonly useExperimentalDebugger?: boolean; + readonly autoTestDiscoverOnSaveEnabled: boolean; } export interface IPylintCategorySeverity { readonly convention: DiagnosticSeverity; @@ -189,13 +212,17 @@ export interface IFormattingSettings { readonly provider: string; autopep8Path: string; readonly autopep8Args: string[]; + blackPath: string; + readonly blackArgs: string[]; yapfPath: string; readonly yapfArgs: string[]; } -export interface IAutoCompeteSettings { +export interface IAutoCompleteSettings { readonly addBrackets: boolean; readonly extraPaths: string[]; readonly preloadModules: string[]; + readonly showAdvancedMembers: boolean; + readonly typeshedPaths: string[]; } export interface IWorkspaceSymbolSettings { readonly enabled: boolean; @@ -210,9 +237,17 @@ export interface ITerminalSettings { readonly launchArgs: string[]; readonly activateEnvironment: boolean; } +export interface IAnalysisSettings { + readonly openFilesOnly: boolean; + readonly typeshedPaths: string[]; + readonly errors: string[]; + readonly warnings: string[]; + readonly information: string[]; + readonly disabled: string[]; + readonly traceLogging: boolean; +} export const IConfigurationService = Symbol('IConfigurationService'); - export interface IConfigurationService { getSettings(resource?: Uri): IPythonSettings; isTestExecution(): boolean; @@ -222,5 +257,33 @@ export interface IConfigurationService { export const ISocketServer = Symbol('ISocketServer'); export interface ISocketServer extends Disposable { readonly client: Promise; - Start(options?: { port?: number, host?: string }): Promise; + Start(options?: { port?: number; host?: string }): Promise; +} + +export const IExtensionContext = Symbol('ExtensionContext'); +export interface IExtensionContext extends ExtensionContext { } + +export const IBrowserService = Symbol('IBrowserService'); +export interface IBrowserService { + launch(url: string): void; +} + +export const IExperimentalDebuggerBanner = Symbol('IExperimentalDebuggerBanner'); +export interface IExperimentalDebuggerBanner { + enabled: boolean; + initialize(): void; + showBanner(): Promise; + shouldShowBanner(): Promise; + disable(): Promise; + launchSurvey(): Promise; +} + +export const IPythonExtensionBanner = Symbol('IPythonExtensionBanner'); +export interface IPythonExtensionBanner { + enabled: boolean; + shownCount: Promise; + optionLabels: string[]; + showBanner(): Promise; } +export const BANNER_NAME_LS_SURVEY: string = 'LSSurveyBanner'; +export const BANNER_NAME_PROPOSE_LS: string = 'ProposeLS'; diff --git a/src/client/common/utils.ts b/src/client/common/utils.ts index 3e803b388466..4cd5f2c027e0 100644 --- a/src/client/common/utils.ts +++ b/src/client/common/utils.ts @@ -1,6 +1,7 @@ 'use strict'; // tslint:disable: no-any one-line no-suspicious-comment prefer-template prefer-const no-unnecessary-callback-wrapper no-function-expression no-string-literal no-control-regex no-shadowed-variable +import * as crypto from 'crypto'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; @@ -111,3 +112,18 @@ export function arePathsSame(path1: string, path2: string) { return path1 === path2; } } + +function getRandom(): number { + let num: number = 0; + + const buf: Buffer = crypto.randomBytes(2); + num = (buf.readUInt8(0) << 8) + buf.readUInt8(1); + + const maxValue: number = Math.pow(16, 4) - 1; + return (num / maxValue); +} + +export function getRandomBetween(min: number = 0, max: number = 10): number { + const randomVal: number = getRandom(); + return min + (randomVal * (max - min)); +} diff --git a/src/client/common/variables/environment.ts b/src/client/common/variables/environment.ts index 0ad9443380e7..245d2f3c15e2 100644 --- a/src/client/common/variables/environment.ts +++ b/src/client/common/variables/environment.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as dotenv from 'dotenv'; import * as fs from 'fs-extra'; import { inject, injectable } from 'inversify'; import * as path from 'path'; @@ -10,7 +11,7 @@ import { EnvironmentVariables, IEnvironmentVariablesService } from './types'; @injectable() export class EnvironmentVariablesService implements IEnvironmentVariablesService { private readonly pathVariable: 'PATH' | 'Path'; - constructor( @inject(IPathUtils) pathUtils: IPathUtils) { + constructor(@inject(IPathUtils) pathUtils: IPathUtils) { this.pathVariable = pathUtils.getPathVariableName(); } public async parseFile(filePath: string): Promise { @@ -21,14 +22,7 @@ export class EnvironmentVariablesService implements IEnvironmentVariablesService if (!fs.lstatSync(filePath).isFile()) { return undefined; } - return new Promise((resolve, reject) => { - fs.readFile(filePath, 'utf8', (error, data) => { - if (error) { - return reject(error); - } - resolve(parseEnvironmentVariables(data)); - }); - }); + return dotenv.parse(await fs.readFile(filePath)); } public mergeVariables(source: EnvironmentVariables, target: EnvironmentVariables) { if (!target) { @@ -67,22 +61,3 @@ export class EnvironmentVariablesService implements IEnvironmentVariablesService return vars; } } - -function parseEnvironmentVariables(contents: string): EnvironmentVariables | undefined { - if (typeof contents !== 'string' || contents.length === 0) { - return undefined; - } - - const env = {} as EnvironmentVariables; - contents.split('\n').forEach(line => { - const match = line.match(/^\s*([\w\.\-]+)\s*=\s*(.*)?\s*$/); - if (match !== null) { - let value = typeof match[2] === 'string' ? match[2] : ''; - if (value.length > 0 && value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') { - value = value.replace(/\\n/gm, '\n'); - } - env[match[1]] = value.replace(/(^['"]|['"]$)/g, ''); - } - }); - return env; -} diff --git a/src/client/common/variables/environmentVariablesProvider.ts b/src/client/common/variables/environmentVariablesProvider.ts index f1e2ca6541bf..a10bb94c0a65 100644 --- a/src/client/common/variables/environmentVariablesProvider.ts +++ b/src/client/common/variables/environmentVariablesProvider.ts @@ -14,7 +14,7 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid private fileWatchers = new Map(); private disposables: Disposable[] = []; private changeEventEmitter: EventEmitter; - constructor( @inject(IEnvironmentVariablesService) private envVarsService: IEnvironmentVariablesService, + constructor(@inject(IEnvironmentVariablesService) private envVarsService: IEnvironmentVariablesService, @inject(IDisposableRegistry) disposableRegistry: Disposable[], @inject(IsWindows) private isWidows: boolean, @inject(ICurrentProcess) private process: ICurrentProcess) { disposableRegistry.push(this); diff --git a/src/client/common/variables/types.ts b/src/client/common/variables/types.ts index 3b5ee44a3481..1130ffd33c8c 100644 --- a/src/client/common/variables/types.ts +++ b/src/client/common/variables/types.ts @@ -39,5 +39,5 @@ export const IEnvironmentVariablesProvider = Symbol('IEnvironmentVariablesProvid export interface IEnvironmentVariablesProvider { onDidEnvironmentVariablesChange: Event; - getEnvironmentVariables(resource?: Uri): Promise; + getEnvironmentVariables(resource?: Uri): Promise; } diff --git a/src/client/debugger/Common/Contracts.ts b/src/client/debugger/Common/Contracts.ts index bd5c697fede4..2445a1cffebb 100644 --- a/src/client/debugger/Common/Contracts.ts +++ b/src/client/debugger/Common/Contracts.ts @@ -1,14 +1,15 @@ -// tslint:disable:interface-name member-access no-single-line-block-comment no-any no-stateless-class member-ordering prefer-method-signature +// tslint:disable:interface-name member-access no-single-line-block-comment no-any no-stateless-class member-ordering prefer-method-signature no-unnecessary-class 'use strict'; import { ChildProcess } from 'child_process'; import * as net from 'net'; import { OutputEvent } from 'vscode-debugadapter'; -import { DebugProtocol } from 'vscode-debugprotocol'; +import { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; import { DebuggerPerformanceTelemetry, DebuggerTelemetry } from '../../telemetry/types'; +import { ExperimentalDebuggerType } from './constants'; export class TelemetryEvent extends OutputEvent { - body: { + body!: { /** The category of output (such as: 'console', 'stdout', 'stderr', 'telemetry'). If not specified, 'console' is assumed. */ category: string; /** The output to report. */ @@ -23,19 +24,36 @@ export class TelemetryEvent extends OutputEvent { } } } -export const DjangoApp = 'DJANGO'; + +export const VALID_DEBUG_OPTIONS = [ + 'WaitOnAbnormalExit', + 'WaitOnNormalExit', + 'RedirectOutput', + 'DebugStdLib', + 'BreakOnSystemExitZero', + 'DjangoDebugging', + 'Django' +]; + export enum DebugFlags { None = 0, IgnoreCommandBursts = 1 } -export class DebugOptions { - public static get WaitOnAbnormalExit(): string { return 'WaitOnAbnormalExit'; } - public static get WaitOnNormalExit(): string { return 'WaitOnNormalExit'; } - public static get RedirectOutput(): string { return 'RedirectOutput'; } - public static get DjangoDebugging(): string { return 'DjangoDebugging'; } - public static get DebugStdLib(): string { return 'DebugStdLib'; } - public static get BreakOnSystemExitZero(): string { return 'BreakOnSystemExitZero'; } +export enum DebugOptions { + WaitOnAbnormalExit = 'WaitOnAbnormalExit', + WaitOnNormalExit = 'WaitOnNormalExit', + RedirectOutput = 'RedirectOutput', + Django = 'Django', + DjangoDebugging = 'DjangoDebugging', + Jinja = 'Jinja', + DebugStdLib = 'DebugStdLib', + BreakOnSystemExitZero = 'BreakOnSystemExitZero', + Sudo = 'Sudo', + Pyramid = 'Pyramid', + FixFilePathCase = 'FixFilePathCase', + WindowsClient = 'WindowsClient', + UnixClient = 'UnixClient' } export interface ExceptionHandling { @@ -44,39 +62,71 @@ export interface ExceptionHandling { unhandled: string[]; } -export type DebuggerType = 'python' | 'pythonExperimental'; +export type DebuggerType = 'python' | typeof ExperimentalDebuggerType; + +export interface AdditionalLaunchDebugOptions { + redirectOutput?: boolean; + django?: boolean; + gevent?: boolean; + jinja?: boolean; + debugStdLib?: boolean; + sudo?: boolean; + pyramid?: boolean; +} + +export interface AdditionalAttachDebugOptions { + redirectOutput?: boolean; + django?: boolean; + gevent?: boolean; + jinja?: boolean; + debugStdLib?: boolean; +} -export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { +export interface BaseLaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { type?: DebuggerType; /** An absolute path to the program to debug. */ module?: string; - program: string; + program?: string; pythonPath: string; /** Automatically stop target after launch. If not specified, target does not stop. */ stopOnEntry?: boolean; args: string[]; - applicationType?: string; cwd?: string; - debugOptions?: string[]; + debugOptions?: DebugOptions[]; env?: Object; envFile: string; - exceptionHandling?: ExceptionHandling; console?: 'none' | 'integratedTerminal' | 'externalTerminal'; port?: number; host?: string; logToFile?: boolean; } -export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments { +export interface LaunchRequestArgumentsV1 extends BaseLaunchRequestArguments { + exceptionHandling?: ExceptionHandling; +} + +export interface LaunchRequestArguments extends BaseLaunchRequestArguments, AdditionalLaunchDebugOptions { +} + +export interface BaseAttachRequestArguments extends DebugProtocol.AttachRequestArguments { + type?: DebuggerType; /** An absolute path to local directory with source. */ - localRoot: string; - remoteRoot: string; port?: number; host?: string; - secret?: string; logToFile?: boolean; + debugOptions?: DebugOptions[]; +} +export interface AttachRequestArgumentsV1 extends BaseAttachRequestArguments { + secret?: string; + localRoot: string; + remoteRoot: string; } +export interface AttachRequestArguments extends BaseAttachRequestArguments, AdditionalAttachDebugOptions { + localRoot?: string; + remoteRoot?: string; + pathMappings?: { localRoot: string; remoteRoot: string }[]; +} export interface IDebugServer { port: number; host?: string; diff --git a/src/client/debugger/Common/Utils.ts b/src/client/debugger/Common/Utils.ts index 6d6d785f7e36..700ebb055042 100644 --- a/src/client/debugger/Common/Utils.ts +++ b/src/client/debugger/Common/Utils.ts @@ -110,6 +110,9 @@ export function getPythonExecutable(pythonPath: string): string { } function isValidPythonPath(pythonPath): boolean { + if (fs.existsSync(pythonPath)) { + return true; + } try { const output = child_process.execFileSync(pythonPath, ['-c', 'print(1234)'], { encoding: 'utf8' }); return output.startsWith('1234'); diff --git a/src/client/debugger/Common/constants.ts b/src/client/debugger/Common/constants.ts new file mode 100644 index 000000000000..e153527c27d1 --- /dev/null +++ b/src/client/debugger/Common/constants.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { EXTENSION_ROOT_DIR } from '../../common/constants'; + +export const PTVSD_PATH = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd'); +export const ExperimentalDebuggerType = 'pythonExperimental'; diff --git a/src/client/debugger/Common/telemetry.ts b/src/client/debugger/Common/telemetry.ts index cecde60b2658..9cb9fd3dc24a 100644 --- a/src/client/debugger/Common/telemetry.ts +++ b/src/client/debugger/Common/telemetry.ts @@ -4,13 +4,13 @@ // tslint:disable:no-function-expression no-any no-invalid-this no-use-before-declare import { DebugSession, StoppedEvent } from 'vscode-debugadapter'; +import { StopWatch } from '../../common/stopWatch'; import { DEBUGGER_PERFORMANCE } from '../../telemetry/constants'; -import { StopWatch } from '../../telemetry/stopWatch'; import { DebuggerPerformanceTelemetry } from '../../telemetry/types'; import { TelemetryEvent } from './Contracts'; type DebugAction = 'stepIn' | 'stepOut' | 'continue' | 'next' | 'launch'; -type DebugPerformanceInformation = { action: DebugAction, timer: StopWatch }; +type DebugPerformanceInformation = { action: DebugAction; timer: StopWatch }; const executionStack: DebugPerformanceInformation[] = []; diff --git a/src/client/debugger/DebugClients/DebugFactory.ts b/src/client/debugger/DebugClients/DebugFactory.ts index 21229631727e..f8c3b6378e9c 100644 --- a/src/client/debugger/DebugClients/DebugFactory.ts +++ b/src/client/debugger/DebugClients/DebugFactory.ts @@ -1,17 +1,25 @@ import { DebugSession } from 'vscode-debugadapter'; import { AttachRequestArguments, LaunchRequestArguments } from '../Common/Contracts'; +import { IDebugLauncherScriptProvider } from '../types'; import { DebugClient } from './DebugClient'; -import { DebuggerLauncherScriptProvider, DebuggerV2LauncherScriptProvider, NoDebugLauncherScriptProvider } from './launcherProvider'; +import { DebuggerLauncherScriptProvider, NoDebugLauncherScriptProvider } from './launcherProvider'; import { LocalDebugClient } from './LocalDebugClient'; +import { LocalDebugClientV2 } from './localDebugClientV2'; import { NonDebugClient } from './NonDebugClient'; +import { NonDebugClientV2 } from './nonDebugClientV2'; import { RemoteDebugClient } from './RemoteDebugClient'; export function CreateLaunchDebugClient(launchRequestOptions: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean): DebugClient<{}> { + let launchScriptProvider: IDebugLauncherScriptProvider; + let debugClientClass: typeof LocalDebugClient; if (launchRequestOptions.noDebug === true) { - return new NonDebugClient(launchRequestOptions, debugSession, canLaunchTerminal, new NoDebugLauncherScriptProvider()); + launchScriptProvider = new NoDebugLauncherScriptProvider(); + debugClientClass = launchRequestOptions.type === 'pythonExperimental' ? NonDebugClientV2 : NonDebugClient; + } else { + launchScriptProvider = new DebuggerLauncherScriptProvider(); + debugClientClass = launchRequestOptions.type === 'pythonExperimental' ? LocalDebugClientV2 : LocalDebugClient; } - const launchScriptProvider = launchRequestOptions.type === 'pythonExperimental' ? new DebuggerV2LauncherScriptProvider() : new DebuggerLauncherScriptProvider(); - return new LocalDebugClient(launchRequestOptions, debugSession, canLaunchTerminal, launchScriptProvider); + return new debugClientClass(launchRequestOptions, debugSession, canLaunchTerminal, launchScriptProvider); } export function CreateAttachDebugClient(attachRequestOptions: AttachRequestArguments, debugSession: DebugSession): DebugClient<{}> { return new RemoteDebugClient(attachRequestOptions, debugSession); diff --git a/src/client/debugger/DebugClients/LocalDebugClient.ts b/src/client/debugger/DebugClients/LocalDebugClient.ts index 10b8b2de24c9..661c50f12c76 100644 --- a/src/client/debugger/DebugClients/LocalDebugClient.ts +++ b/src/client/debugger/DebugClients/LocalDebugClient.ts @@ -1,15 +1,14 @@ -import * as child_process from 'child_process'; -import { ChildProcess } from 'child_process'; +import { ChildProcess, spawn } from 'child_process'; import * as path from 'path'; import { DebugSession, OutputEvent } from 'vscode-debugadapter'; import { DebugProtocol } from 'vscode-debugprotocol'; -import { EXTENSION_ROOT_DIR } from '../../common/constants'; import { open } from '../../common/open'; import { PathUtils } from '../../common/platform/pathUtils'; import { CurrentProcess } from '../../common/process/currentProcess'; import { EnvironmentVariablesService } from '../../common/variables/environment'; import { IServiceContainer } from '../../ioc/types'; -import { IDebugServer, IPythonProcess, LaunchRequestArguments } from '../Common/Contracts'; +import { PTVSD_PATH } from '../Common/constants'; +import { DebugOptions, IDebugServer, IPythonProcess, LaunchRequestArguments, VALID_DEBUG_OPTIONS } from '../Common/Contracts'; import { IS_WINDOWS } from '../Common/Utils'; import { BaseDebugServer } from '../DebugServers/BaseDebugServer'; import { LocalDebugServer } from '../DebugServers/LocalDebugServer'; @@ -18,12 +17,6 @@ import { IDebugLauncherScriptProvider } from '../types'; import { DebugClient, DebugType } from './DebugClient'; import { DebugClientHelper } from './helper'; -const VALID_DEBUG_OPTIONS = [ - 'RedirectOutput', - 'DebugStdLib', - 'BreakOnSystemExitZero', - 'DjangoDebugging']; - enum DebugServerStatus { Unknown = 1, Running = 2, @@ -31,8 +24,8 @@ enum DebugServerStatus { } export class LocalDebugClient extends DebugClient { - protected pyProc: child_process.ChildProcess | undefined; - protected pythonProcess: IPythonProcess; + protected pyProc: ChildProcess | undefined; + protected pythonProcess!: IPythonProcess; protected debugServer: BaseDebugServer | undefined; private get debugServerStatus(): DebugServerStatus { if (this.debugServer && this.debugServer!.IsRunning) { @@ -88,8 +81,7 @@ export class LocalDebugClient extends DebugClient { const environmentVariables = await helper.getEnvironmentVariables(this.args); if (this.args.type === 'pythonExperimental') { // Import the PTVSD debugger, allowing users to use their own latest copies. - const experimentalPTVSDPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd'); - environmentVariablesService.appendPythonPath(environmentVariables, experimentalPTVSDPath); + environmentVariablesService.appendPythonPath(environmentVariables, PTVSD_PATH); } // tslint:disable-next-line:max-func-body-length cyclomatic-complexity no-any return new Promise((resolve, reject) => { @@ -102,10 +94,7 @@ export class LocalDebugClient extends DebugClient { if (typeof this.args.pythonPath === 'string' && this.args.pythonPath.trim().length > 0) { pythonPath = this.args.pythonPath; } - const ptVSToolsFilePath = this.launcherScriptProvider.getLauncherFilePath(); - const launcherArgs = this.buildLauncherArguments(); - - const args = [ptVSToolsFilePath, processCwd, dbgServer.port.toString(), '34806ad9-833a-4524-8cd6-18ca4aa74f14'].concat(launcherArgs); + const args = this.buildLaunchArguments(processCwd, dbgServer.port); switch (this.args.console) { case 'externalTerminal': case 'integratedTerminal': { @@ -114,7 +103,7 @@ export class LocalDebugClient extends DebugClient { break; } default: { - this.pyProc = child_process.spawn(pythonPath, args, { cwd: processCwd, env: environmentVariables }); + this.pyProc = spawn(pythonPath, args, { cwd: processCwd, env: environmentVariables }); this.handleProcessOutput(this.pyProc!, reject); // Here we wait for the application to connect to the socket server. @@ -162,19 +151,35 @@ export class LocalDebugClient extends DebugClient { let x = 0; }); } + private buildLaunchArguments(cwd: string, debugPort: number): string[] { + return [...this.buildDebugArguments(cwd, debugPort), ...this.buildStandardArguments()]; + } + // tslint:disable-next-line:member-ordering - protected buildLauncherArguments(): string[] { - const vsDebugOptions = ['RedirectOutput']; + protected buildDebugArguments(cwd: string, debugPort: number): string[] { + const ptVSToolsFilePath = this.launcherScriptProvider.getLauncherFilePath(); + const vsDebugOptions: string[] = [DebugOptions.RedirectOutput]; if (Array.isArray(this.args.debugOptions)) { this.args.debugOptions.filter(opt => VALID_DEBUG_OPTIONS.indexOf(opt) >= 0) .forEach(item => vsDebugOptions.push(item)); } - + const djangoIndex = vsDebugOptions.indexOf(DebugOptions.Django); + // PTVSD expects the string `DjangoDebugging` + if (djangoIndex >= 0) { + vsDebugOptions[djangoIndex] = 'DjangoDebugging'; + } + return [ptVSToolsFilePath, cwd, debugPort.toString(), '34806ad9-833a-4524-8cd6-18ca4aa74f14', vsDebugOptions.join(',')]; + } + // tslint:disable-next-line:member-ordering + protected buildStandardArguments() { const programArgs = Array.isArray(this.args.args) && this.args.args.length > 0 ? this.args.args : []; if (typeof this.args.module === 'string' && this.args.module.length > 0) { - return [vsDebugOptions.join(','), '-m', this.args.module].concat(programArgs); + return ['-m', this.args.module, ...programArgs]; + } + if (this.args.program && this.args.program.length > 0) { + return [this.args.program, ...programArgs]; } - return [vsDebugOptions.join(','), this.args.program].concat(programArgs); + return programArgs; } private launchExternalTerminal(sudo: boolean, cwd: string, pythonPath: string, args: string[], env: {}) { return new Promise((resolve, reject) => { diff --git a/src/client/debugger/DebugClients/RemoteDebugClient.ts b/src/client/debugger/DebugClients/RemoteDebugClient.ts index 45aec00388a4..172b8ba79bd4 100644 --- a/src/client/debugger/DebugClients/RemoteDebugClient.ts +++ b/src/client/debugger/DebugClients/RemoteDebugClient.ts @@ -1,20 +1,26 @@ import { DebugSession } from 'vscode-debugadapter'; -import { AttachRequestArguments, IPythonProcess } from '../Common/Contracts'; +import { AttachRequestArgumentsV1, BaseAttachRequestArguments, IPythonProcess } from '../Common/Contracts'; import { BaseDebugServer } from '../DebugServers/BaseDebugServer'; import { RemoteDebugServer } from '../DebugServers/RemoteDebugServer'; +import { RemoteDebugServerV2 } from '../DebugServers/RemoteDebugServerv2'; import { DebugClient, DebugType } from './DebugClient'; -export class RemoteDebugClient extends DebugClient { - private pythonProcess: IPythonProcess; +export class RemoteDebugClient extends DebugClient { + private pythonProcess?: IPythonProcess; private debugServer?: BaseDebugServer; // tslint:disable-next-line:no-any - constructor(args: any, debugSession: DebugSession) { + constructor(args: T, debugSession: DebugSession) { super(args, debugSession); } public CreateDebugServer(pythonProcess?: IPythonProcess): BaseDebugServer { - this.pythonProcess = pythonProcess!; - this.debugServer = new RemoteDebugServer(this.debugSession, this.pythonProcess!, this.args); + if (this.args.type === 'pythonExperimental') { + // tslint:disable-next-line:no-any + this.debugServer = new RemoteDebugServerV2(this.debugSession, undefined as any, this.args); + } else { + this.pythonProcess = pythonProcess!; + this.debugServer = new RemoteDebugServer(this.debugSession, this.pythonProcess!, this.args as {} as AttachRequestArgumentsV1); + } return this.debugServer!; } public get DebugType(): DebugType { diff --git a/src/client/debugger/DebugClients/helper.ts b/src/client/debugger/DebugClients/helper.ts index 033427b5bdae..a9d9beb1fdab 100644 --- a/src/client/debugger/DebugClients/helper.ts +++ b/src/client/debugger/DebugClients/helper.ts @@ -10,7 +10,8 @@ export class DebugClientHelper { // Merge variables from both .env file and env json variables. const envFileVars = await this.envParser.parseFile(args.envFile); - const debugLaunchEnvVars = (args.env && Object.keys(args.env).length > 0) ? { ...args.env } as EnvironmentVariables : {}; + // tslint:disable-next-line:no-any + const debugLaunchEnvVars: {[key: string]: string} = (args.env && Object.keys(args.env).length > 0) ? { ...args.env } as any : {} as any; const env = envFileVars ? { ...envFileVars! } : {}; this.envParser.mergeVariables(debugLaunchEnvVars, env); @@ -51,6 +52,10 @@ export class DebugClientHelper { env.PYTHONUNBUFFERED = '1'; } + if (args.gevent) { + env.GEVENT_SUPPORT = 'True'; // this is read in pydevd_constants.py + } + return env; } } diff --git a/src/client/debugger/DebugClients/launcherProvider.ts b/src/client/debugger/DebugClients/launcherProvider.ts index 25f722457918..06ea43e0002e 100644 --- a/src/client/debugger/DebugClients/launcherProvider.ts +++ b/src/client/debugger/DebugClients/launcherProvider.ts @@ -1,6 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +'use strict'; + +// tslint:disable:max-classes-per-file + import * as path from 'path'; import { IDebugLauncherScriptProvider } from '../types'; @@ -15,9 +19,3 @@ export class DebuggerLauncherScriptProvider implements IDebugLauncherScriptProvi return path.join(path.dirname(__dirname), '..', '..', '..', 'pythonFiles', 'PythonTools', 'visualstudio_py_launcher.py'); } } - -export class DebuggerV2LauncherScriptProvider implements IDebugLauncherScriptProvider { - public getLauncherFilePath(): string { - return path.join(path.dirname(__dirname), '..', '..', '..', 'pythonFiles', 'experimental', 'ptvsd_launcher.py'); - } -} diff --git a/src/client/debugger/DebugClients/localDebugClientV2.ts b/src/client/debugger/DebugClients/localDebugClientV2.ts new file mode 100644 index 000000000000..9028cb10f01e --- /dev/null +++ b/src/client/debugger/DebugClients/localDebugClientV2.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { DebugSession } from 'vscode-debugadapter'; +import { LaunchRequestArguments } from '../Common/Contracts'; +import { IDebugLauncherScriptProvider } from '../types'; +import { LocalDebugClient } from './LocalDebugClient'; + +export class LocalDebugClientV2 extends LocalDebugClient { + constructor(args: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean, launcherScriptProvider: IDebugLauncherScriptProvider) { + super(args, debugSession, canLaunchTerminal, launcherScriptProvider); + } + protected buildDebugArguments(cwd: string, debugPort: number): string[] { + const noDebugArg = this.args.noDebug ? ['--nodebug'] : []; + return ['-m', 'ptvsd', ...noDebugArg, '--host', 'localhost', '--port', debugPort.toString()]; + } + protected buildStandardArguments() { + const programArgs = Array.isArray(this.args.args) && this.args.args.length > 0 ? this.args.args : []; + if (typeof this.args.module === 'string' && this.args.module.length > 0) { + return ['-m', this.args.module, ...programArgs]; + } + if (this.args.program && this.args.program.length > 0) { + return [this.args.program, ...programArgs]; + } + return programArgs; + } +} diff --git a/src/client/debugger/DebugClients/nonDebugClientV2.ts b/src/client/debugger/DebugClients/nonDebugClientV2.ts new file mode 100644 index 000000000000..0d47171e810c --- /dev/null +++ b/src/client/debugger/DebugClients/nonDebugClientV2.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ChildProcess } from 'child_process'; +import { DebugSession } from 'vscode-debugadapter'; +import { LaunchRequestArguments } from '../Common/Contracts'; +import { IDebugLauncherScriptProvider } from '../types'; +import { DebugType } from './DebugClient'; +import { LocalDebugClientV2 } from './localDebugClientV2'; + +export class NonDebugClientV2 extends LocalDebugClientV2 { + constructor(args: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean, launcherScriptProvider: IDebugLauncherScriptProvider) { + super(args, debugSession, canLaunchTerminal, launcherScriptProvider); + } + + public get DebugType(): DebugType { + return DebugType.RunLocal; + } + + public Stop() { + super.Stop(); + if (this.pyProc) { + try { + this.pyProc!.kill(); + // tslint:disable-next-line:no-empty + } catch { } + this.pyProc = undefined; + } + } + protected handleProcessOutput(proc: ChildProcess, _failedToLaunch: (error: Error | string | Buffer) => void) { + // Do nothing + } +} diff --git a/src/client/debugger/DebugServers/RemoteDebugServer.ts b/src/client/debugger/DebugServers/RemoteDebugServer.ts index 32d93d004920..8574bce7aff7 100644 --- a/src/client/debugger/DebugServers/RemoteDebugServer.ts +++ b/src/client/debugger/DebugServers/RemoteDebugServer.ts @@ -1,8 +1,8 @@ -// tslint:disable:quotemark ordered-imports no-any no-empty curly member-ordering one-line max-func-body-length no-var-self prefer-const cyclomatic-complexity prefer-template +// tslint:disable:quotemark ordered-imports no-any no-empty curly member-ordering one-line max-func-body-length no-var-self prefer-const cyclomatic-complexity prefer-template no-this-assignment "use strict"; import { DebugSession, OutputEvent } from "vscode-debugadapter"; -import { IPythonProcess, IDebugServer, AttachRequestArguments } from "../Common/Contracts"; +import { IPythonProcess, IDebugServer, AttachRequestArgumentsV1, VALID_DEBUG_OPTIONS } from "../Common/Contracts"; import * as net from "net"; import { BaseDebugServer } from "./BaseDebugServer"; import { SocketStream } from "../../common/net/socket/SocketStream"; @@ -15,8 +15,8 @@ const AttachCommandBytes: Buffer = new Buffer("ATCH", "ascii"); export class RemoteDebugServer extends BaseDebugServer { private socket?: net.Socket; - private args: AttachRequestArguments; - constructor(debugSession: DebugSession, pythonProcess: IPythonProcess, args: AttachRequestArguments) { + private args: AttachRequestArgumentsV1; + constructor(debugSession: DebugSession, pythonProcess: IPythonProcess, args: AttachRequestArgumentsV1) { super(debugSession, pythonProcess); this.args = args; } @@ -29,7 +29,7 @@ export class RemoteDebugServer extends BaseDebugServer { catch (ex) { } this.socket = undefined; } - private stream: SocketStream; + private stream!: SocketStream; public Start(): Promise { return new Promise((resolve, reject) => { let that = this; @@ -132,6 +132,9 @@ export class RemoteDebugServer extends BaseDebugServer { if (!commandBytesWritten) { that.stream.Write(AttachCommandBytes); let debugOptions = "WaitOnAbnormalExit, WaitOnNormalExit, RedirectOutput"; + if (Array.isArray(this.args.debugOptions)) { + debugOptions = this.args.debugOptions.filter(opt => VALID_DEBUG_OPTIONS.indexOf(opt) >= 0).join(','); + } that.stream.WriteString(debugOptions); commandBytesWritten = true; } diff --git a/src/client/debugger/DebugServers/RemoteDebugServerv2.ts b/src/client/debugger/DebugServers/RemoteDebugServerv2.ts new file mode 100644 index 000000000000..ff6ff06f9a40 --- /dev/null +++ b/src/client/debugger/DebugServers/RemoteDebugServerv2.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Socket } from 'net'; +import { DebugSession } from 'vscode-debugadapter'; +import { AttachRequestArguments, IDebugServer, IPythonProcess } from '../Common/Contracts'; +import { BaseDebugServer } from './BaseDebugServer'; + +export class RemoteDebugServerV2 extends BaseDebugServer { + private args: AttachRequestArguments; + private socket?: Socket; + constructor(debugSession: DebugSession, pythonProcess: IPythonProcess, args: AttachRequestArguments) { + super(debugSession, pythonProcess); + this.args = args; + } + + public Stop() { + if (this.socket) { + this.socket.destroy(); + } + } + public Start(): Promise { + return new Promise((resolve, reject) => { + const port = this.args.port!; + const options = { port }; + if (typeof this.args.host === 'string' && this.args.host.length > 0) { + // tslint:disable-next-line:no-any + (options).host = this.args.host; + } + try { + let connected = false; + const socket = new Socket(); + socket.on('error', ex => { + if (connected) { + return; + } + reject(ex); + }); + socket.connect(options, () => { + connected = true; + this.socket = socket; + this.clientSocket.resolve(socket); + resolve(options); + }); + } catch (ex) { + reject(ex); + } + }); + } +} diff --git a/src/client/debugger/Main.ts b/src/client/debugger/Main.ts index e2fc28642df1..50e221042f1f 100644 --- a/src/client/debugger/Main.ts +++ b/src/client/debugger/Main.ts @@ -1,4 +1,4 @@ -// tslint:disable:quotemark ordered-imports promise-must-complete member-ordering no-any prefer-template cyclomatic-complexity no-empty no-multiline-string one-line no-invalid-template-strings no-suspicious-comment no-var-self +// tslint:disable:quotemark ordered-imports promise-must-complete member-ordering no-any prefer-template cyclomatic-complexity no-empty no-multiline-string one-line no-invalid-template-strings no-suspicious-comment no-var-self no-duplicate-imports "use strict"; // This line should always be right on top. @@ -9,21 +9,19 @@ if ((Reflect as any).metadata === undefined) { } import * as fs from "fs"; import * as path from "path"; -import { Handles, InitializedEvent, OutputEvent, Scope, Source, StackFrame, StoppedEvent, TerminatedEvent, Thread, Variable, LoggingDebugSession, logger, BreakpointEvent, Breakpoint } from "vscode-debugadapter"; -import { ThreadEvent } from "vscode-debugadapter"; +import { Handles, InitializedEvent, OutputEvent, Scope, Source, StackFrame, StoppedEvent, TerminatedEvent, Thread, Variable, LoggingDebugSession, logger, BreakpointEvent, Breakpoint, ThreadEvent } from "vscode-debugadapter"; import { DebugProtocol } from "vscode-debugprotocol"; import { DEBUGGER } from '../../client/telemetry/constants'; import { DebuggerTelemetry } from '../../client/telemetry/types'; import { isNotInstalledError } from '../common/helpers'; -import { enum_EXCEPTION_STATE, IPythonBreakpoint, IPythonException, PythonBreakpointConditionKind, PythonBreakpointPassCountKind, PythonEvaluationResultReprKind } from "./Common/Contracts"; +import { enum_EXCEPTION_STATE, IPythonBreakpoint, IPythonException, PythonBreakpointConditionKind, PythonBreakpointPassCountKind, PythonEvaluationResultReprKind, LaunchRequestArgumentsV1, AttachRequestArgumentsV1 } from "./Common/Contracts"; import { IDebugServer, IPythonEvaluationResult, IPythonModule, IPythonStackFrame, IPythonThread } from "./Common/Contracts"; -import { AttachRequestArguments, DebugOptions, LaunchRequestArguments, PythonEvaluationResultFlags, TelemetryEvent } from "./Common/Contracts"; +import { DebugOptions, LaunchRequestArguments, PythonEvaluationResultFlags, TelemetryEvent } from "./Common/Contracts"; import { getPythonExecutable, validatePath } from './Common/Utils'; import { DebugClient } from "./DebugClients/DebugClient"; import { CreateAttachDebugClient, CreateLaunchDebugClient } from "./DebugClients/DebugFactory"; import { BaseDebugServer } from "./DebugServers/BaseDebugServer"; import { PythonProcess } from "./PythonProcess"; -import { IS_WINDOWS } from './Common/Utils'; import { sendPerformanceTelemetry, capturePerformanceTelemetry, PerformanceTelemetryCondition } from "./Common/telemetry"; import { LogLevel } from "vscode-debugadapter/lib/logger"; @@ -41,13 +39,13 @@ export class PythonDebugger extends LoggingDebugSession { private registeredBreakpoints: Map; private registeredBreakpointsByFileName: Map; private debuggerLoaded: Promise; - private debuggerLoadedPromiseResolve: () => void; + private debuggerLoadedPromiseResolve!: () => void; private debugClient?: DebugClient<{}>; - private configurationDone: Promise; + private configurationDone!: Promise; private configurationDonePromiseResolve?: () => void; private lastException?: IPythonException; - private _supportsRunInTerminalRequest: boolean; - private terminateEventSent: boolean; + private _supportsRunInTerminalRequest: boolean = false; + private terminateEventSent: boolean = false; public constructor(debuggerLinesStartAt1: boolean, isServer: boolean) { super(path.join(__dirname, '..', '..', '..', 'debug.log'), debuggerLinesStartAt1, isServer === true); this._variableHandles = new Handles(); @@ -89,12 +87,12 @@ export class PythonDebugger extends LoggingDebugSession { } private pythonProcess?: PythonProcess; - private debugServer: BaseDebugServer; + private debugServer!: BaseDebugServer; private startDebugServer(): Promise { let programDirectory = ''; if ((this.launchArgs && this.launchArgs.program) || (this.attachArgs && this.attachArgs.localRoot)) { - programDirectory = this.launchArgs ? path.dirname(this.launchArgs.program) : this.attachArgs.localRoot; + programDirectory = (this.launchArgs && this.launchArgs.program) ? path.dirname(this.launchArgs.program) : this.attachArgs.localRoot; } if (this.launchArgs && typeof this.launchArgs.cwd === 'string' && this.launchArgs.cwd.length > 0 && this.launchArgs.cwd !== 'null') { programDirectory = this.launchArgs.cwd; @@ -168,6 +166,7 @@ export class PythonDebugger extends LoggingDebugSession { private onPythonProcessLoaded(pyThread?: IPythonThread) { if (this.entryResponse) { this.sendResponse(this.entryResponse); + this.entryResponse = undefined; } this.debuggerLoadedPromiseResolve(); if (this.launchArgs && !this.launchArgs.console) { @@ -205,8 +204,8 @@ export class PythonDebugger extends LoggingDebugSession { this.sendEvent(new OutputEvent(output, outputChannel)); } private entryResponse?: DebugProtocol.LaunchResponse; - private launchArgs: LaunchRequestArguments; - private attachArgs: AttachRequestArguments; + private launchArgs!: LaunchRequestArgumentsV1; + private attachArgs!: AttachRequestArgumentsV1; private canStartDebugger(): Promise { return Promise.resolve(true); } @@ -227,18 +226,9 @@ export class PythonDebugger extends LoggingDebugSession { } catch (ex) { } - if (Array.isArray(args.debugOptions) && args.debugOptions.indexOf("Pyramid") >= 0) { - const pserve = IS_WINDOWS ? "pserve.exe" : "pserve"; - if (fs.existsSync(args.pythonPath)) { - args.program = path.join(path.dirname(args.pythonPath), pserve); - } - else { - args.program = pserve; - } - } // Confirm the file exists if (typeof args.module !== 'string' || args.module.length === 0) { - if (!fs.existsSync(args.program)) { + if (!args.program || !fs.existsSync(args.program)) { return this.sendErrorResponse(response, 2001, `File does not exist. "${args.program}"`); } } @@ -276,6 +266,7 @@ export class PythonDebugger extends LoggingDebugSession { }); this.entryResponse = response; + // tslint:disable-next-line:no-this-assignment const that = this; this.startDebugServer().then(dbgServer => { @@ -290,7 +281,7 @@ export class PythonDebugger extends LoggingDebugSession { this.sendErrorResponse(response, 200, errorMsg); }); } - protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments) { + protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArgumentsV1) { if (args.logToFile === true) { logger.setup(LogLevel.Verbose, true); } @@ -299,6 +290,7 @@ export class PythonDebugger extends LoggingDebugSession { this.attachArgs = args; this.debugClient = CreateAttachDebugClient(args, this); this.entryResponse = response; + // tslint:disable-next-line:no-this-assignment const that = this; this.canStartDebugger().then(() => { @@ -341,7 +333,12 @@ export class PythonDebugger extends LoggingDebugSession { let isDjangoFile = false; if (this.launchArgs && Array.isArray(this.launchArgs.debugOptions) && - this.launchArgs.debugOptions.indexOf(DebugOptions.DjangoDebugging) >= 0) { + (this.launchArgs.debugOptions.indexOf(DebugOptions.Django) >= 0 || this.launchArgs.debugOptions.indexOf(DebugOptions.DjangoDebugging) >= 0)) { + isDjangoFile = filePath.toUpperCase().endsWith(".HTML"); + } + if (this.attachArgs && + Array.isArray(this.attachArgs.debugOptions) && + (this.attachArgs.debugOptions.indexOf(DebugOptions.Django) >= 0 || this.attachArgs.debugOptions.indexOf(DebugOptions.DjangoDebugging) >= 0)) { isDjangoFile = filePath.toUpperCase().endsWith(".HTML"); } @@ -372,7 +369,7 @@ export class PythonDebugger extends LoggingDebugSession { } // VSC needs `id` to uniquely identify each breakpoint (part of the protocol spec). - const breakpoints: { verified: boolean, line: number, id: number }[] = []; + const breakpoints: { verified: boolean; line: number; id: number }[] = []; const linesToAdd = args.breakpoints!.map(b => b.line); const registeredBks = this.registeredBreakpointsByFileName.get(args.source.path!)!; const linesToRemove = registeredBks.map(b => b.LineNo).filter(oldLine => linesToAdd.indexOf(oldLine) === -1); @@ -495,8 +492,9 @@ export class PythonDebugger extends LoggingDebugSession { return new StackFrame(frameId, frame.FunctionName); } else { + const realFilePath = fs.realpathSync(fileName); return new StackFrame(frameId, frame.FunctionName, - new Source(path.basename(frame.FileName), fileName), + new Source(path.basename(realFilePath), realFilePath), this.convertDebuggerLineToClient(frame.LineNo - 1), 1); } diff --git a/src/client/debugger/PythonProcessCallbackHandler.ts b/src/client/debugger/PythonProcessCallbackHandler.ts index 2b18c62ee804..da5d32359577 100644 --- a/src/client/debugger/PythonProcessCallbackHandler.ts +++ b/src/client/debugger/PythonProcessCallbackHandler.ts @@ -259,7 +259,7 @@ export class PythonProcessCallbackHandler extends EventEmitter { return; } - if (typeName && desc) { + if (typeName || desc) { let ex: IPythonException = { TypeName: typeName, Description: desc diff --git a/src/client/debugger/banner.ts b/src/client/debugger/banner.ts new file mode 100644 index 000000000000..480dc604b5e0 --- /dev/null +++ b/src/client/debugger/banner.ts @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as crypto from 'crypto'; +import { inject, injectable } from 'inversify'; +import { Disposable } from 'vscode'; +import { IApplicationEnvironment, IApplicationShell, IDebugService } from '../common/application/types'; +import '../common/extensions'; +import { IBrowserService, IDisposableRegistry, IExperimentalDebuggerBanner, + ILogger, IPersistentStateFactory } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { ExperimentalDebuggerType } from './Common/constants'; + +export enum PersistentStateKeys { + ShowBanner = 'ShowBanner', + DebuggerLaunchCounter = 'DebuggerLaunchCounter', + DebuggerLaunchThresholdCounter = 'DebuggerLaunchThresholdCounter' +} + +@injectable() +export class ExperimentalDebuggerBanner implements IExperimentalDebuggerBanner { + private initialized?: boolean; + private disabledInCurrentSession?: boolean; + public get enabled(): boolean { + const factory = this.serviceContainer.get(IPersistentStateFactory); + return factory.createGlobalPersistentState(PersistentStateKeys.ShowBanner, true).value; + } + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } + public initialize() { + if (this.initialized) { + return; + } + this.initialized = true; + + // Don't even bother adding handlers if banner has been turned off. + if (!this.enabled) { + return; + } + const debuggerService = this.serviceContainer.get(IDebugService); + const disposable = debuggerService.onDidTerminateDebugSession(async e => { + if (e.type === ExperimentalDebuggerType) { + const logger = this.serviceContainer.get(ILogger); + await this.onDidTerminateDebugSession() + .catch(ex => logger.logError('Error in debugger Banner', ex)); + } + }); + + this.serviceContainer.get(IDisposableRegistry).push(disposable); + } + public async showBanner(): Promise { + const appShell = this.serviceContainer.get(IApplicationShell); + const yes = 'Yes, take survey now'; + const no = 'No thanks'; + const response = await appShell.showInformationMessage('Can you please take 2 minutes to tell us how the Experimental Debugger is working for you?', yes, no); + switch (response) { + case yes: + { + await this.launchSurvey(); + await this.disable(); + break; + } + case no: { + await this.disable(); + break; + } + default: { + // Disable for the current session. + this.disabledInCurrentSession = true; + } + } + } + public async shouldShowBanner(): Promise { + if (!this.enabled || this.disabledInCurrentSession) { + return false; + } + const [threshold, debuggerCounter] = await Promise.all([this.getDebuggerLaunchThresholdCounter(), this.getGetDebuggerLaunchCounter()]); + return debuggerCounter >= threshold; + } + + public async disable(): Promise { + const factory = this.serviceContainer.get(IPersistentStateFactory); + await factory.createGlobalPersistentState(PersistentStateKeys.ShowBanner, false).updateValue(false); + } + public async launchSurvey(): Promise { + const debuggerLaunchCounter = await this.getGetDebuggerLaunchCounter(); + const browser = this.serviceContainer.get(IBrowserService); + browser.launch(`https://www.research.net/r/N7B25RV?n=${debuggerLaunchCounter}`); + } + private async incrementDebuggerLaunchCounter(): Promise { + const factory = this.serviceContainer.get(IPersistentStateFactory); + const state = factory.createGlobalPersistentState(PersistentStateKeys.DebuggerLaunchCounter, 0); + await state.updateValue(state.value + 1); + } + private async getGetDebuggerLaunchCounter(): Promise { + const factory = this.serviceContainer.get(IPersistentStateFactory); + const state = factory.createGlobalPersistentState(PersistentStateKeys.DebuggerLaunchCounter, 0); + return state.value; + } + private async getDebuggerLaunchThresholdCounter(): Promise { + const factory = this.serviceContainer.get(IPersistentStateFactory); + const state = factory.createGlobalPersistentState(PersistentStateKeys.DebuggerLaunchThresholdCounter, undefined); + if (state.value === undefined) { + const hexValue = parseInt(`0x${this.getRandomHex()}`, 16); + const randomNumber = Math.floor((10 * hexValue) / 16) + 1; + await state.updateValue(randomNumber); + } + return state.value!; + } + private getRandomHex() { + const appEnv = this.serviceContainer.get(IApplicationEnvironment); + const lastHexValue = appEnv.machineId.slice(-1); + const num = parseInt(`0x${lastHexValue}`, 16); + return isNaN(num) ? crypto.randomBytes(1).toString('hex').slice(-1) : lastHexValue; + } + private async onDidTerminateDebugSession(): Promise { + if (!this.enabled) { + return; + } + await this.incrementDebuggerLaunchCounter(); + const show = await this.shouldShowBanner(); + if (!show) { + return; + } + + await this.showBanner(); + } +} diff --git a/src/client/debugger/configProviders/baseProvider.ts b/src/client/debugger/configProviders/baseProvider.ts index 7a8a73240e13..81c86c7520a7 100644 --- a/src/client/debugger/configProviders/baseProvider.ts +++ b/src/client/debugger/configProviders/baseProvider.ts @@ -3,42 +3,60 @@ 'use strict'; +// tslint:disable:no-invalid-template-strings + import { injectable, unmanaged } from 'inversify'; import * as path from 'path'; -import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, ProviderResult, Uri, WorkspaceFolder } from 'vscode'; +import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, Uri, WorkspaceFolder } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../../common/application/types'; -import { PythonLanguage } from '../../common/constants'; +import { PYTHON_LANGUAGE } from '../../common/constants'; import { IConfigurationService } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; -import { DebuggerType, LaunchRequestArguments } from '../Common/Contracts'; - -// tslint:disable:no-invalid-template-strings +import { BaseAttachRequestArguments, BaseLaunchRequestArguments, DebuggerType } from '../Common/Contracts'; -export type PythonDebugConfiguration = DebugConfiguration & LaunchRequestArguments; -export type PTVSDDebugConfiguration = PythonDebugConfiguration & { redirectOutput: boolean, fixFilePathCase: boolean }; +export type PythonLaunchDebugConfiguration = DebugConfiguration & T; +export type PythonAttachDebugConfiguration = DebugConfiguration & T; @injectable() -export abstract class BaseConfigurationProvider implements DebugConfigurationProvider { +export abstract class BaseConfigurationProvider implements DebugConfigurationProvider { constructor(@unmanaged() public debugType: DebuggerType, protected serviceContainer: IServiceContainer) { } - public resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult { - const config = debugConfiguration as PythonDebugConfiguration; - const numberOfSettings = Object.keys(config); - const workspaceFolder = this.getWorkspaceFolder(folder, config); + public async resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): Promise { + const workspaceFolder = this.getWorkspaceFolder(folder); - if ((config.noDebug === true && numberOfSettings.length === 1) || numberOfSettings.length === 0) { - const defaultProgram = this.getProgram(config); + if (debugConfiguration.request === 'attach') { + await this.provideAttachDefaults(workspaceFolder, debugConfiguration as PythonAttachDebugConfiguration); + } else { + const config = debugConfiguration as PythonLaunchDebugConfiguration; + const numberOfSettings = Object.keys(config); + + if ((config.noDebug === true && numberOfSettings.length === 1) || numberOfSettings.length === 0) { + const defaultProgram = this.getProgram(); + + config.name = 'Launch'; + config.type = this.debugType; + config.request = 'launch'; + config.program = defaultProgram ? defaultProgram : ''; + config.env = {}; + } - config.name = 'Launch'; - config.type = this.debugType; - config.request = 'launch'; - config.program = defaultProgram ? defaultProgram : ''; - config.env = {}; + await this.provideLaunchDefaults(workspaceFolder, config); } - this.provideDefaults(workspaceFolder, config); - return config; + const dbgConfig = (debugConfiguration as (BaseLaunchRequestArguments | BaseAttachRequestArguments)); + if (Array.isArray(dbgConfig.debugOptions)) { + dbgConfig.debugOptions = dbgConfig.debugOptions!.filter((item, pos) => dbgConfig.debugOptions!.indexOf(item) === pos); + } + return debugConfiguration; } - protected provideDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonDebugConfiguration): void { + protected async provideAttachDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonAttachDebugConfiguration): Promise { + if (!Array.isArray(debugConfiguration.debugOptions)) { + debugConfiguration.debugOptions = []; + } + if (!debugConfiguration.host) { + debugConfiguration.host = 'localhost'; + } + } + protected async provideLaunchDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonLaunchDebugConfiguration): Promise { this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); if (typeof debugConfiguration.cwd !== 'string' && workspaceFolder) { debugConfiguration.cwd = workspaceFolder.fsPath; @@ -51,7 +69,7 @@ export abstract class BaseConfigurationProvider implements DebugConfigurationPro debugConfiguration.stopOnEntry = false; } if (!debugConfiguration.console) { - debugConfiguration.console = 'none'; + debugConfiguration.console = 'integratedTerminal'; } // If using a terminal, then never open internal console. if (debugConfiguration.console !== 'none' && !debugConfiguration.internalConsoleOptions) { @@ -60,16 +78,12 @@ export abstract class BaseConfigurationProvider implements DebugConfigurationPro if (!Array.isArray(debugConfiguration.debugOptions)) { debugConfiguration.debugOptions = []; } - // Always redirect output. - if (debugConfiguration.debugOptions.indexOf('RedirectOutput') === -1) { - debugConfiguration.debugOptions.push('RedirectOutput'); - } } - private getWorkspaceFolder(folder: WorkspaceFolder | undefined, config: PythonDebugConfiguration): Uri | undefined { + private getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { if (folder) { return folder.uri; } - const program = this.getProgram(config); + const program = this.getProgram(); const workspaceService = this.serviceContainer.get(IWorkspaceService); if (!Array.isArray(workspaceService.workspaceFolders) || workspaceService.workspaceFolders.length === 0) { return program ? Uri.file(path.dirname(program)) : undefined; @@ -84,14 +98,14 @@ export abstract class BaseConfigurationProvider implements DebugConfigurationPro } } } - private getProgram(config: PythonDebugConfiguration): string | undefined { + private getProgram(): string | undefined { const documentManager = this.serviceContainer.get(IDocumentManager); const editor = documentManager.activeTextEditor; - if (editor && editor.document.languageId === PythonLanguage.language) { + if (editor && editor.document.languageId === PYTHON_LANGUAGE) { return editor.document.fileName; } } - private resolveAndUpdatePythonPath(workspaceFolder: Uri | undefined, debugConfiguration: PythonDebugConfiguration): void { + private resolveAndUpdatePythonPath(workspaceFolder: Uri | undefined, debugConfiguration: PythonLaunchDebugConfiguration): void { if (!debugConfiguration) { return; } diff --git a/src/client/debugger/configProviders/configurationProviderUtils.ts b/src/client/debugger/configProviders/configurationProviderUtils.ts new file mode 100644 index 000000000000..77e6d3f5de0c --- /dev/null +++ b/src/client/debugger/configProviders/configurationProviderUtils.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { IFileSystem } from '../../common/platform/types'; +import { IPythonExecutionFactory } from '../../common/process/types'; +import { ILogger } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { IConfigurationProviderUtils } from './types'; + +const PSERVE_SCRIPT_FILE_NAME = 'pserve.py'; + +@injectable() +export class ConfigurationProviderUtils implements IConfigurationProviderUtils { + private readonly executionFactory: IPythonExecutionFactory; + private readonly fs: IFileSystem; + private readonly logger: ILogger; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.executionFactory = this.serviceContainer.get(IPythonExecutionFactory); + this.fs = this.serviceContainer.get(IFileSystem); + this.logger = this.serviceContainer.get(ILogger); + } + public async getPyramidStartupScriptFilePath(resource?: Uri): Promise { + try { + const executionService = await this.executionFactory.create({ resource }); + const output = await executionService.exec(['-c', 'import pyramid;print(pyramid.__file__)'], { throwOnStdErr: true }); + const pserveFilePath = path.join(path.dirname(output.stdout.trim()), 'scripts', PSERVE_SCRIPT_FILE_NAME); + return await this.fs.fileExists(pserveFilePath) ? pserveFilePath : undefined; + } catch (ex) { + const message = 'Unable to locate \'pserve.py\' required for debugging of Pyramid applications.'; + this.logger.logError(message, ex); + const app = this.serviceContainer.get(IApplicationShell); + app.showErrorMessage(message); + return; + } + } +} diff --git a/src/client/debugger/configProviders/pythonProvider.ts b/src/client/debugger/configProviders/pythonProvider.ts index 1b349bf2c465..0f3b3e34e6f8 100644 --- a/src/client/debugger/configProviders/pythonProvider.ts +++ b/src/client/debugger/configProviders/pythonProvider.ts @@ -4,12 +4,37 @@ 'use strict'; import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; import { IServiceContainer } from '../../ioc/types'; -import { BaseConfigurationProvider } from './baseProvider'; +import { AttachRequestArgumentsV1, DebugOptions, LaunchRequestArgumentsV1 } from '../Common/Contracts'; +import { BaseConfigurationProvider, PythonAttachDebugConfiguration, PythonLaunchDebugConfiguration } from './baseProvider'; +import { IConfigurationProviderUtils } from './types'; @injectable() -export class PythonDebugConfigurationProvider extends BaseConfigurationProvider { +export class PythonDebugConfigurationProvider extends BaseConfigurationProvider { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super('python', serviceContainer); } + protected async provideLaunchDefaults(workspaceFolder: Uri, debugConfiguration: PythonLaunchDebugConfiguration): Promise { + await super.provideLaunchDefaults(workspaceFolder, debugConfiguration); + // Always redirect output. + if (debugConfiguration.debugOptions!.indexOf(DebugOptions.RedirectOutput) === -1) { + debugConfiguration.debugOptions!.push(DebugOptions.RedirectOutput); + } + if (debugConfiguration.debugOptions!.indexOf(DebugOptions.Pyramid) >= 0) { + const utils = this.serviceContainer.get(IConfigurationProviderUtils); + debugConfiguration.program = (await utils.getPyramidStartupScriptFilePath(workspaceFolder))!; + } + } + protected async provideAttachDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonAttachDebugConfiguration): Promise { + await super.provideAttachDefaults(workspaceFolder, debugConfiguration); + const debugOptions = debugConfiguration.debugOptions!; + // Always redirect output. + if (debugOptions.indexOf(DebugOptions.RedirectOutput) === -1) { + debugOptions.push(DebugOptions.RedirectOutput); + } + if (!debugConfiguration.localRoot && workspaceFolder) { + debugConfiguration.localRoot = workspaceFolder.fsPath; + } + } } diff --git a/src/client/debugger/configProviders/pythonV2Provider.ts b/src/client/debugger/configProviders/pythonV2Provider.ts index 6f81abdfa1a5..19f37e07798c 100644 --- a/src/client/debugger/configProviders/pythonV2Provider.ts +++ b/src/client/debugger/configProviders/pythonV2Provider.ts @@ -7,23 +7,134 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; import { IPlatformService } from '../../common/platform/types'; import { IServiceContainer } from '../../ioc/types'; -import { BaseConfigurationProvider, PTVSDDebugConfiguration, PythonDebugConfiguration } from './baseProvider'; +import { sendTelemetryEvent } from '../../telemetry'; +import { DEBUGGER } from '../../telemetry/constants'; +import { DebuggerTelemetryV2 } from '../../telemetry/types'; +import { AttachRequestArguments, DebugOptions, LaunchRequestArguments } from '../Common/Contracts'; +import { BaseConfigurationProvider, PythonAttachDebugConfiguration, PythonLaunchDebugConfiguration } from './baseProvider'; +import { IConfigurationProviderUtils } from './types'; @injectable() -export class PythonV2DebugConfigurationProvider extends BaseConfigurationProvider { +export class PythonV2DebugConfigurationProvider extends BaseConfigurationProvider { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super('pythonExperimental', serviceContainer); } - protected provideDefaults(workspaceFolder: Uri, debugConfiguration: PythonDebugConfiguration): void { - super.provideDefaults(workspaceFolder, debugConfiguration); - debugConfiguration.stopOnEntry = false; - if (debugConfiguration.console !== 'externalTerminal' && debugConfiguration.console !== 'integratedTerminal') { - debugConfiguration.console = 'integratedTerminal'; + protected async provideLaunchDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonLaunchDebugConfiguration): Promise { + await super.provideLaunchDefaults(workspaceFolder, debugConfiguration); + const debugOptions = debugConfiguration.debugOptions!; + if (debugConfiguration.debugStdLib) { + this.debugOption(debugOptions, DebugOptions.DebugStdLib); + } + if (debugConfiguration.django) { + this.debugOption(debugOptions, DebugOptions.Django); + } + if (debugConfiguration.jinja) { + this.debugOption(debugOptions, DebugOptions.Jinja); + } + if (debugConfiguration.redirectOutput || debugConfiguration.redirectOutput === undefined) { + this.debugOption(debugOptions, DebugOptions.RedirectOutput); + } + if (debugConfiguration.sudo) { + this.debugOption(debugOptions, DebugOptions.Sudo); + } + if (this.serviceContainer.get(IPlatformService).isWindows) { + this.debugOption(debugOptions, DebugOptions.FixFilePathCase); + } + const isFlask = this.isDebuggingFlask(debugConfiguration); + if ((debugConfiguration.pyramid || isFlask) + && debugOptions.indexOf(DebugOptions.Jinja) === -1 + && debugConfiguration.jinja !== false) { + this.debugOption(debugOptions, DebugOptions.Jinja); + } + if (debugConfiguration.pyramid) { + const utils = this.serviceContainer.get(IConfigurationProviderUtils); + debugConfiguration.program = (await utils.getPyramidStartupScriptFilePath(workspaceFolder))!; + } + this.sendTelemetry('launch', debugConfiguration); + } + // tslint:disable-next-line:cyclomatic-complexity + protected async provideAttachDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonAttachDebugConfiguration): Promise { + await super.provideAttachDefaults(workspaceFolder, debugConfiguration); + const debugOptions = debugConfiguration.debugOptions!; + if (debugConfiguration.debugStdLib) { + this.debugOption(debugOptions, DebugOptions.DebugStdLib); + } + if (debugConfiguration.django) { + this.debugOption(debugOptions, DebugOptions.Django); + } + if (debugConfiguration.jinja) { + this.debugOption(debugOptions, DebugOptions.Jinja); + } + if (debugConfiguration.pyramid + && debugOptions.indexOf(DebugOptions.Jinja) === -1 + && debugConfiguration.jinja !== false) { + this.debugOption(debugOptions, DebugOptions.Jinja); + } + if (debugConfiguration.redirectOutput || debugConfiguration.redirectOutput === undefined) { + this.debugOption(debugOptions, DebugOptions.RedirectOutput); + } + + // We'll need paths to be fixed only in the case where local and remote hosts are the same + // I.e. only if hostName === 'localhost' or '127.0.0.1' or '' + const isLocalHost = this.isLocalHost(debugConfiguration.host); + if (this.serviceContainer.get(IPlatformService).isWindows && isLocalHost) { + this.debugOption(debugOptions, DebugOptions.FixFilePathCase); + } + if (this.serviceContainer.get(IPlatformService).isWindows) { + this.debugOption(debugOptions, DebugOptions.WindowsClient); + } else { + this.debugOption(debugOptions, DebugOptions.UnixClient); } - // Add PTVSD specific flags. - const ptvsdDebugConfigurationFlags = debugConfiguration as PTVSDDebugConfiguration; - ptvsdDebugConfigurationFlags.redirectOutput = Array.isArray(debugConfiguration.debugOptions) && debugConfiguration.debugOptions.indexOf('RedirectOutput') >= 0; - ptvsdDebugConfigurationFlags.fixFilePathCase = this.serviceContainer.get(IPlatformService).isWindows; + if (!debugConfiguration.pathMappings) { + debugConfiguration.pathMappings = []; + } + // This is for backwards compatibility. + if (debugConfiguration.localRoot && debugConfiguration.remoteRoot) { + debugConfiguration.pathMappings!.push({ + localRoot: debugConfiguration.localRoot, + remoteRoot: debugConfiguration.remoteRoot + }); + } + // If attaching to local host, then always map local root and remote roots. + if (workspaceFolder && debugConfiguration.host && + debugConfiguration.pathMappings!.length === 0 && + ['LOCALHOST', '127.0.0.1', '::1'].indexOf(debugConfiguration.host.toUpperCase()) >= 0) { + debugConfiguration.pathMappings!.push({ + localRoot: workspaceFolder.fsPath, + remoteRoot: workspaceFolder.fsPath + }); + } + this.sendTelemetry('attach', debugConfiguration); + } + private debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions) { + if (debugOptions.indexOf(debugOption) >= 0) { + return; + } + debugOptions.push(debugOption); + } + private isLocalHost(hostName?: string) { + const LocalHosts = ['localhost', '127.0.0.1', '::1']; + return (hostName && LocalHosts.indexOf(hostName.toLowerCase()) >= 0) ? true : false; + } + private isDebuggingFlask(debugConfiguration: PythonAttachDebugConfiguration>) { + return (debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK') ? true : false; + } + private sendTelemetry(trigger: 'launch' | 'attach', debugConfiguration: PythonAttachDebugConfiguration>) { + const telemetryProps: DebuggerTelemetryV2 = { + trigger, + console: debugConfiguration.console, + hasEnvVars: typeof debugConfiguration.env === 'object' && Object.keys(debugConfiguration.env).length > 0, + django: !!debugConfiguration.django, + flask: this.isDebuggingFlask(debugConfiguration), + hasArgs: Array.isArray(debugConfiguration.args) && debugConfiguration.args.length > 0, + isLocalhost: this.isLocalHost(debugConfiguration.host), + isModule: typeof debugConfiguration.module === 'string' && debugConfiguration.module.length > 0, + isSudo: !!debugConfiguration.sudo, + jinja: !!debugConfiguration.jinja, + pyramid: !!debugConfiguration.pyramid, + stopOnEntry: !!debugConfiguration.stopOnEntry + }; + sendTelemetryEvent(DEBUGGER, undefined, telemetryProps); } } diff --git a/src/client/debugger/configProviders/serviceRegistry.ts b/src/client/debugger/configProviders/serviceRegistry.ts index d664bfac771c..3e6dbded95c4 100644 --- a/src/client/debugger/configProviders/serviceRegistry.ts +++ b/src/client/debugger/configProviders/serviceRegistry.ts @@ -8,8 +8,11 @@ import { DebugConfigurationProvider } from 'vscode'; import { PythonDebugConfigurationProvider, PythonV2DebugConfigurationProvider } from '..'; import { IServiceManager } from '../../ioc/types'; import { IDebugConfigurationProvider } from '../types'; +import { ConfigurationProviderUtils } from './configurationProviderUtils'; +import { IConfigurationProviderUtils } from './types'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IDebugConfigurationProvider, PythonDebugConfigurationProvider); serviceManager.addSingleton(IDebugConfigurationProvider, PythonV2DebugConfigurationProvider); + serviceManager.addSingleton(IConfigurationProviderUtils, ConfigurationProviderUtils); } diff --git a/src/client/debugger/configProviders/types.ts b/src/client/debugger/configProviders/types.ts new file mode 100644 index 000000000000..8ece886ab352 --- /dev/null +++ b/src/client/debugger/configProviders/types.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Uri } from 'vscode'; + +export const IConfigurationProviderUtils = Symbol('IConfigurationProviderUtils'); + +export interface IConfigurationProviderUtils { + getPyramidStartupScriptFilePath(resource?: Uri): Promise; +} diff --git a/src/client/debugger/mainV2.ts b/src/client/debugger/mainV2.ts index 4706260601b1..e06382d36ae0 100644 --- a/src/client/debugger/mainV2.ts +++ b/src/client/debugger/mainV2.ts @@ -12,20 +12,19 @@ if ((Reflect as any).metadata === undefined) { import { Socket } from 'net'; import { EOL } from 'os'; import * as path from 'path'; -import { PassThrough } from 'stream'; +import { PassThrough, Writable } from 'stream'; import { Disposable } from 'vscode'; -import { DebugSession, ErrorDestination, logger, OutputEvent, TerminatedEvent } from 'vscode-debugadapter'; +import { DebugSession, ErrorDestination, Event, logger, OutputEvent, Response, TerminatedEvent } from 'vscode-debugadapter'; import { LogLevel } from 'vscode-debugadapter/lib/logger'; -import { Event } from 'vscode-debugadapter/lib/messages'; import { DebugProtocol } from 'vscode-debugprotocol'; import '../../client/common/extensions'; import { noop, sleep } from '../common/core.utils'; import { createDeferred, Deferred, isNotInstalledError } from '../common/helpers'; +import { IFileSystem } from '../common/platform/types'; import { ICurrentProcess } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { AttachRequestArguments, LaunchRequestArguments } from './Common/Contracts'; -import { DebugClient } from './DebugClients/DebugClient'; -import { CreateLaunchDebugClient } from './DebugClients/DebugFactory'; +import { CreateAttachDebugClient, CreateLaunchDebugClient } from './DebugClients/DebugFactory'; import { BaseDebugServer } from './DebugServers/BaseDebugServer'; import { initializeIoc } from './serviceRegistry'; import { IDebugStreamProvider, IProtocolLogger, IProtocolMessageWriter, IProtocolParser } from './types'; @@ -44,9 +43,8 @@ const MIN_DEBUGGER_CONNECT_TIMEOUT = 5000; */ export class PythonDebugger extends DebugSession { public debugServer?: BaseDebugServer; - public debugClient?: DebugClient<{}>; public client = createDeferred(); - private supportsRunInTerminalRequest: boolean; + private supportsRunInTerminalRequest: boolean = false; constructor(private readonly serviceContainer: IServiceContainer) { super(false); } @@ -55,10 +53,6 @@ export class PythonDebugger extends DebugSession { this.debugServer.Stop(); this.debugServer = undefined; } - if (this.debugClient) { - this.debugClient.Stop(); - this.debugClient = undefined; - } super.shutdown(); } protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { @@ -69,6 +63,13 @@ export class PythonDebugger extends DebugSession { body.supportsConditionalBreakpoints = true; body.supportsSetVariable = true; body.supportsExceptionOptions = true; + body.supportsEvaluateForHovers = true; + body.supportsModulesRequest = true; + body.supportsValueFormattingOptions = true; + body.supportsHitConditionalBreakpoints = true; + body.supportsSetExpression = true; + body.supportsLogPoints = true; + body.supportTerminateDebuggee = true; body.exceptionBreakpointFilters = [ { filter: 'raised', @@ -87,14 +88,28 @@ export class PythonDebugger extends DebugSession { this.sendResponse(response); } protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { - this.sendResponse(response); + const launcher = CreateAttachDebugClient(args, this); + this.debugServer = launcher.CreateDebugServer(undefined, this.serviceContainer); + this.debugServer!.Start() + .then(() => this.emit('debugger_attached')) + .catch(ex => { + logger.error('Attach failed'); + logger.error(`${ex}, ${ex.name}, ${ex.message}, ${ex.stack}`); + const message = this.getUserFriendlyAttachErrorMessage(ex) || 'Attach Failed'; + this.sendErrorResponse(response, { format: message, id: 1 }, undefined, undefined, ErrorDestination.User); + }); + } protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { + const fs = this.serviceContainer.get(IFileSystem); + if ((typeof args.module !== 'string' || args.module.length === 0) && args.program && !fs.fileExistsSync(args.program)) { + return this.sendErrorResponse(response, { format: `File does not exist. "${args.program}"`, id: 1 }, undefined, undefined, ErrorDestination.User); + } this.launchPTVSD(args) .then(() => this.waitForPTVSDToConnect(args)) - .then(() => this.sendResponse(response)) + .then(() => this.emit('debugger_launched')) .catch(ex => { - const message = this.getErrorUserFriendlyMessage(args, ex) || 'Debug Error'; + const message = this.getUserFriendlyLaunchErrorMessage(args, ex) || 'Debug Error'; this.sendErrorResponse(response, { format: message, id: 1 }, undefined, undefined, ErrorDestination.User); }); } @@ -130,7 +145,7 @@ export class PythonDebugger extends DebugSession { const connectionTimeout = typeof (args as any).timeout === 'number' ? (args as any).timeout as number : DEBUGGER_CONNECT_TIMEOUT; return Math.max(connectionTimeout, MIN_DEBUGGER_CONNECT_TIMEOUT); } - private getErrorUserFriendlyMessage(launchArgs: LaunchRequestArguments, error: any): string | undefined { + private getUserFriendlyLaunchErrorMessage(launchArgs: LaunchRequestArguments, error: any): string | undefined { if (!error) { return; } @@ -141,6 +156,16 @@ export class PythonDebugger extends DebugSession { return errorMsg; } } + private getUserFriendlyAttachErrorMessage(error: any): string | undefined { + if (!error) { + return; + } + if (error.code === 'ECONNREFUSED' || error.errno === 'ECONNREFUSED') { + return `Failed to attach (${error.message})`; + } else { + return typeof error === 'string' ? error : ((error.message && error.message.length > 0) ? error.message : ''); + } + } } /** @@ -150,8 +175,8 @@ export class PythonDebugger extends DebugSession { */ class DebugManager implements Disposable { // #region VS Code debug Streams. - private inputStream: NodeJS.ReadStream | Socket; - private outputStream: NodeJS.WriteStream | Socket; + private inputStream!: NodeJS.ReadStream | Socket; + private outputStream!: NodeJS.WriteStream | Socket; // #endregion // #region Proxy Streams (used to listen in on the communications). private readonly throughOutputStream: PassThrough; @@ -162,19 +187,22 @@ class DebugManager implements Disposable { private readonly debugSessionInputStream: PassThrough; // #endregion // #region Streams used to communicate with PTVSD. - private ptvsdSocket: Socket; + private socket!: Socket; // #endregion private readonly inputProtocolParser: IProtocolParser; private readonly outputProtocolParser: IProtocolParser; private readonly protocolLogger: IProtocolLogger; private readonly protocolMessageWriter: IProtocolMessageWriter; - private isServerMode: boolean; + private isServerMode: boolean = false; private readonly disposables: Disposable[] = []; - private hasShutdown: boolean; + private hasShutdown: boolean = false; private debugSession?: PythonDebugger; private ptvsdProcessId?: number; - private killPTVSDProcess: boolean; - private terminatedEventSent: boolean; + private launchOrAttach?: 'launch' | 'attach'; + private terminatedEventSent: boolean = false; + private disconnectResponseSent: boolean = false; + private disconnectRequest?: DebugProtocol.DisconnectRequest; + private restart: boolean = false; private readonly initializeRequestDeferred: Deferred; private get initializeRequest(): Promise { return this.initializeRequestDeferred.promise; @@ -184,6 +212,11 @@ class DebugManager implements Disposable { return this.launchRequestDeferred.promise; } + private readonly attachRequestDeferred: Deferred; + private get attachRequest(): Promise { + return this.attachRequestDeferred.promise; + } + private set loggingEnabled(value: boolean) { if (value) { logger.setup(LogLevel.Verbose, true); @@ -211,8 +244,10 @@ class DebugManager implements Disposable { this.initializeRequestDeferred = createDeferred(); this.launchRequestDeferred = createDeferred(); + this.attachRequestDeferred = createDeferred(); } public dispose() { + logger.verbose('main dispose'); this.shutdown().ignoreErrors(); } public async start() { @@ -224,7 +259,11 @@ class DebugManager implements Disposable { this.inputStream.pause(); if (!this.isServerMode) { const currentProcess = this.serviceContainer.get(ICurrentProcess); - currentProcess.on('SIGTERM', this.shutdown); + currentProcess.on('SIGTERM', () => { + if (!this.restart) { + this.shutdown().ignoreErrors(); + } + }); } this.interceptProtocolMessages(); this.startDebugSession(); @@ -237,6 +276,7 @@ class DebugManager implements Disposable { * @private * @memberof DebugManager */ + // tslint:disable-next-line:cyclomatic-complexity private shutdown = async () => { logger.verbose('check and shutdown'); if (this.hasShutdown) { @@ -245,13 +285,8 @@ class DebugManager implements Disposable { this.hasShutdown = true; logger.verbose('shutdown'); - if (this.ptvsdSocket) { - this.throughInputStream.unpipe(this.ptvsdSocket); - this.ptvsdSocket.unpipe(this.throughOutputStream); - } - - if (!this.terminatedEventSent) { - // Possible VS Code has closed its stream. + if (!this.terminatedEventSent && !this.restart) { + // Possible PTVSD died before sending message back. try { logger.verbose('Sending Terminated Event'); this.sendMessage(new TerminatedEvent(), this.outputStream); @@ -263,7 +298,20 @@ class DebugManager implements Disposable { this.terminatedEventSent = true; } - if (this.killPTVSDProcess && this.ptvsdProcessId) { + if (!this.disconnectResponseSent && this.restart && this.disconnectRequest) { + // This is a work around for PTVSD bug, else this entire block is unnecessary. + try { + logger.verbose('Sending Disconnect Response'); + this.sendMessage(new Response(this.disconnectRequest, ''), this.outputStream); + } catch (err) { + const message = `Error in sending Disconnect Response: ${err && err.message ? err.message : err.toString()}`; + const details = [message, err && err.name ? err.name : '', err && err.stack ? err.stack : ''].join(EOL); + logger.error(`${message}${EOL}${details}`); + } + this.disconnectResponseSent = true; + } + + if (this.launchOrAttach === 'launch' && this.ptvsdProcessId) { logger.verbose('killing process'); try { // 1. Wait for some time, its possible the program has run to completion. @@ -271,21 +319,23 @@ class DebugManager implements Disposable { // 2. Also, its possible we manually sent the `Terminated` event above. // Hence we need to wait till VSC receives the above event. await sleep(100); + logger.verbose('Kill process now'); killProcessTree(this.ptvsdProcessId!); } catch { } - this.killPTVSDProcess = false; this.ptvsdProcessId = undefined; } - if (this.debugSession) { - logger.verbose('Shutting down debug session'); - this.debugSession.shutdown(); - } + if (!this.restart) { + if (this.debugSession) { + logger.verbose('Shutting down debug session'); + this.debugSession.shutdown(); + } - logger.verbose('disposing'); - await sleep(100); - // Dispose last, we don't want to dispose the protocol loggers too early. - this.disposables.forEach(disposable => disposable.dispose()); + logger.verbose('disposing'); + await sleep(100); + // Dispose last, we don't want to dispose the protocol loggers too early. + this.disposables.forEach(disposable => disposable.dispose()); + } } private sendMessage(message: DebugProtocol.ProtocolMessage, outputStream: Socket | PassThrough | NodeJS.WriteStream): void { this.protocolMessageWriter.write(outputStream, message); @@ -295,6 +345,9 @@ class DebugManager implements Disposable { this.debugSession = new PythonDebugger(this.serviceContainer); this.debugSession.setRunAsServer(this.isServerMode); + this.debugSession.once('debugger_attached', this.connectVSCodeToPTVSD); + this.debugSession.once('debugger_launched', this.connectVSCodeToPTVSD); + this.debugSessionOutputStream.pipe(this.throughOutputStream); this.debugSessionOutputStream.pipe(this.outputStream); @@ -309,64 +362,94 @@ class DebugManager implements Disposable { // Keep track of the initialize and launch requests, we'll need to re-send these to ptvsd, for bootstrapping. this.inputProtocolParser.once('request_initialize', this.onRequestInitialize); this.inputProtocolParser.once('request_launch', this.onRequestLaunch); + this.inputProtocolParser.once('request_attach', this.onRequestAttach); + this.inputProtocolParser.once('request_disconnect', this.onRequestDisconnect); this.outputProtocolParser.once('event_terminated', this.onEventTerminated); this.outputProtocolParser.once('response_disconnect', this.onResponseDisconnect); - this.outputProtocolParser.once('response_launch', this.connectVSCodeToPTVSD); } /** - * Once PTVSD process has been started (done by DebugSession), we need to connect PTVSD socket to VS Code. + * Connect PTVSD socket to VS Code. * This allows PTVSD to communicate directly with VS Code. * @private * @memberof DebugManager */ - private connectVSCodeToPTVSD = async () => { + private connectVSCodeToPTVSD = async (response: DebugProtocol.AttachResponse | DebugProtocol.LaunchResponse) => { + const attachOrLaunchRequest = await (this.launchOrAttach === 'attach' ? this.attachRequest : this.launchRequest); // By now we're connected to the client. - this.ptvsdSocket = await this.debugSession!.debugServer!.client; + this.socket = await this.debugSession!.debugServer!.client; // We need to handle both end and error, sometimes the socket will error out without ending (if debugee is killed). // Note, we need a handler for the error event, else nodejs complains when socket gets closed and there are no error handlers. - this.ptvsdSocket.on('end', this.shutdown); - this.ptvsdSocket.on('error', this.shutdown); - const debugSoketProtocolParser = this.serviceContainer.get(IProtocolParser); - debugSoketProtocolParser.connect(this.ptvsdSocket); - - // Send PTVSD the launch request (PTVSD needs to do its own initialization using launch arguments). - // E.g. redirectOutput & fixFilePathCase found in launch request are used to initialize the debugger. - this.sendMessage(await this.launchRequest, this.ptvsdSocket); - await new Promise(resolve => debugSoketProtocolParser.once('response_launch', resolve)); + this.socket.on('end', () => { + logger.verbose('Socket End'); + this.shutdown().ignoreErrors(); + }); + this.socket.on('error', () => { + logger.verbose('Socket Error'); + this.shutdown().ignoreErrors(); + }); + // Keep track of processid for killing it. + if (this.launchOrAttach === 'launch') { + const debugSoketProtocolParser = this.serviceContainer.get(IProtocolParser); + debugSoketProtocolParser.connect(this.socket); + debugSoketProtocolParser.once('event_process', (proc: DebugProtocol.ProcessEvent) => { + this.ptvsdProcessId = proc.body.systemProcessId; + }); + } - // The PTVSD process has launched, now send the initialize request to it (required by PTVSD). - this.sendMessage(await this.initializeRequest, this.ptvsdSocket); + // Get ready for PTVSD to communicate directly with VS Code. + (this.inputStream as any as NodeJS.ReadStream).unpipe(this.debugSessionInputStream); + this.debugSessionOutputStream.unpipe(this.outputStream); - // Keep track of processid for killing it. - debugSoketProtocolParser.once('event_process', (proc: DebugProtocol.ProcessEvent) => { - this.ptvsdProcessId = proc.body.systemProcessId; + // Do not pipe. When restarting the debugger, the socket gets closed, + // In which case, VSC will see this and shutdown the debugger completely. + (this.inputStream as any as NodeJS.ReadStream).on('data', data => { + this.socket.write(data); + }); + this.socket.on('data', (data: string | Buffer) => { + this.throughOutputStream.write(data); + this.outputStream.write(data as string); }); - // Wait for PTVSD to reply back with initialized event. - debugSoketProtocolParser.once('event_initialized', (initialized: DebugProtocol.InitializedEvent) => { - // Get ready for PTVSD to communicate directly with VS Code. - this.inputStream.unpipe(this.debugSessionInputStream); - this.debugSessionOutputStream.unpipe(this.outputStream); - - this.inputStream.pipe(this.ptvsdSocket!); - this.ptvsdSocket!.pipe(this.throughOutputStream); - this.ptvsdSocket!.pipe(this.outputStream); + // Send the launch/attach request to PTVSD and wait for it to reply back. + this.sendMessage(attachOrLaunchRequest, this.socket); - // Forward the initialized event sent by PTVSD onto VSCode. - // This is what will cause PTVSD to start the actualy work. - this.sendMessage(initialized, this.outputStream); - }); + // Send the initialize request and wait for it to reply back with the initialized event + this.sendMessage(await this.initializeRequest, this.socket); } private onRequestInitialize = (request: DebugProtocol.InitializeRequest) => { + this.hasShutdown = false; + this.terminatedEventSent = false; + this.disconnectResponseSent = false; + this.restart = false; + this.disconnectRequest = undefined; this.initializeRequestDeferred.resolve(request); } private onRequestLaunch = (request: DebugProtocol.LaunchRequest) => { - this.killPTVSDProcess = true; + this.launchOrAttach = 'launch'; this.loggingEnabled = (request.arguments as LaunchRequestArguments).logToFile === true; this.launchRequestDeferred.resolve(request); } + private onRequestAttach = (request: DebugProtocol.AttachRequest) => { + this.launchOrAttach = 'attach'; + this.loggingEnabled = (request.arguments as AttachRequestArguments).logToFile === true; + this.attachRequestDeferred.resolve(request); + } + private onRequestDisconnect = (request: DebugProtocol.DisconnectRequest) => { + this.disconnectRequest = request; + if (this.launchOrAttach === 'attach') { + return; + } + const args = request.arguments as { restart: boolean } | undefined; + if (args && args.restart) { + this.restart = true; + } + + // When VS Code sends a disconnect request, PTVSD replies back with a response. + // Wait for sometime, untill the messages are sent out (remember, we're just intercepting streams here). + setTimeout(this.shutdown, 500); + } private onEventTerminated = async () => { logger.verbose('onEventTerminated'); this.terminatedEventSent = true; @@ -374,6 +457,7 @@ class DebugManager implements Disposable { setTimeout(this.shutdown, 300); } private onResponseDisconnect = async () => { + this.disconnectResponseSent = true; logger.verbose('onResponseDisconnect'); // When VS Code sends a disconnect request, PTVSD replies back with a response, but its upto us to kill the process. // Wait for sometime, untill the messages are sent out (remember, we're just intercepting streams here). diff --git a/src/client/debugger/serviceRegistry.ts b/src/client/debugger/serviceRegistry.ts index b3e2a657d32a..3030f77d91e7 100644 --- a/src/client/debugger/serviceRegistry.ts +++ b/src/client/debugger/serviceRegistry.ts @@ -9,10 +9,11 @@ import { FileSystem } from '../common/platform/fileSystem'; import { PlatformService } from '../common/platform/platformService'; import { IFileSystem, IPlatformService } from '../common/platform/types'; import { CurrentProcess } from '../common/process/currentProcess'; -import { ICurrentProcess, ISocketServer } from '../common/types'; +import { ICurrentProcess, IExperimentalDebuggerBanner, ISocketServer } from '../common/types'; import { ServiceContainer } from '../ioc/container'; import { ServiceManager } from '../ioc/serviceManager'; import { IServiceContainer, IServiceManager } from '../ioc/types'; +import { ExperimentalDebuggerBanner } from './banner'; import { DebugStreamProvider } from './Common/debugStreamProvider'; import { ProtocolLogger } from './Common/protocolLogger'; import { ProtocolParser } from './Common/protocolParser'; @@ -24,11 +25,11 @@ export function initializeIoc(): IServiceContainer { const serviceManager = new ServiceManager(cont); const serviceContainer = new ServiceContainer(cont); serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); - registerTypes(serviceManager); + registerDebuggerTypes(serviceManager); return serviceContainer; } -function registerTypes(serviceManager: IServiceManager) { +function registerDebuggerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(ICurrentProcess, CurrentProcess); serviceManager.addSingleton(IDebugStreamProvider, DebugStreamProvider); serviceManager.addSingleton(IProtocolLogger, ProtocolLogger); @@ -38,3 +39,7 @@ function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(ISocketServer, SocketServer); serviceManager.addSingleton(IProtocolMessageWriter, ProtocolMessageWriter); } + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(IExperimentalDebuggerBanner, ExperimentalDebuggerBanner); +} diff --git a/src/client/extension.ts b/src/client/extension.ts index 4788030e98de..2761d4b6b863 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -5,12 +5,19 @@ if ((Reflect as any).metadata === undefined) { // tslint:disable-next-line:no-require-imports no-var-requires require('reflect-metadata'); } +import { StopWatch } from './common/stopWatch'; +// Do not move this linne of code (used to measure extension load times). +const stopWatch = new StopWatch(); + import { Container } from 'inversify'; -import * as vscode from 'vscode'; -import { Disposable, Memento, OutputChannel, window } from 'vscode'; +import { CodeActionKind, debug, Disposable, ExtensionContext, extensions, IndentAction, languages, Memento, OutputChannel, window } from 'vscode'; +import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry'; +import { IExtensionActivationService } from './activation/types'; +import { registerTypes as appRegisterTypes } from './application/serviceRegistry'; +import { IApplicationDiagnostics } from './application/types'; +import { IWorkspaceService } from './common/application/types'; import { PythonSettings } from './common/configSettings'; -import * as settings from './common/configSettings'; -import { STANDARD_OUTPUT_CHANNEL } from './common/constants'; +import { PYTHON, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from './common/constants'; import { FeatureDeprecationManager } from './common/featureDeprecationManager'; import { createDeferred } from './common/helpers'; import { PythonInstaller } from './common/installer/pythonInstallation'; @@ -18,93 +25,76 @@ import { registerTypes as installerRegisterTypes } from './common/installer/serv import { registerTypes as platformRegisterTypes } from './common/platform/serviceRegistry'; import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry'; import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; -import { GLOBAL_MEMENTO, IDisposableRegistry, ILogger, IMemento, IOutputChannel, IPersistentStateFactory, WORKSPACE_MEMENTO } from './common/types'; +import { ITerminalHelper } from './common/terminal/types'; +import { GLOBAL_MEMENTO, IConfigurationService, IDisposableRegistry, + IExperimentalDebuggerBanner, IExtensionContext, ILogger, IMemento, IOutputChannel, + IPersistentStateFactory, WORKSPACE_MEMENTO } from './common/types'; import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry'; +import { AttachRequestArguments, LaunchRequestArguments } from './debugger/Common/Contracts'; import { BaseConfigurationProvider } from './debugger/configProviders/baseProvider'; import { registerTypes as debugConfigurationRegisterTypes } from './debugger/configProviders/serviceRegistry'; +import { registerTypes as debuggerRegisterTypes } from './debugger/serviceRegistry'; import { IDebugConfigurationProvider } from './debugger/types'; import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; import { IInterpreterSelector } from './interpreter/configuration/types'; -import { ICondaService, IInterpreterService, IShebangCodeLensProvider } from './interpreter/contracts'; +import { ICondaService, IInterpreterService, PythonInterpreter } from './interpreter/contracts'; import { registerTypes as interpretersRegisterTypes } from './interpreter/serviceRegistry'; import { ServiceContainer } from './ioc/container'; import { ServiceManager } from './ioc/serviceManager'; -import { IServiceContainer } from './ioc/types'; -import { JediFactory } from './languageServices/jediProxyFactory'; +import { IServiceContainer, IServiceManager } from './ioc/types'; import { LinterCommands } from './linters/linterCommands'; import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; import { ILintingEngine } from './linters/types'; -import { PythonCompletionItemProvider } from './providers/completionProvider'; -import { PythonDefinitionProvider } from './providers/definitionProvider'; +import { PythonCodeActionProvider } from './providers/codeActionsProvider'; import { PythonFormattingEditProvider } from './providers/formatProvider'; -import { PythonHoverProvider } from './providers/hoverProvider'; import { LinterProvider } from './providers/linterProvider'; -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 { activateSimplePythonRefactorProvider } from './providers/simpleRefactorProvider'; -import { PythonSymbolProvider } from './providers/symbolProvider'; import { TerminalProvider } from './providers/terminalProvider'; import { activateUpdateSparkLibraryProvider } from './providers/updateSparkLibraryProvider'; import * as sortImports from './sortImports'; import { sendTelemetryEvent } from './telemetry'; import { EDITOR_LOAD } from './telemetry/constants'; -import { StopWatch } from './telemetry/stopWatch'; import { registerTypes as commonRegisterTerminalTypes } from './terminals/serviceRegistry'; import { ICodeExecutionManager } from './terminals/types'; import { BlockFormatProviders } from './typeFormatters/blockFormatProvider'; import { OnEnterFormatter } from './typeFormatters/onEnterFormatter'; import { TEST_OUTPUT_CHANNEL } from './unittests/common/constants'; -import * as tests from './unittests/main'; import { registerTypes as unitTestsRegisterTypes } from './unittests/serviceRegistry'; -import { WorkspaceSymbols } from './workspaceSymbols/main'; -const PYTHON: vscode.DocumentFilter = { language: 'python' }; const activationDeferred = createDeferred(); export const activated = activationDeferred.promise; // tslint:disable-next-line:max-func-body-length -export async function activate(context: vscode.ExtensionContext) { +export async function activate(context: ExtensionContext) { const cont = new Container(); const serviceManager = new ServiceManager(cont); const serviceContainer = new ServiceContainer(cont); - serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); - serviceManager.addSingletonInstance(IDisposableRegistry, context.subscriptions); - serviceManager.addSingletonInstance(IMemento, context.globalState, GLOBAL_MEMENTO); - serviceManager.addSingletonInstance(IMemento, context.workspaceState, WORKSPACE_MEMENTO); + registerServices(context, serviceManager, serviceContainer); - const standardOutputChannel = window.createOutputChannel('Python'); - const unitTestOutChannel = window.createOutputChannel('Python Test Log'); - serviceManager.addSingletonInstance(IOutputChannel, standardOutputChannel, STANDARD_OUTPUT_CHANNEL); - serviceManager.addSingletonInstance(IOutputChannel, unitTestOutChannel, TEST_OUTPUT_CHANNEL); + const appDiagnostics = serviceContainer.get(IApplicationDiagnostics); + await appDiagnostics.performPreStartupHealthCheck(); - commonRegisterTypes(serviceManager); - processRegisterTypes(serviceManager); - variableRegisterTypes(serviceManager); - unitTestsRegisterTypes(serviceManager); - lintersRegisterTypes(serviceManager); - interpretersRegisterTypes(serviceManager); - formattersRegisterTypes(serviceManager); - platformRegisterTypes(serviceManager); - installerRegisterTypes(serviceManager); - commonRegisterTerminalTypes(serviceManager); - debugConfigurationRegisterTypes(serviceManager); + const interpreterManager = serviceContainer.get(IInterpreterService); + // This must be completed before we can continue as language server needs the interpreter path. + interpreterManager.initialize(); + await interpreterManager.autoSetInterpreter(); - serviceManager.get(ICodeExecutionManager).registerCommands(); + const configuration = serviceManager.get(IConfigurationService); + const pythonSettings = configuration.getSettings(); - const persistentStateFactory = serviceManager.get(IPersistentStateFactory); - const pythonSettings = settings.PythonSettings.getInstance(); - // tslint:disable-next-line:no-floating-promises - sendStartupTelemetry(activated, serviceContainer); + const standardOutputChannel = serviceManager.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + context.subscriptions.push(languages.registerRenameProvider(PYTHON, new PythonRenameProvider(serviceManager))); + activateSimplePythonRefactorProvider(context, standardOutputChannel, serviceManager); - sortImports.activate(context, standardOutputChannel, serviceContainer); - const interpreterManager = serviceContainer.get(IInterpreterService); + const activationService = serviceContainer.get(IExtensionActivationService); + await activationService.activate(); - // This must be completed before we can continue. - interpreterManager.initialize(); - await interpreterManager.autoSetInterpreter(); + sortImports.activate(context, standardOutputChannel, serviceManager); + + serviceManager.get(ICodeExecutionManager).registerCommands(); + sendStartupTelemetry(activated, serviceContainer).ignoreErrors(); const pythonInstaller = new PythonInstaller(serviceContainer); pythonInstaller.checkPythonInstallation(PythonSettings.getInstance()) @@ -113,89 +103,118 @@ export async function activate(context: vscode.ExtensionContext) { interpreterManager.refresh() .catch(ex => console.error('Python Extension: interpreterManager.refresh', ex)); - context.subscriptions.push(serviceContainer.get(IInterpreterSelector)); - context.subscriptions.push(activateUpdateSparkLibraryProvider()); - activateSimplePythonRefactorProvider(context, standardOutputChannel, serviceContainer); - const jediFactory = new JediFactory(context.asAbsolutePath('.'), serviceContainer); - context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory)); + const jupyterExtension = extensions.getExtension('donjayamanne.jupyter'); + const lintingEngine = serviceManager.get(ILintingEngine); + lintingEngine.linkJupiterExtension(jupyterExtension).ignoreErrors(); - context.subscriptions.push(new ReplProvider(serviceContainer)); - context.subscriptions.push(new TerminalProvider(serviceContainer)); - context.subscriptions.push(new LinterCommands(serviceContainer)); + context.subscriptions.push(new LinterCommands(serviceManager)); + const linterProvider = new LinterProvider(context, serviceManager); + context.subscriptions.push(linterProvider); // Enable indentAction // tslint:disable-next-line:no-non-null-assertion - vscode.languages.setLanguageConfiguration(PYTHON.language!, { + languages.setLanguageConfiguration(PYTHON_LANGUAGE, { onEnterRules: [ { - beforeText: /^\s*(?:def|class|for|if|elif|else|while|try|with|finally|except|async)\b.*/, - action: { indentAction: vscode.IndentAction.Indent } + beforeText: /^\s*(?:def|class|for|if|elif|else|while|try|with|finally|except|async)\b.*:\s*/, + action: { indentAction: IndentAction.Indent } }, { beforeText: /^\s*#.*/, afterText: /.+$/, - action: { indentAction: vscode.IndentAction.None, appendText: '# ' } + action: { indentAction: IndentAction.None, appendText: '# ' } }, { beforeText: /^\s+(continue|break|return)\b.*/, afterText: /\s+$/, - action: { indentAction: vscode.IndentAction.Outdent } + action: { indentAction: IndentAction.Outdent } } ] }); - context.subscriptions.push(jediFactory); - context.subscriptions.push(vscode.languages.registerRenameProvider(PYTHON, new PythonRenameProvider(serviceContainer))); - const definitionProvider = new PythonDefinitionProvider(jediFactory); - context.subscriptions.push(vscode.languages.registerDefinitionProvider(PYTHON, definitionProvider)); - 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, serviceContainer), '.')); - context.subscriptions.push(vscode.languages.registerCodeLensProvider(PYTHON, serviceContainer.get(IShebangCodeLensProvider))); - - 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(jediFactory), '(', ',')); - } - if (pythonSettings.formatting.provider !== 'none') { + if (pythonSettings && pythonSettings.formatting && pythonSettings.formatting.provider !== 'none') { const formatProvider = new PythonFormattingEditProvider(context, serviceContainer); - context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider(PYTHON, formatProvider)); - context.subscriptions.push(vscode.languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider)); + context.subscriptions.push(languages.registerDocumentFormattingEditProvider(PYTHON, formatProvider)); + context.subscriptions.push(languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider)); } - const linterProvider = new LinterProvider(context, serviceContainer); - context.subscriptions.push(linterProvider); + context.subscriptions.push(languages.registerOnTypeFormattingEditProvider(PYTHON, new BlockFormatProviders(), ':')); + context.subscriptions.push(languages.registerOnTypeFormattingEditProvider(PYTHON, new OnEnterFormatter(), '\n')); - const jupyterExtension = vscode.extensions.getExtension('donjayamanne.jupyter'); - const lintingEngine = serviceContainer.get(ILintingEngine); - lintingEngine.linkJupiterExtension(jupyterExtension).ignoreErrors(); + const persistentStateFactory = serviceManager.get(IPersistentStateFactory); + const deprecationMgr = new FeatureDeprecationManager(persistentStateFactory, !!jupyterExtension); + deprecationMgr.initialize(); + context.subscriptions.push(new FeatureDeprecationManager(persistentStateFactory, !!jupyterExtension)); - tests.activate(context, unitTestOutChannel, symbolProvider, serviceContainer); + context.subscriptions.push(serviceContainer.get(IInterpreterSelector)); + context.subscriptions.push(activateUpdateSparkLibraryProvider()); - context.subscriptions.push(new WorkspaceSymbols(serviceContainer)); - context.subscriptions.push(vscode.languages.registerOnTypeFormattingEditProvider(PYTHON, new BlockFormatProviders(), ':')); - context.subscriptions.push(vscode.languages.registerOnTypeFormattingEditProvider(PYTHON, new OnEnterFormatter(), '\n')); + context.subscriptions.push(new ReplProvider(serviceContainer)); + context.subscriptions.push(new TerminalProvider(serviceContainer)); - serviceContainer.getAll(IDebugConfigurationProvider).forEach(debugConfig => { - context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider(debugConfig.debugType, debugConfig)); + context.subscriptions.push(languages.registerCodeActionsProvider(PYTHON, new PythonCodeActionProvider(), { providedCodeActionKinds: [CodeActionKind.SourceOrganizeImports] })); + + type ConfigurationProvider = BaseConfigurationProvider; + serviceContainer.getAll(IDebugConfigurationProvider).forEach(debugConfig => { + context.subscriptions.push(debug.registerDebugConfigurationProvider(debugConfig.debugType, debugConfig)); }); + + serviceContainer.get(IExperimentalDebuggerBanner).initialize(); activationDeferred.resolve(); +} - const deprecationMgr = new FeatureDeprecationManager(persistentStateFactory, !!jupyterExtension); - deprecationMgr.initialize(); - context.subscriptions.push(new FeatureDeprecationManager(persistentStateFactory, !!jupyterExtension)); +function registerServices(context: ExtensionContext, serviceManager: ServiceManager, serviceContainer: ServiceContainer) { + serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); + serviceManager.addSingletonInstance(IServiceManager, serviceManager); + serviceManager.addSingletonInstance(IDisposableRegistry, context.subscriptions); + serviceManager.addSingletonInstance(IMemento, context.globalState, GLOBAL_MEMENTO); + serviceManager.addSingletonInstance(IMemento, context.workspaceState, WORKSPACE_MEMENTO); + serviceManager.addSingletonInstance(IExtensionContext, context); + + const standardOutputChannel = window.createOutputChannel('Python'); + const unitTestOutChannel = window.createOutputChannel('Python Test Log'); + serviceManager.addSingletonInstance(IOutputChannel, standardOutputChannel, STANDARD_OUTPUT_CHANNEL); + serviceManager.addSingletonInstance(IOutputChannel, unitTestOutChannel, TEST_OUTPUT_CHANNEL); + + activationRegisterTypes(serviceManager); + commonRegisterTypes(serviceManager); + processRegisterTypes(serviceManager); + variableRegisterTypes(serviceManager); + unitTestsRegisterTypes(serviceManager); + lintersRegisterTypes(serviceManager); + interpretersRegisterTypes(serviceManager); + formattersRegisterTypes(serviceManager); + platformRegisterTypes(serviceManager); + installerRegisterTypes(serviceManager); + commonRegisterTerminalTypes(serviceManager); + debugConfigurationRegisterTypes(serviceManager); + debuggerRegisterTypes(serviceManager); + appRegisterTypes(serviceManager); } async function sendStartupTelemetry(activatedPromise: Promise, serviceContainer: IServiceContainer) { - const stopWatch = new StopWatch(); const logger = serviceContainer.get(ILogger); try { await activatedPromise; + const terminalHelper = serviceContainer.get(ITerminalHelper); + const terminalShellType = terminalHelper.identifyTerminalShell(terminalHelper.getTerminalShellPath()); const duration = stopWatch.elapsedTime; const condaLocator = serviceContainer.get(ICondaService); - const condaVersion = await condaLocator.getCondaVersion().catch(() => undefined); - const props = condaVersion ? { condaVersion } : undefined; + const interpreterService = serviceContainer.get(IInterpreterService); + const [condaVersion, interpreter, interpreters] = await Promise.all([ + condaLocator.getCondaVersion().catch(() => undefined), + interpreterService.getActiveInterpreter().catch(() => undefined), + interpreterService.getInterpreters().catch(() => []) + ]); + const workspaceService = serviceContainer.get(IWorkspaceService); + const workspaceFolderCount = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders!.length : 0; + const pythonVersion = interpreter ? interpreter.version_info.join('.') : undefined; + const interpreterType = interpreter ? interpreter.type : undefined; + const hasPython3 = interpreters + .filter(item => item && Array.isArray(item.version_info) ? item.version_info[0] === 3 : false) + .length > 0; + + const props = { condaVersion, terminal: terminalShellType, pythonVersion, interpreterType, workspaceFolderCount, hasPython3 }; sendTelemetryEvent(EDITOR_LOAD, duration, props); } catch (ex) { logger.logError('sendStartupTelemetry failed.', ex); diff --git a/src/client/formatters/autoPep8Formatter.ts b/src/client/formatters/autoPep8Formatter.ts index 316fab349cc9..b3818c8eef0e 100644 --- a/src/client/formatters/autoPep8Formatter.ts +++ b/src/client/formatters/autoPep8Formatter.ts @@ -1,10 +1,10 @@ import * as vscode from 'vscode'; import { Product } from '../common/installer/productInstaller'; +import { StopWatch } from '../common/stopWatch'; import { IConfigurationService } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { sendTelemetryWhenDone } from '../telemetry'; import { FORMAT } from '../telemetry/constants'; -import { StopWatch } from '../telemetry/stopWatch'; import { BaseFormatter } from './baseFormatter'; export class AutoPep8Formatter extends BaseFormatter { diff --git a/src/client/formatters/baseFormatter.ts b/src/client/formatters/baseFormatter.ts index 9e91b00a5a19..7fd32abae119 100644 --- a/src/client/formatters/baseFormatter.ts +++ b/src/client/formatters/baseFormatter.ts @@ -1,7 +1,6 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import * as vscode from 'vscode'; -import { OutputChannel, TextEdit, Uri } from 'vscode'; import { IWorkspaceService } from '../common/application/types'; import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; import '../common/extensions'; @@ -13,12 +12,12 @@ import { getTempFileWithDocumentContents, getTextEditsFromPatch } from './../com import { IFormatterHelper } from './types'; export abstract class BaseFormatter { - protected readonly outputChannel: OutputChannel; + protected readonly outputChannel: vscode.OutputChannel; protected readonly workspace: IWorkspaceService; private readonly helper: IFormatterHelper; constructor(public Id: string, private product: Product, protected serviceContainer: IServiceContainer) { - this.outputChannel = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + this.outputChannel = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); this.helper = serviceContainer.get(IFormatterHelper); this.workspace = serviceContainer.get(IWorkspaceService); } @@ -42,15 +41,14 @@ export abstract class BaseFormatter { return vscode.Uri.file(__dirname); } protected async provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, args: string[], cwd?: string): Promise { - this.outputChannel.clear(); if (typeof cwd !== 'string' || cwd.length === 0) { cwd = this.getWorkspaceUri(document).fsPath; } // autopep8 and yapf 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. + // Yet 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 tempFile = await this.createTempFile(document); if (this.checkCancellation(document.fileName, tempFile, token)) { return []; @@ -59,21 +57,21 @@ export abstract class BaseFormatter { const executionInfo = this.helper.getExecutionInfo(this.product, args, document.uri); executionInfo.args.push(tempFile); const pythonToolsExecutionService = this.serviceContainer.get(IPythonToolExecutionService); - const promise = pythonToolsExecutionService.exec(executionInfo, { cwd, throwOnStdErr: true, token }, document.uri) + const promise = pythonToolsExecutionService.exec(executionInfo, { cwd, throwOnStdErr: false, token }, document.uri) .then(output => output.stdout) .then(data => { if (this.checkCancellation(document.fileName, tempFile, token)) { - return [] as TextEdit[]; + return [] as vscode.TextEdit[]; } return getTextEditsFromPatch(document.getText(), data); }) .catch(error => { if (this.checkCancellation(document.fileName, tempFile, token)) { - return [] as TextEdit[]; + return [] as vscode.TextEdit[]; } // tslint:disable-next-line:no-empty this.handleError(this.Id, error, document.uri).catch(() => { }); - return [] as TextEdit[]; + return [] as vscode.TextEdit[]; }) .then(edits => { this.deleteTempFile(document.fileName, tempFile).ignoreErrors(); @@ -83,7 +81,7 @@ export abstract class BaseFormatter { return promise; } - protected async handleError(expectedFileName: string, error: Error, resource?: Uri) { + protected async handleError(expectedFileName: string, error: Error, resource?: vscode.Uri) { let customError = `Formatting with ${this.Id} failed.`; if (isNotInstalledError(error)) { @@ -100,7 +98,7 @@ export abstract class BaseFormatter { private async createTempFile(document: vscode.TextDocument): Promise { return document.isDirty - ? await getTempFileWithDocumentContents(document) + ? getTempFileWithDocumentContents(document) : document.fileName; } diff --git a/src/client/formatters/blackFormatter.ts b/src/client/formatters/blackFormatter.ts new file mode 100644 index 000000000000..d1bb356dc584 --- /dev/null +++ b/src/client/formatters/blackFormatter.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as vscode from 'vscode'; +import { Product } from '../common/installer/productInstaller'; +import { StopWatch } from '../common/stopWatch'; +import { IConfigurationService } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { sendTelemetryWhenDone } from '../telemetry'; +import { FORMAT } from '../telemetry/constants'; +import { BaseFormatter } from './baseFormatter'; + +export class BlackFormatter extends BaseFormatter { + constructor(serviceContainer: IServiceContainer) { + super('black', Product.black, serviceContainer); + } + + public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable { + const stopWatch = new StopWatch(); + const settings = this.serviceContainer.get(IConfigurationService).getSettings(document.uri); + const hasCustomArgs = Array.isArray(settings.formatting.blackArgs) && settings.formatting.blackArgs.length > 0; + const formatSelection = range ? !range.isEmpty : false; + + if (formatSelection) { + const errorMessage = async () => { + // Black does not support partial formatting on purpose. + await vscode.window.showErrorMessage('Black does not support the "Format Selection" command'); + return [] as vscode.TextEdit[]; + }; + + return errorMessage(); + } + + const blackArgs = ['--diff', '--quiet']; + const promise = super.provideDocumentFormattingEdits(document, options, token, blackArgs); + sendTelemetryWhenDone(FORMAT, promise, stopWatch, { tool: 'black', hasCustomArgs, formatSelection }); + return promise; + } +} diff --git a/src/client/formatters/helper.ts b/src/client/formatters/helper.ts index e5930c2f0683..95383e69b538 100644 --- a/src/client/formatters/helper.ts +++ b/src/client/formatters/helper.ts @@ -4,17 +4,17 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; -import { IConfigurationService, IFormattingSettings } from '../common/types'; -import { ExecutionInfo, Product } from '../common/types'; +import { ExecutionInfo, IConfigurationService, IFormattingSettings, Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { FormatterId, FormatterSettingsPropertyNames, IFormatterHelper } from './types'; @injectable() export class FormatterHelper implements IFormatterHelper { - constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer) { } + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } public translateToId(formatter: Product): FormatterId { switch (formatter) { case Product.autopep8: return 'autopep8'; + case Product.black: return 'black'; case Product.yapf: return 'yapf'; default: { throw new Error(`Unrecognized Formatter '${formatter}'`); @@ -39,7 +39,6 @@ export class FormatterHelper implements IFormatterHelper { let moduleName: string | undefined; // If path information is not available, then treat it as a module, - // except for prospector as that needs to be run as an executable (it's a Python package). if (path.basename(execPath) === execPath) { moduleName = execPath; } diff --git a/src/client/formatters/lineFormatter.ts b/src/client/formatters/lineFormatter.ts index 4b3bff70aa8d..867a115c1a64 100644 --- a/src/client/formatters/lineFormatter.ts +++ b/src/client/formatters/lineFormatter.ts @@ -3,21 +3,42 @@ // tslint:disable-next-line:import-name import Char from 'typescript-char'; +import { Position, Range, TextDocument } from 'vscode'; import { BraceCounter } from '../language/braceCounter'; import { TextBuilder } from '../language/textBuilder'; +import { TextRangeCollection } from '../language/textRangeCollection'; import { Tokenizer } from '../language/tokenizer'; import { ITextRangeCollection, IToken, TokenType } from '../language/types'; +const keywordsWithSpaceBeforeBrace = [ + 'and', 'as', 'assert', 'await', + 'del', + 'except', 'elif', + 'for', 'from', + 'global', + 'if', 'import', 'in', 'is', + 'lambda', + 'nonlocal', 'not', + 'or', + 'raise', 'return', + 'while', 'with', + 'yield' +]; + export class LineFormatter { - private builder: TextBuilder; - private tokens: ITextRangeCollection; - private braceCounter: BraceCounter; - private text: string; + private builder = new TextBuilder(); + private tokens: ITextRangeCollection = new TextRangeCollection([]); + private braceCounter = new BraceCounter(); + private text = ''; + private document?: TextDocument; + private lineNumber = 0; // tslint:disable-next-line:cyclomatic-complexity - public formatLine(text: string): string { - this.tokens = new Tokenizer().tokenize(text); - this.text = text; + public formatLine(document: TextDocument, lineNumber: number): string { + this.document = document; + this.lineNumber = lineNumber; + this.text = document.lineAt(lineNumber).text; + this.tokens = new Tokenizer().tokenize(this.text); this.builder = new TextBuilder(); this.braceCounter = new BraceCounter(); @@ -27,7 +48,7 @@ export class LineFormatter { const ws = this.text.substr(0, this.tokens.getItemAt(0).start); if (ws.length > 0) { - this.builder.append(ws); // Preserve leading indentation + this.builder.append(ws); // Preserve leading indentation. } for (let i = 0; i < this.tokens.count; i += 1) { @@ -42,7 +63,7 @@ export class LineFormatter { case TokenType.Comma: this.builder.append(','); - if (next && !this.isCloseBraceType(next.type)) { + if (next && !this.isCloseBraceType(next.type) && next.type !== TokenType.Colon) { this.builder.softAppendSpace(); } break; @@ -51,79 +72,223 @@ export class LineFormatter { if (prev && !this.isOpenBraceType(prev.type) && prev.type !== TokenType.Colon && prev.type !== TokenType.Operator) { this.builder.softAppendSpace(); } - this.builder.append(this.text.substring(t.start, t.end)); + const id = this.text.substring(t.start, t.end); + this.builder.append(id); + if (this.isKeywordWithSpaceBeforeBrace(id) && next && this.isOpenBraceType(next.type)) { + // for x in () + this.builder.softAppendSpace(); + } break; case TokenType.Colon: - // x: 1 if not in slice, x[1:y] if inside the slice + // x: 1 if not in slice, x[1:y] if inside the slice. this.builder.append(':'); if (!this.braceCounter.isOpened(TokenType.OpenBracket) && (next && next.type !== TokenType.Colon)) { - // Not inside opened [[ ... ] sequence + // Not inside opened [[ ... ] sequence. this.builder.softAppendSpace(); } break; case TokenType.Comment: - // add space before in-line comment + // Add 2 spaces before in-line comment per PEP guidelines. if (prev) { - this.builder.softAppendSpace(); + this.builder.softAppendSpace(2); } this.builder.append(this.text.substring(t.start, t.end)); break; + case TokenType.Semicolon: + this.builder.append(';'); + break; + default: - this.handleOther(t); + this.handleOther(t, i); break; } } return this.builder.getText(); } + // tslint:disable-next-line:cyclomatic-complexity private handleOperator(index: number): void { const t = this.tokens.getItemAt(index); + const prev = index > 0 ? this.tokens.getItemAt(index - 1) : undefined; + const opCode = this.text.charCodeAt(t.start); + const next = index < this.tokens.count - 1 ? this.tokens.getItemAt(index + 1) : undefined; + if (t.length === 1) { - const opCode = this.text.charCodeAt(t.start); switch (opCode) { case Char.Equal: - if (index >= 2 && this.handleEqual(t, index)) { - return; - } - break; + this.handleEqual(t, index); + return; case Char.Period: + if (prev && this.isKeyword(prev, 'from')) { + this.builder.softAppendSpace(); + } this.builder.append('.'); + if (next && this.isKeyword(next, 'import')) { + this.builder.softAppendSpace(); + } return; case Char.At: - this.builder.append('@'); + if (prev) { + // Binary case + this.builder.softAppendSpace(); + this.builder.append('@'); + this.builder.softAppendSpace(); + } else { + this.builder.append('@'); + } return; + case Char.ExclamationMark: + this.builder.append('!'); + return; + case Char.Asterisk: + if (prev && this.isKeyword(prev, 'lambda')) { + this.builder.softAppendSpace(); + this.builder.append('*'); + return; + } + if (this.handleStarOperator(t, prev)) { + return; + } + break; default: break; } + } else if (t.length === 2) { + if (this.text.charCodeAt(t.start) === Char.Asterisk && this.text.charCodeAt(t.start + 1) === Char.Asterisk) { + if (this.handleStarOperator(t, prev)) { + return; + } + } + } + + // Do not append space if operator is preceded by '(' or ',' as in foo(**kwarg) + if (prev && (this.isOpenBraceType(prev.type) || prev.type === TokenType.Comma)) { + this.builder.append(this.text.substring(t.start, t.end)); + return; } + this.builder.softAppendSpace(); this.builder.append(this.text.substring(t.start, t.end)); + + // Check unary case + if (prev && prev.type === TokenType.Operator) { + if (opCode === Char.Hyphen || opCode === Char.Plus || opCode === Char.Tilde) { + return; + } + } this.builder.softAppendSpace(); } - private handleEqual(t: IToken, index: number): boolean { - if (this.braceCounter.isOpened(TokenType.OpenBrace)) { - // Check if this is = in function arguments. If so, do not - // add spaces around it. - const prev = this.tokens.getItemAt(index - 1); - const prevPrev = this.tokens.getItemAt(index - 2); - if (prev.type === TokenType.Identifier && - (prevPrev.type === TokenType.Comma || prevPrev.type === TokenType.OpenBrace)) { - this.builder.append('='); + private handleStarOperator(current: IToken, prev: IToken): boolean { + if (this.text.charCodeAt(current.start) === Char.Asterisk && this.text.charCodeAt(current.start + 1) === Char.Asterisk) { + if (!prev || (prev.type !== TokenType.Identifier && prev.type !== TokenType.Number)) { + this.builder.append('**'); + return true; + } + if (prev && this.isKeyword(prev, 'lambda')) { + this.builder.softAppendSpace(); + this.builder.append('**'); return true; } } + // Check previous line for the **/* condition + const lastLine = this.getPreviousLineTokens(); + const lastToken = lastLine && lastLine.count > 0 ? lastLine.getItemAt(lastLine.count - 1) : undefined; + if (lastToken && (this.isOpenBraceType(lastToken.type) || lastToken.type === TokenType.Comma)) { + this.builder.append(this.text.substring(current.start, current.end)); + return true; + } return false; } - private handleOther(t: IToken): void { + private handleEqual(t: IToken, index: number): void { + if (this.isMultipleStatements(index) && !this.braceCounter.isOpened(TokenType.OpenBrace)) { + // x = 1; x, y = y, x + this.builder.softAppendSpace(); + this.builder.append('='); + this.builder.softAppendSpace(); + return; + } + + // Check if this is = in function arguments. If so, do not add spaces around it. + if (this.isEqualsInsideArguments(index)) { + this.builder.append('='); + return; + } + + this.builder.softAppendSpace(); + this.builder.append('='); + this.builder.softAppendSpace(); + } + + private handleOther(t: IToken, index: number): void { if (this.isBraceType(t.type)) { this.braceCounter.countBrace(t); + this.builder.append(this.text.substring(t.start, t.end)); + return; + } + + const prev = index > 0 ? this.tokens.getItemAt(index - 1) : undefined; + if (prev && prev.length === 1 && this.text.charCodeAt(prev.start) === Char.Equal && this.isEqualsInsideArguments(index - 1)) { + // Don't add space around = inside function arguments. + this.builder.append(this.text.substring(t.start, t.end)); + return; + } + + if (prev && (this.isOpenBraceType(prev.type) || prev.type === TokenType.Colon)) { + // Don't insert space after (, [ or { . + this.builder.append(this.text.substring(t.start, t.end)); + return; + } + + if (t.type === TokenType.Number && prev && prev.type === TokenType.Operator && prev.length === 1 && this.text.charCodeAt(prev.start) === Char.Tilde) { + // Special case for ~ before numbers + this.builder.append(this.text.substring(t.start, t.end)); + return; + } + + if (t.type === TokenType.Unknown) { + this.handleUnknown(t); + } else { + // In general, keep tokens separated. + this.builder.softAppendSpace(); + this.builder.append(this.text.substring(t.start, t.end)); + } + } + + private handleUnknown(t: IToken): void { + const prevChar = t.start > 0 ? this.text.charCodeAt(t.start - 1) : 0; + if (prevChar === Char.Space || prevChar === Char.Tab) { + this.builder.softAppendSpace(); } this.builder.append(this.text.substring(t.start, t.end)); + + const nextChar = t.end < this.text.length - 1 ? this.text.charCodeAt(t.end) : 0; + if (nextChar === Char.Space || nextChar === Char.Tab) { + this.builder.softAppendSpace(); + } + } + + // tslint:disable-next-line:cyclomatic-complexity + private isEqualsInsideArguments(index: number): boolean { + if (index < 1) { + return false; + } + + // We are looking for IDENT = ? + const prev = this.tokens.getItemAt(index - 1); + if (prev.type !== TokenType.Identifier) { + return false; + } + + if (index > 1 && this.tokens.getItemAt(index - 2).type === TokenType.Colon) { + return false; // Type hint should have spaces around like foo(x: int = 1) per PEP 8 + } + + return this.isInsideFunctionArguments(this.tokens.getItemAt(index).start); } private isOpenBraceType(type: TokenType): boolean { @@ -135,4 +300,142 @@ export class LineFormatter { private isBraceType(type: TokenType): boolean { return this.isOpenBraceType(type) || this.isCloseBraceType(type); } + + private isMultipleStatements(index: number): boolean { + for (let i = index; i >= 0; i -= 1) { + if (this.tokens.getItemAt(i).type === TokenType.Semicolon) { + return true; + } + } + return false; + } + + private isKeywordWithSpaceBeforeBrace(s: string): boolean { + return keywordsWithSpaceBeforeBrace.indexOf(s) >= 0; + } + private isKeyword(t: IToken, keyword: string): boolean { + return t.type === TokenType.Identifier && t.length === keyword.length && this.text.substr(t.start, t.length) === keyword; + } + + // tslint:disable-next-line:cyclomatic-complexity + private isInsideFunctionArguments(position: number): boolean { + if (!this.document) { + return false; // unable to determine + } + + // Walk up until beginning of the document or line with 'def IDENT(' or line ending with : + // IDENT( by itself is not reliable since they can be nested in IDENT(IDENT(a), x=1) + let start = new Position(0, 0); + for (let i = this.lineNumber; i >= 0; i -= 1) { + const line = this.document.lineAt(i); + const lineTokens = new Tokenizer().tokenize(line.text); + if (lineTokens.count === 0) { + continue; + } + // 'def IDENT(' + const first = lineTokens.getItemAt(0); + if (lineTokens.count >= 3 && + first.length === 3 && line.text.substr(first.start, first.length) === 'def' && + lineTokens.getItemAt(1).type === TokenType.Identifier && + lineTokens.getItemAt(2).type === TokenType.OpenBrace) { + start = line.range.start; + break; + } + + if (lineTokens.count > 0 && i < this.lineNumber) { + // One of previous lines ends with : + const last = lineTokens.getItemAt(lineTokens.count - 1); + if (last.type === TokenType.Colon) { + start = this.document.lineAt(i + 1).range.start; + break; + } else if (lineTokens.count > 1) { + const beforeLast = lineTokens.getItemAt(lineTokens.count - 2); + if (beforeLast.type === TokenType.Colon && last.type === TokenType.Comment) { + start = this.document.lineAt(i + 1).range.start; + break; + } + } + } + } + + // Now tokenize from the nearest reasonable point + const currentLine = this.document.lineAt(this.lineNumber); + const text = this.document.getText(new Range(start, currentLine.range.end)); + const tokens = new Tokenizer().tokenize(text); + + // Translate position in the line being formatted to the position in the tokenized block + position = this.document.offsetAt(currentLine.range.start) + position - this.document.offsetAt(start); + + // Walk tokens locating narrowest function signature as in IDENT( | ) + let funcCallStartIndex = -1; + let funcCallEndIndex = -1; + for (let i = 0; i < tokens.count - 1; i += 1) { + const t = tokens.getItemAt(i); + if (t.type === TokenType.Identifier) { + const next = tokens.getItemAt(i + 1); + if (next.type === TokenType.OpenBrace && !this.isKeywordWithSpaceBeforeBrace(text.substr(t.start, t.length))) { + // We are at IDENT(, try and locate the closing brace + let closeBraceIndex = this.findClosingBrace(tokens, i + 1); + // Closing brace is not required in case construct is not yet terminated + closeBraceIndex = closeBraceIndex > 0 ? closeBraceIndex : tokens.count - 1; + // Are we in range? + if (position > next.start && position < tokens.getItemAt(closeBraceIndex).start) { + funcCallStartIndex = i; + funcCallEndIndex = closeBraceIndex; + } + } + } + } + // Did we find anything? + if (funcCallStartIndex < 0) { + // No? See if we are between 'lambda' and ':' + for (let i = 0; i < tokens.count; i += 1) { + const t = tokens.getItemAt(i); + if (t.type === TokenType.Identifier && text.substr(t.start, t.length) === 'lambda') { + if (position < t.start) { + break; // Position is before the nearest 'lambda' + } + let colonIndex = this.findNearestColon(tokens, i + 1); + // Closing : is not required in case construct is not yet terminated + colonIndex = colonIndex > 0 ? colonIndex : tokens.count - 1; + if (position > t.start && position < tokens.getItemAt(colonIndex).start) { + funcCallStartIndex = i; + funcCallEndIndex = colonIndex; + } + } + } + } + return funcCallStartIndex >= 0 && funcCallEndIndex > 0; + } + + private findNearestColon(tokens: ITextRangeCollection, index: number): number { + for (let i = index; i < tokens.count; i += 1) { + if (tokens.getItemAt(i).type === TokenType.Colon) { + return i; + } + } + return -1; + } + + private findClosingBrace(tokens: ITextRangeCollection, index: number): number { + const braceCounter = new BraceCounter(); + for (let i = index; i < tokens.count; i += 1) { + const t = tokens.getItemAt(i); + if (t.type === TokenType.OpenBrace || t.type === TokenType.CloseBrace) { + braceCounter.countBrace(t); + } + if (braceCounter.count === 0) { + return i; + } + } + return -1; + } + + private getPreviousLineTokens(): ITextRangeCollection | undefined { + if (!this.document || this.lineNumber === 0) { + return undefined; // unable to determine + } + const line = this.document.lineAt(this.lineNumber - 1); + return new Tokenizer().tokenize(line.text); + } } diff --git a/src/client/formatters/types.ts b/src/client/formatters/types.ts index d21ace78bb4f..7f4bcf5b7524 100644 --- a/src/client/formatters/types.ts +++ b/src/client/formatters/types.ts @@ -6,7 +6,7 @@ import { ExecutionInfo, IFormattingSettings, Product } from '../common/types'; export const IFormatterHelper = Symbol('IFormatterHelper'); -export type FormatterId = 'autopep8' | 'yapf'; +export type FormatterId = 'autopep8' | 'black' | 'yapf'; export type FormatterSettingsPropertyNames = { argsName: keyof IFormattingSettings; diff --git a/src/client/formatters/yapfFormatter.ts b/src/client/formatters/yapfFormatter.ts index 31a768ca0a57..7482970c35d0 100644 --- a/src/client/formatters/yapfFormatter.ts +++ b/src/client/formatters/yapfFormatter.ts @@ -1,9 +1,9 @@ import * as vscode from 'vscode'; +import { StopWatch } from '../common/stopWatch'; import { IConfigurationService, Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { sendTelemetryWhenDone } from '../telemetry'; import { FORMAT } from '../telemetry/constants'; -import { StopWatch } from '../telemetry/stopWatch'; import { BaseFormatter } from './baseFormatter'; export class YapfFormatter extends BaseFormatter { diff --git a/src/client/interpreter/configuration/interpreterSelector.ts b/src/client/interpreter/configuration/interpreterSelector.ts index 1d116c7e2ab6..716dced6c0f7 100644 --- a/src/client/interpreter/configuration/interpreterSelector.ts +++ b/src/client/interpreter/configuration/interpreterSelector.ts @@ -76,10 +76,9 @@ export class InterpreterSelector implements IInterpreterSelector { private async removeDuplicates(interpreters: PythonInterpreter[]): Promise { const result: PythonInterpreter[] = []; - await Promise.all(interpreters.map(async item => item.realPath = await this.fileSystem.getRealPathAsync(item.path))); interpreters.forEach(x => { if (result.findIndex(a => a.displayName === x.displayName - && a.type === x.type && this.fileSystem.arePathsSame(a.realPath!, x.realPath!)) < 0) { + && a.type === x.type && this.fileSystem.arePathsSame(path.dirname(a.path), path.dirname(x.path))) < 0) { result.push(x); } }); diff --git a/src/client/interpreter/configuration/pythonPathUpdaterService.ts b/src/client/interpreter/configuration/pythonPathUpdaterService.ts index 67a6e0f5009d..e9579a565177 100644 --- a/src/client/interpreter/configuration/pythonPathUpdaterService.ts +++ b/src/client/interpreter/configuration/pythonPathUpdaterService.ts @@ -1,10 +1,11 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { ConfigurationTarget, Uri, window } from 'vscode'; +import { InterpreterInfomation, IPythonExecutionFactory } from '../../common/process/types'; +import { StopWatch } from '../../common/stopWatch'; import { IServiceContainer } from '../../ioc/types'; import { sendTelemetryEvent } from '../../telemetry'; import { PYTHON_INTERPRETER } from '../../telemetry/constants'; -import { StopWatch } from '../../telemetry/stopWatch'; import { PythonInterpreterTelemetry } from '../../telemetry/types'; import { IInterpreterVersionService } from '../contracts'; import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } from './types'; @@ -13,9 +14,11 @@ import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } fr export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManager { private readonly pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory; private readonly interpreterVersionService: IInterpreterVersionService; + private readonly executionFactory: IPythonExecutionFactory; constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { this.pythonPathSettingsUpdaterFactory = serviceContainer.get(IPythonPathUpdaterServiceFactory); this.interpreterVersionService = serviceContainer.get(IInterpreterVersionService); + this.executionFactory = serviceContainer.get(IPythonExecutionFactory); } public async updatePythonPath(pythonPath: string, configTarget: ConfigurationTarget, trigger: 'ui' | 'shebang' | 'load', wkspace?: Uri): Promise { const stopWatch = new StopWatch(); @@ -39,17 +42,17 @@ export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManage failed, trigger }; if (!failed) { - const pyVersionPromise = this.interpreterVersionService.getVersion(pythonPath, '') - .then(pyVersion => pyVersion.length === 0 ? undefined : pyVersion); + const processService = await this.executionFactory.create({ pythonPath }); + const infoPromise = processService.getInterpreterInformation().catch(() => undefined); const pipVersionPromise = this.interpreterVersionService.getPipVersion(pythonPath) .then(value => value.length === 0 ? undefined : value) - .catch(() => undefined); - const versions = await Promise.all([pyVersionPromise, pipVersionPromise]); - if (versions[0]) { - telemtryProperties.version = versions[0] as string; + .catch(() => undefined); + const [info, pipVersion] = await Promise.all([infoPromise, pipVersionPromise]); + if (info) { + telemtryProperties.version = info.version; } - if (versions[1]) { - telemtryProperties.pipVersion = versions[1] as string; + if (pipVersion) { + telemtryProperties.pipVersion = pipVersion; } } sendTelemetryEvent(PYTHON_INTERPRETER, duration, telemtryProperties); diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index b71c77eb5ccd..6d23d28ea739 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -1,5 +1,5 @@ import { CodeLensProvider, ConfigurationTarget, Disposable, Event, TextDocument, Uri } from 'vscode'; -import { Architecture } from '../common/platform/types'; +import { InterpreterInfomation } from '../common/process/types'; export const INTERPRETER_LOCATOR_SERVICE = 'IInterpreterLocatorService'; export const WINDOWS_REGISTRY_SERVICE = 'WindowsRegistryService'; @@ -10,7 +10,6 @@ export const KNOWN_PATH_SERVICE = 'KnownPathsService'; export const GLOBAL_VIRTUAL_ENV_SERVICE = 'VirtualEnvService'; export const WORKSPACE_VIRTUAL_ENV_SERVICE = 'WorkspaceVirtualEnvService'; export const PIPENV_SERVICE = 'PipEnvService'; - export const IInterpreterVersionService = Symbol('IInterpreterVersionService'); export interface IInterpreterVersionService { getVersion(pythonPath: string, defaultValue: string): Promise; @@ -45,29 +44,27 @@ export interface ICondaService { isCondaAvailable(): Promise; getCondaVersion(): Promise; getCondaInfo(): Promise; - getCondaEnvironments(ignoreCache: boolean): Promise<({ name: string, path: string }[]) | undefined>; + getCondaEnvironments(ignoreCache: boolean): Promise<({ name: string; path: string }[]) | undefined>; getInterpreterPath(condaEnvironmentPath: string): string; isCondaEnvironment(interpreterPath: string): Promise; - getCondaEnvironment(interpreterPath: string): Promise<{ name: string, path: string } | undefined>; + getCondaEnvironment(interpreterPath: string): Promise<{ name: string; path: string } | undefined>; } export enum InterpreterType { - Unknown = 1, - Conda = 2, - VirtualEnv = 4 + Unknown = 'Unknown', + Conda = 'Conda', + VirtualEnv = 'VirtualEnv', + PipEnv = 'PipEnv', + Pyenv = 'Pyenv', + Venv = 'Venv' } - -export type PythonInterpreter = { - path: string; +export type PythonInterpreter = InterpreterInfomation & { companyDisplayName?: string; displayName?: string; - version?: string; - architecture?: Architecture; type: InterpreterType; envName?: string; envPath?: string; cachedEntry?: boolean; - realPath?: string; }; export type WorkspacePythonPath = { @@ -81,6 +78,7 @@ export interface IInterpreterService { getInterpreters(resource?: Uri): Promise; autoSetInterpreter(): Promise; getActiveInterpreter(resource?: Uri): Promise; + getInterpreterDetails(pythonPath: string): Promise>; refresh(): Promise; initialize(): void; } @@ -98,4 +96,10 @@ export interface IShebangCodeLensProvider extends CodeLensProvider { export const IInterpreterHelper = Symbol('IInterpreterHelper'); export interface IInterpreterHelper { getActiveWorkspaceUri(): WorkspacePythonPath | undefined; + getInterpreterInformation(pythonPath: string): Promise>; +} + +export const IPipEnvService = Symbol('IPipEnvService'); +export interface IPipEnvService { + isRelatedPipEnvironment(dir: string, pythonPath: string): Promise; } diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index 1c19a75f2e70..14291ea76468 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -6,7 +6,7 @@ import { IApplicationShell, IWorkspaceService } from '../../common/application/t import { IFileSystem } from '../../common/platform/types'; import { IConfigurationService, IDisposableRegistry } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; -import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService, IInterpreterVersionService } from '../contracts'; +import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService, PythonInterpreter } from '../contracts'; import { IVirtualEnvironmentManager } from '../virtualEnvs/types'; // tslint:disable-next-line:completed-docs @@ -15,7 +15,6 @@ export class InterpreterDisplay implements IInterpreterDisplay { private readonly statusBar: StatusBarItem; private readonly interpreterService: IInterpreterService; private readonly virtualEnvMgr: IVirtualEnvironmentManager; - private readonly versionProvider: IInterpreterVersionService; private readonly fileSystem: IFileSystem; private readonly configurationService: IConfigurationService; private readonly helper: IInterpreterHelper; @@ -24,7 +23,6 @@ export class InterpreterDisplay implements IInterpreterDisplay { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { this.interpreterService = serviceContainer.get(IInterpreterService); this.virtualEnvMgr = serviceContainer.get(IVirtualEnvironmentManager); - this.versionProvider = serviceContainer.get(IInterpreterVersionService); this.fileSystem = serviceContainer.get(IFileSystem); this.configurationService = serviceContainer.get(IConfigurationService); this.helper = serviceContainer.get(IInterpreterHelper); @@ -63,17 +61,17 @@ export class InterpreterDisplay implements IInterpreterDisplay { this.statusBar.tooltip += toolTipSuffix; } } else { - const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; await Promise.all([ - this.fileSystem.fileExistsAsync(pythonPath), - this.versionProvider.getVersion(pythonPath, defaultDisplayName), - this.getVirtualEnvironmentName(pythonPath).catch(() => '') + this.fileSystem.fileExists(pythonPath), + this.helper.getInterpreterInformation(pythonPath).catch>(() => undefined), + this.getVirtualEnvironmentName(pythonPath).catch(() => '') ]) - .then(([interpreterExists, displayName, virtualEnvName]) => { + .then(([interpreterExists, details, virtualEnvName]) => { + const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; const dislayNameSuffix = virtualEnvName.length > 0 ? ` (${virtualEnvName})` : ''; - this.statusBar.text = `${displayName}${dislayNameSuffix}`; + this.statusBar.text = `${details ? details.version : defaultDisplayName}${dislayNameSuffix}`; - if (!interpreterExists && displayName === defaultDisplayName && interpreters.length > 0) { + if (!interpreterExists && !details && interpreters.length > 0) { this.statusBar.color = 'yellow'; this.statusBar.text = '$(alert) Select Python Environment'; } diff --git a/src/client/interpreter/display/shebangCodeLensProvider.ts b/src/client/interpreter/display/shebangCodeLensProvider.ts index 8bb2595e752b..457e914200a8 100644 --- a/src/client/interpreter/display/shebangCodeLensProvider.ts +++ b/src/client/interpreter/display/shebangCodeLensProvider.ts @@ -1,8 +1,7 @@ import { inject, injectable } from 'inversify'; -import * as vscode from 'vscode'; -import { CancellationToken, CodeLens, TextDocument } from 'vscode'; +import { CancellationToken, CodeLens, Command, Event, Position, Range, TextDocument, Uri, workspace } from 'vscode'; import * as settings from '../../common/configSettings'; -import { IProcessService } from '../../common/process/types'; +import { IProcessServiceFactory } from '../../common/process/types'; import { IS_WINDOWS } from '../../common/utils'; import { IServiceContainer } from '../../ioc/types'; import { IShebangCodeLensProvider } from '../contracts'; @@ -10,10 +9,10 @@ import { IShebangCodeLensProvider } from '../contracts'; @injectable() export class ShebangCodeLensProvider implements IShebangCodeLensProvider { // tslint:disable-next-line:no-any - public onDidChangeCodeLenses: vscode.Event = vscode.workspace.onDidChangeConfiguration as any as vscode.Event; - private readonly processService: IProcessService; + public onDidChangeCodeLenses: Event = workspace.onDidChangeConfiguration as any as Event; + private readonly processServiceFactory: IProcessServiceFactory; constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - this.processService = serviceContainer.get(IProcessService); + this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); } public async detectShebang(document: TextDocument): Promise { const firstLine = document.lineAt(0); @@ -26,14 +25,14 @@ export class ShebangCodeLensProvider implements IShebangCodeLensProvider { } const shebang = firstLine.text.substr(2).trim(); - const pythonPath = await this.getFullyQualifiedPathToInterpreter(shebang); + const pythonPath = await this.getFullyQualifiedPathToInterpreter(shebang, document.uri); return typeof pythonPath === 'string' && pythonPath.length > 0 ? pythonPath : undefined; } public async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { const codeLenses = await this.createShebangCodeLens(document); return Promise.resolve(codeLenses); } - private async getFullyQualifiedPathToInterpreter(pythonPath: string) { + private async getFullyQualifiedPathToInterpreter(pythonPath: string, resource: Uri) { let cmdFile = pythonPath; let args = ['-c', 'import sys;print(sys.executable)']; if (pythonPath.indexOf('bin/env ') >= 0 && !IS_WINDOWS) { @@ -42,24 +41,25 @@ export class ShebangCodeLensProvider implements IShebangCodeLensProvider { cmdFile = parts.shift()!; args = parts.concat(args); } - return this.processService.exec(cmdFile, args) + const processService = await this.processServiceFactory.create(resource); + return processService.exec(cmdFile, args) .then(output => output.stdout.trim()) .catch(() => ''); } private async createShebangCodeLens(document: TextDocument) { const shebang = await this.detectShebang(document); const pythonPath = settings.PythonSettings.getInstance(document.uri).pythonPath; - const resolvedPythonPath = await this.getFullyQualifiedPathToInterpreter(pythonPath); + const resolvedPythonPath = await this.getFullyQualifiedPathToInterpreter(pythonPath, document.uri); if (!shebang || shebang === resolvedPythonPath) { 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 startOfShebang = new Position(0, 0); + const endOfShebang = new Position(0, firstLine.text.length - 1); + const shebangRange = new Range(startOfShebang, endOfShebang); - const cmd: vscode.Command = { + const cmd: Command = { command: 'python.setShebangInterpreter', title: 'Set as interpreter' }; diff --git a/src/client/interpreter/helpers.ts b/src/client/interpreter/helpers.ts index ba272607a147..00535634a590 100644 --- a/src/client/interpreter/helpers.ts +++ b/src/client/interpreter/helpers.ts @@ -1,8 +1,14 @@ import { inject, injectable } from 'inversify'; import { ConfigurationTarget } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { IFileSystem } from '../common/platform/types'; +import { IPythonExecutionFactory } from '../common/process/types'; +import { IPersistentStateFactory } from '../common/types'; import { IServiceContainer } from '../ioc/types'; -import { IInterpreterHelper, WorkspacePythonPath } from './contracts'; +import { IInterpreterHelper, PythonInterpreter, WorkspacePythonPath } from './contracts'; + +const EXPITY_DURATION = 24 * 60 * 60 * 1000; +type CachedPythonInterpreter = Partial & { fileHash: string }; export function getFirstNonEmptyLineFromMultilineString(stdout: string) { if (!stdout) { @@ -14,13 +20,17 @@ export function getFirstNonEmptyLineFromMultilineString(stdout: string) { @injectable() export class InterpreterHelper implements IInterpreterHelper { + private readonly fs: IFileSystem; + private readonly persistentFactory: IPersistentStateFactory; constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.persistentFactory = this.serviceContainer.get(IPersistentStateFactory); + this.fs = this.serviceContainer.get(IFileSystem); } public getActiveWorkspaceUri(): WorkspacePythonPath | undefined { const workspaceService = this.serviceContainer.get(IWorkspaceService); const documentManager = this.serviceContainer.get(IDocumentManager); - if (!Array.isArray(workspaceService.workspaceFolders) || workspaceService.workspaceFolders.length === 0) { + if (!workspaceService.hasWorkspaceFolders) { return; } if (workspaceService.workspaceFolders.length === 1) { @@ -33,4 +43,28 @@ export class InterpreterHelper implements IInterpreterHelper { } } } + public async getInterpreterInformation(pythonPath: string): Promise> { + const fileHash = await this.fs.getFileHash(pythonPath).catch(() => ''); + const store = this.persistentFactory.createGlobalPersistentState(pythonPath, undefined, EXPITY_DURATION); + if (store.value && store.value.fileHash === fileHash) { + return store.value; + } + const processService = await this.serviceContainer.get(IPythonExecutionFactory).create({ pythonPath }); + + try { + const info = await processService.getInterpreterInformation().catch(() => undefined); + if (!info) { + return; + } + const details = { + ...(info), + fileHash + }; + await store.updateValue(details); + return details; + } catch (ex) { + console.error(`Failed to get interpreter information for '${pythonPath}'`, ex); + return {}; + } + } } diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index 84fa7f8cdb57..b0b69910241a 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -10,8 +10,8 @@ import { IServiceContainer } from '../ioc/types'; import { IPythonPathUpdaterServiceManager } from './configuration/types'; import { IInterpreterDisplay, IInterpreterHelper, IInterpreterLocatorService, - IInterpreterService, IInterpreterVersionService, INTERPRETER_LOCATOR_SERVICE, - InterpreterType, PIPENV_SERVICE, PythonInterpreter, WORKSPACE_VIRTUAL_ENV_SERVICE + IInterpreterService, INTERPRETER_LOCATOR_SERVICE, + PIPENV_SERVICE, PythonInterpreter, WORKSPACE_VIRTUAL_ENV_SERVICE } from './contracts'; import { IVirtualEnvironmentManager } from './virtualEnvs/types'; @@ -36,7 +36,7 @@ export class InterpreterService implements Disposable, IInterpreterService { public initialize() { const disposables = this.serviceContainer.get(IDisposableRegistry); const documentManager = this.serviceContainer.get(IDocumentManager); - disposables.push(documentManager.onDidChangeActiveTextEditor((e) => this.refresh(e.document.uri))); + disposables.push(documentManager.onDidChangeActiveTextEditor((e) => e ? this.refresh(e.document.uri) : undefined)); const configService = this.serviceContainer.get(IConfigurationService); (configService.getSettings() as PythonSettings).addListener('change', this.onConfigChanged); } @@ -93,29 +93,40 @@ export class InterpreterService implements Disposable, IInterpreterService { public async getActiveInterpreter(resource?: Uri): Promise { const pythonExecutionFactory = this.serviceContainer.get(IPythonExecutionFactory); - const pythonExecutionService = await pythonExecutionFactory.create(resource); + const pythonExecutionService = await pythonExecutionFactory.create({ resource }); const fullyQualifiedPath = await pythonExecutionService.getExecutablePath().catch(() => undefined); // Python path is invalid or python isn't installed. if (!fullyQualifiedPath) { return; } + + return this.getInterpreterDetails(fullyQualifiedPath, resource); + } + public async getInterpreterDetails(pythonPath: string, resource?: Uri): Promise { const interpreters = await this.getInterpreters(resource); - const interpreter = interpreters.find(i => utils.arePathsSame(i.path, fullyQualifiedPath)); + const interpreter = interpreters.find(i => utils.arePathsSame(i.path, pythonPath)); if (interpreter) { return interpreter; } - const pythonExecutableName = path.basename(fullyQualifiedPath); - const versionInfo = await this.serviceContainer.get(IInterpreterVersionService).getVersion(fullyQualifiedPath, pythonExecutableName); + const interpreterHelper = this.serviceContainer.get(IInterpreterHelper); const virtualEnvManager = this.serviceContainer.get(IVirtualEnvironmentManager); - const virtualEnvName = await virtualEnvManager.getEnvironmentName(fullyQualifiedPath); + const [details, virtualEnvName, type] = await Promise.all([ + interpreterHelper.getInterpreterInformation(pythonPath), + virtualEnvManager.getEnvironmentName(pythonPath), + virtualEnvManager.getEnvironmentType(pythonPath) + ]); + if (details) { + return; + } const dislayNameSuffix = virtualEnvName.length > 0 ? ` (${virtualEnvName})` : ''; - const displayName = `${versionInfo}${dislayNameSuffix}`; + const displayName = `${details.version!}${dislayNameSuffix}`; return { + ...(details as PythonInterpreter), displayName, - path: fullyQualifiedPath, - type: virtualEnvName.length > 0 ? InterpreterType.VirtualEnv : InterpreterType.Unknown, - version: versionInfo + path: pythonPath, + envName: virtualEnvName, + type: type }; } private async shouldAutoSetInterpreter(): Promise { diff --git a/src/client/interpreter/interpreterVersion.ts b/src/client/interpreter/interpreterVersion.ts index 5235d60f52ce..2bfb72c4f98f 100644 --- a/src/client/interpreter/interpreterVersion.ts +++ b/src/client/interpreter/interpreterVersion.ts @@ -1,21 +1,23 @@ import { inject, injectable } from 'inversify'; import '../common/extensions'; -import { IProcessService } from '../common/process/types'; +import { IProcessServiceFactory } from '../common/process/types'; import { IInterpreterVersionService } from './contracts'; -export const PIP_VERSION_REGEX = '\\d+\\.\\d+(\\.\\d+)'; +export const PIP_VERSION_REGEX = '\\d+\\.\\d+(\\.\\d+)?'; @injectable() export class InterpreterVersionService implements IInterpreterVersionService { - constructor(@inject(IProcessService) private processService: IProcessService) { } + constructor(@inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory) { } public async getVersion(pythonPath: string, defaultValue: string): Promise { - return this.processService.exec(pythonPath, ['--version'], { mergeStdOutErr: true }) + const processService = await this.processServiceFactory.create(); + return processService.exec(pythonPath, ['--version'], { mergeStdOutErr: true }) .then(output => output.stdout.splitLines()[0]) .then(version => version.length === 0 ? defaultValue : version) .catch(() => defaultValue); } public async getPipVersion(pythonPath: string): Promise { - const output = await this.processService.exec(pythonPath, ['-m', 'pip', '--version'], { mergeStdOutErr: true }); + const processService = await this.processServiceFactory.create(); + const output = await processService.exec(pythonPath, ['-m', 'pip', '--version'], { mergeStdOutErr: true }); if (output.stdout.length > 0) { // Here's a sample output: // pip 9.0.1 from /Users/donjayamanne/anaconda3/lib/python3.6/site-packages (python 3.6). diff --git a/src/client/interpreter/locators/helpers.ts b/src/client/interpreter/locators/helpers.ts index 1b5cf698fcbe..b5605e946384 100644 --- a/src/client/interpreter/locators/helpers.ts +++ b/src/client/interpreter/locators/helpers.ts @@ -18,7 +18,8 @@ export function fixInterpreterDisplayName(item: PythonInterpreter) { if (!item.displayName) { const arch = getArchitectureDislayName(item.architecture); const version = typeof item.version === 'string' ? item.version : ''; - item.displayName = ['Python', version, arch].filter(namePart => namePart.length > 0).join(' ').trim(); + const prefix = version.toUpperCase().startsWith('PYTHON') ? '' : 'Python'; + item.displayName = [prefix, version, arch].filter(namePart => namePart.length > 0).join(' ').trim(); } return item; } diff --git a/src/client/interpreter/locators/index.ts b/src/client/interpreter/locators/index.ts index f40667b70b44..45fa5fb18999 100644 --- a/src/client/interpreter/locators/index.ts +++ b/src/client/interpreter/locators/index.ts @@ -31,12 +31,6 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi this.platform = serviceContainer.get(IPlatformService); } public async getInterpreters(resource?: Uri): Promise { - // Pipenv always wins - const pipenv = this.serviceContainer.get(IInterpreterLocatorService, PIPENV_SERVICE); - const interpreters = await pipenv.getInterpreters(resource); - if (interpreters.length > 0) { - return interpreters; - } return this.getInterpretersPerResource(resource); } public dispose() { @@ -49,6 +43,8 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi // tslint:disable-next-line:underscore-consistent-invocation return _.flatten(listOfInterpreters) + .filter(item => !!item) + .map(item => item!) .map(fixInterpreterDisplayName) .map(item => { item.path = path.normalize(item.path); return item; }) .reduce((accumulator, current) => { @@ -70,6 +66,9 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi private getLocators(): IInterpreterLocatorService[] { const locators: IInterpreterLocatorService[] = []; // The order of the services is important. + // The order is important because the data sources at the bottom of the list do not contain all, + // the information about the interpreters (e.g. type, environment name, etc). + // This way, the items returned from the top of the list will win, when we combine the items returned. if (this.platform.isWindows) { locators.push(this.serviceContainer.get(IInterpreterLocatorService, WINDOWS_REGISTRY_SERVICE)); } @@ -82,6 +81,7 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi locators.push(this.serviceContainer.get(IInterpreterLocatorService, KNOWN_PATH_SERVICE)); } locators.push(this.serviceContainer.get(IInterpreterLocatorService, CURRENT_PATH_SERVICE)); + locators.push(this.serviceContainer.get(IInterpreterLocatorService, PIPENV_SERVICE)); return locators; } diff --git a/src/client/interpreter/locators/services/KnownPathsService.ts b/src/client/interpreter/locators/services/KnownPathsService.ts index 1fb6b99f57fa..348bcf3d96a9 100644 --- a/src/client/interpreter/locators/services/KnownPathsService.ts +++ b/src/client/interpreter/locators/services/KnownPathsService.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { fsExistsAsync, IS_WINDOWS } from '../../../common/utils'; import { IServiceContainer } from '../../../ioc/types'; -import { IInterpreterVersionService, IKnownSearchPathsForInterpreters, InterpreterType, PythonInterpreter } from '../../contracts'; +import { IInterpreterHelper, IKnownSearchPathsForInterpreters, InterpreterType, PythonInterpreter } from '../../contracts'; import { lookForInterpretersInDirectory } from '../helpers'; import { CacheableLocatorService } from './cacheableLocatorService'; @@ -13,8 +13,8 @@ const untildify = require('untildify'); @injectable() export class KnownPathsService extends CacheableLocatorService { - public constructor( @inject(IKnownSearchPathsForInterpreters) private knownSearchPaths: string[], - @inject(IInterpreterVersionService) private versionProvider: IInterpreterVersionService, + public constructor(@inject(IKnownSearchPathsForInterpreters) private knownSearchPaths: string[], + @inject(IInterpreterHelper) private helper: IInterpreterHelper, @inject(IServiceContainer) serviceContainer: IServiceContainer) { super('KnownPathsService', serviceContainer); } @@ -29,17 +29,19 @@ export class KnownPathsService extends CacheableLocatorService { // 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)))); + .then(interpreters => Promise.all(interpreters.map(interpreter => this.getInterpreterDetails(interpreter)))) + .then(interpreters => interpreters.filter(interpreter => !!interpreter).map(interpreter => interpreter!)); } - private getInterpreterDetails(interpreter: string) { - return this.versionProvider.getVersion(interpreter, path.basename(interpreter)) - .then(displayName => { - return { - displayName, - path: interpreter, - type: InterpreterType.Unknown - }; - }); + private async getInterpreterDetails(interpreter: string) { + const details = await this.helper.getInterpreterInformation(interpreter); + if (!details) { + return; + } + return { + ...(details as PythonInterpreter), + path: interpreter, + type: InterpreterType.Unknown + }; } private getInterpretersInDirectory(dir: string) { return fsExistsAsync(dir) diff --git a/src/client/interpreter/locators/services/baseVirtualEnvService.ts b/src/client/interpreter/locators/services/baseVirtualEnvService.ts index 06b57b939436..8c48d470d9bc 100644 --- a/src/client/interpreter/locators/services/baseVirtualEnvService.ts +++ b/src/client/interpreter/locators/services/baseVirtualEnvService.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { IFileSystem, IPlatformService } from '../../../common/platform/types'; import { IServiceContainer } from '../../../ioc/types'; -import { IInterpreterVersionService, InterpreterType, IVirtualEnvironmentsSearchPathProvider, PythonInterpreter } from '../../contracts'; +import { IInterpreterHelper, IVirtualEnvironmentsSearchPathProvider, PythonInterpreter } from '../../contracts'; import { IVirtualEnvironmentManager } from '../../virtualEnvs/types'; import { lookForInterpretersInDirectory } from '../helpers'; import { CacheableLocatorService } from './cacheableLocatorService'; @@ -12,7 +12,7 @@ import { CacheableLocatorService } from './cacheableLocatorService'; @injectable() export class BaseVirtualEnvService extends CacheableLocatorService { private readonly virtualEnvMgr: IVirtualEnvironmentManager; - private readonly versionProvider: IInterpreterVersionService; + private readonly helper: IInterpreterHelper; private readonly fileSystem: IFileSystem; public constructor(@unmanaged() private searchPathsProvider: IVirtualEnvironmentsSearchPathProvider, @unmanaged() serviceContainer: IServiceContainer, @@ -20,7 +20,7 @@ export class BaseVirtualEnvService extends CacheableLocatorService { @unmanaged() cachePerWorkspace: boolean = false) { super(name, serviceContainer, cachePerWorkspace); this.virtualEnvMgr = serviceContainer.get(IVirtualEnvironmentManager); - this.versionProvider = serviceContainer.get(IInterpreterVersionService); + this.helper = serviceContainer.get(IInterpreterHelper); this.fileSystem = serviceContainer.get(IFileSystem); } // tslint:disable-next-line:no-empty @@ -30,16 +30,17 @@ export class BaseVirtualEnvService extends CacheableLocatorService { } private async suggestionsFromKnownVenvs(resource?: Uri) { const searchPaths = this.searchPathsProvider.getSearchPaths(resource); - return Promise.all(searchPaths.map(dir => this.lookForInterpretersInVenvs(dir))) + return Promise.all(searchPaths.map(dir => this.lookForInterpretersInVenvs(dir, resource))) .then(listOfInterpreters => _.flatten(listOfInterpreters)); } - private async lookForInterpretersInVenvs(pathToCheck: string) { - return this.fileSystem.getSubDirectoriesAsync(pathToCheck) + private async lookForInterpretersInVenvs(pathToCheck: string, resource?: Uri) { + return this.fileSystem.getSubDirectories(pathToCheck) .then(subDirs => Promise.all(this.getProspectiveDirectoriesForLookup(subDirs))) .then(dirs => dirs.filter(dir => dir.length > 0)) .then(dirs => Promise.all(dirs.map(lookForInterpretersInDirectory))) .then(pathsWithInterpreters => _.flatten(pathsWithInterpreters)) .then(interpreters => Promise.all(interpreters.map(interpreter => this.getVirtualEnvDetails(interpreter)))) + .then(interpreters => interpreters.filter(interpreter => !!interpreter).map(interpreter => interpreter!)) .catch((err) => { console.error('Python Extension (lookForInterpretersInVenvs):', err); // Ignore exceptions. @@ -50,7 +51,7 @@ export class BaseVirtualEnvService extends CacheableLocatorService { const platform = this.serviceContainer.get(IPlatformService); const dirToLookFor = platform.virtualEnvBinName; return subDirs.map(subDir => - this.fileSystem.getSubDirectoriesAsync(subDir) + this.fileSystem.getSubDirectories(subDir) .then(dirs => { const scriptOrBinDirs = dirs.filter(dir => { const folderName = path.basename(dir); @@ -64,17 +65,22 @@ export class BaseVirtualEnvService extends CacheableLocatorService { return ''; })); } - private async getVirtualEnvDetails(interpreter: string): Promise { + private async getVirtualEnvDetails(interpreter: string): Promise { return Promise.all([ - this.versionProvider.getVersion(interpreter, path.basename(interpreter)), - this.virtualEnvMgr.getEnvironmentName(interpreter) + this.helper.getInterpreterInformation(interpreter), + this.virtualEnvMgr.getEnvironmentName(interpreter), + this.virtualEnvMgr.getEnvironmentType(interpreter) ]) - .then(([displayName, virtualEnvName]) => { + .then(([details, virtualEnvName, type]) => { + if (!details) { + return; + } const virtualEnvSuffix = virtualEnvName.length ? virtualEnvName : this.getVirtualEnvironmentRootDirectory(interpreter); return { - displayName: `${displayName} (${virtualEnvSuffix})`.trim(), - path: interpreter, - type: virtualEnvName.length > 0 ? InterpreterType.VirtualEnv : InterpreterType.Unknown + ...(details as PythonInterpreter), + displayName: `${details.version!} (${virtualEnvSuffix})`.trim(), + envName: virtualEnvName, + type: type }; }); } diff --git a/src/client/interpreter/locators/services/condaEnvFileService.ts b/src/client/interpreter/locators/services/condaEnvFileService.ts index 49cbc2eec935..7984c2fb5495 100644 --- a/src/client/interpreter/locators/services/condaEnvFileService.ts +++ b/src/client/interpreter/locators/services/condaEnvFileService.ts @@ -1,12 +1,11 @@ import { inject, injectable } from 'inversify'; -import * as path from 'path'; import { Uri } from 'vscode'; import { IFileSystem } from '../../../common/platform/types'; import { ILogger } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { ICondaService, - IInterpreterVersionService, + IInterpreterHelper, InterpreterType, PythonInterpreter } from '../../contracts'; @@ -15,7 +14,7 @@ import { AnacondaCompanyName, AnacondaCompanyNames, AnacondaDisplayName } from ' @injectable() export class CondaEnvFileService extends CacheableLocatorService { - constructor( @inject(IInterpreterVersionService) private versionService: IInterpreterVersionService, + constructor(@inject(IInterpreterHelper) private helperService: IInterpreterHelper, @inject(ICondaService) private condaService: ICondaService, @inject(IFileSystem) private fileSystem: IFileSystem, @inject(IServiceContainer) serviceContainer: IServiceContainer, @@ -31,7 +30,7 @@ export class CondaEnvFileService extends CacheableLocatorService { if (!this.condaService.condaEnvironmentsFile) { return []; } - return this.fileSystem.fileExistsAsync(this.condaService.condaEnvironmentsFile!) + return this.fileSystem.fileExists(this.condaService.condaEnvironmentsFile!) .then(exists => exists ? this.getEnvironmentsFromFile(this.condaService.condaEnvironmentsFile!) : Promise.resolve([])); } private async getEnvironmentsFromFile(envFile: string) { @@ -66,17 +65,20 @@ export class CondaEnvFileService extends CacheableLocatorService { } private async getInterpreterDetails(environmentPath: string): Promise { const interpreter = this.condaService.getInterpreterPath(environmentPath); - if (!interpreter || !await this.fileSystem.fileExistsAsync(interpreter)) { + if (!interpreter || !await this.fileSystem.fileExists(interpreter)) { return; } - const version = await this.versionService.getVersion(interpreter, path.basename(interpreter)); - const versionWithoutCompanyName = this.stripCompanyName(version); + const details = await this.helperService.getInterpreterInformation(interpreter); + if (!details) { + return; + } + const versionWithoutCompanyName = this.stripCompanyName(details.version!); return { displayName: `${AnacondaDisplayName} ${versionWithoutCompanyName}`, + ...(details as PythonInterpreter), path: interpreter, companyDisplayName: AnacondaCompanyName, - version: version, type: InterpreterType.Conda, envPath: environmentPath }; diff --git a/src/client/interpreter/locators/services/condaEnvService.ts b/src/client/interpreter/locators/services/condaEnvService.ts index 7e2d5c616fcc..dfdde13f1001 100644 --- a/src/client/interpreter/locators/services/condaEnvService.ts +++ b/src/client/interpreter/locators/services/condaEnvService.ts @@ -6,7 +6,7 @@ import { Uri } from 'vscode'; import { IFileSystem } from '../../../common/platform/types'; import { ILogger } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; -import { CondaInfo, ICondaService, IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../contracts'; +import { CondaInfo, ICondaService, IInterpreterHelper, InterpreterType, PythonInterpreter } from '../../contracts'; import { CacheableLocatorService } from './cacheableLocatorService'; import { AnacondaCompanyName, AnacondaCompanyNames } from './conda'; import { CondaHelper } from './condaHelper'; @@ -14,8 +14,8 @@ import { CondaHelper } from './condaHelper'; @injectable() export class CondaEnvService extends CacheableLocatorService { private readonly condaHelper = new CondaHelper(); - constructor( @inject(ICondaService) private condaService: ICondaService, - @inject(IInterpreterVersionService) private versionService: IInterpreterVersionService, + constructor(@inject(ICondaService) private condaService: ICondaService, + @inject(IInterpreterHelper) private helper: IInterpreterHelper, @inject(ILogger) private logger: ILogger, @inject(IServiceContainer) serviceContainer: IServiceContainer, @inject(IFileSystem) private fileSystem: IFileSystem) { @@ -37,25 +37,24 @@ export class CondaEnvService extends CacheableLocatorService { .map(async envPath => { const pythonPath = this.condaService.getInterpreterPath(envPath); - const existsPromise = pythonPath ? this.fileSystem.fileExistsAsync(pythonPath) : Promise.resolve(false); - const versionPromise = this.versionService.getVersion(pythonPath, ''); - - const [exists, version] = await Promise.all([existsPromise, versionPromise]); - if (!exists) { + if (!(await this.fileSystem.fileExists(pythonPath))) { + return; + } + const details = await this.helper.getInterpreterInformation(pythonPath); + if (!details) { return; } - const versionWithoutCompanyName = this.stripCondaDisplayName(this.stripCompanyName(version), condaDisplayName); + const versionWithoutCompanyName = this.stripCondaDisplayName(this.stripCompanyName(details.version!), condaDisplayName); const displayName = `${condaDisplayName} ${versionWithoutCompanyName}`.trim(); - // tslint:disable-next-line:no-unnecessary-local-variable - const interpreter: PythonInterpreter = { + return { + ...(details as PythonInterpreter), path: pythonPath, displayName, companyDisplayName: AnacondaCompanyName, type: InterpreterType.Conda, envPath }; - return interpreter; }); return Promise.all(promises) diff --git a/src/client/interpreter/locators/services/condaService.ts b/src/client/interpreter/locators/services/condaService.ts index f73cb277fd35..ee7a1f5b780b 100644 --- a/src/client/interpreter/locators/services/condaService.ts +++ b/src/client/interpreter/locators/services/condaService.ts @@ -1,7 +1,7 @@ import { inject, injectable, named, optional } from 'inversify'; import * as path from 'path'; import { IFileSystem, IPlatformService } from '../../../common/platform/types'; -import { IProcessService } from '../../../common/process/types'; +import { IProcessServiceFactory } from '../../../common/process/types'; import { ILogger, IPersistentStateFactory } from '../../../common/types'; import { VersionUtils } from '../../../common/versionUtils'; import { IServiceContainer } from '../../../ioc/types'; @@ -11,15 +11,15 @@ import { CondaHelper } from './condaHelper'; // tslint:disable-next-line:no-require-imports no-var-requires const untildify: (value: string) => string = require('untildify'); -export const KNOWN_CONDA_LOCATIONS = ['~/anaconda/bin/conda', '~/miniconda/bin/conda', - '~/anaconda2/bin/conda', '~/miniconda2/bin/conda', - '~/anaconda3/bin/conda', '~/miniconda3/bin/conda']; +// This glob pattern will match all of the following: +// ~/anaconda/bin/conda, ~/anaconda3/bin/conda, ~/miniconda/bin/conda, ~/miniconda3/bin/conda +export const CondaLocationsGlob = '~/*conda*/bin/conda'; @injectable() export class CondaService implements ICondaService { - private condaFile: Promise; + private condaFile!: Promise; private isAvailable: boolean | undefined; - private readonly processService: IProcessService; + private readonly processServiceFactory: IProcessServiceFactory; private readonly platform: IPlatformService; private readonly logger: ILogger; private readonly fileSystem: IFileSystem; @@ -30,7 +30,7 @@ export class CondaService implements ICondaService { } constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, @inject(IInterpreterLocatorService) @named(WINDOWS_REGISTRY_SERVICE) @optional() private registryLookupForConda?: IInterpreterLocatorService) { - this.processService = this.serviceContainer.get(IProcessService); + this.processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); this.platform = this.serviceContainer.get(IPlatformService); this.logger = this.serviceContainer.get(ILogger); this.fileSystem = this.serviceContainer.get(IFileSystem); @@ -54,20 +54,23 @@ export class CondaService implements ICondaService { .catch(() => this.isAvailable = false); } public async getCondaVersion(): Promise { + const processService = await this.processServiceFactory.create(); return this.getCondaFile() - .then(condaFile => this.processService.exec(condaFile, ['--version'], {})) + .then(condaFile => processService.exec(condaFile, ['--version'], {})) .then(result => result.stdout.trim()) .catch(() => undefined); } public async isCondaInCurrentPath() { - return this.processService.exec('conda', ['--version']) + const processService = await this.processServiceFactory.create(); + return processService.exec('conda', ['--version']) .then(output => output.stdout.length > 0) .catch(() => false); } public async getCondaInfo(): Promise { try { const condaFile = await this.getCondaFile(); - const condaInfo = await this.processService.exec(condaFile, ['info', '--json']).then(output => output.stdout); + const processService = await this.processServiceFactory.create(); + const condaInfo = await processService.exec(condaFile, ['info', '--json']).then(output => output.stdout); return JSON.parse(condaInfo) as CondaInfo; } catch (ex) { @@ -88,9 +91,9 @@ export class CondaService implements ICondaService { const dir = path.dirname(interpreterPath); const isWindows = this.serviceContainer.get(IPlatformService).isWindows; const condaMetaDirectory = isWindows ? path.join(dir, 'conda-meta') : path.join(dir, '..', 'conda-meta'); - return fs.directoryExistsAsync(condaMetaDirectory); + return fs.directoryExists(condaMetaDirectory); } - public async getCondaEnvironment(interpreterPath: string): Promise<{ name: string, path: string } | undefined> { + public async getCondaEnvironment(interpreterPath: string): Promise<{ name: string; path: string } | undefined> { const isCondaEnv = await this.isCondaEnvironment(interpreterPath); if (!isCondaEnv) { return; @@ -118,18 +121,19 @@ export class CondaService implements ICondaService { // If still not available, then the user created the env after starting vs code. // The only solution is to get the user to re-start vscode. } - public async getCondaEnvironments(ignoreCache: boolean): Promise<({ name: string, path: string }[]) | undefined> { + public async getCondaEnvironments(ignoreCache: boolean): Promise<({ name: string; path: string }[]) | undefined> { // Global cache. const persistentFactory = this.serviceContainer.get(IPersistentStateFactory); // tslint:disable-next-line:no-any - const globalPersistence = persistentFactory.createGlobalPersistentState<{ data: { name: string, path: string }[] | undefined }>('CONDA_ENVIRONMENTS', undefined as any); + const globalPersistence = persistentFactory.createGlobalPersistentState<{ data: { name: string; path: string }[] | undefined }>('CONDA_ENVIRONMENTS', undefined as any); if (!ignoreCache && globalPersistence.value) { return globalPersistence.value.data; } try { const condaFile = await this.getCondaFile(); - const envInfo = await this.processService.exec(condaFile, ['env', 'list']).then(output => output.stdout); + const processService = await this.processServiceFactory.create(); + const envInfo = await processService.exec(condaFile, ['env', 'list']).then(output => output.stdout); const environments = this.condaHelper.parseCondaEnvironmentNames(envInfo); await globalPersistence.updateValue({ data: environments }); return environments; @@ -138,7 +142,7 @@ export class CondaService implements ICondaService { // Failed because either: // 1. conda is not installed. // 2. `conda env list has changed signature. - this.logger.logError('Failed to get conda environment list from conda', ex); + this.logger.logInformation('Failed to get conda environment list from conda', ex); } } public getInterpreterPath(condaEnvironmentPath: string): string { @@ -171,15 +175,14 @@ export class CondaService implements ICondaService { return condaInterpreter ? path.join(path.dirname(condaInterpreter.path), 'conda.exe') : 'conda'; }) .then(async condaPath => { - return this.fileSystem.fileExistsAsync(condaPath).then(exists => exists ? condaPath : 'conda'); + return this.fileSystem.fileExists(condaPath).then(exists => exists ? condaPath : 'conda'); }); } return this.getCondaFileFromKnownLocations(); } private async getCondaFileFromKnownLocations(): Promise { - const condaFiles = await Promise.all(KNOWN_CONDA_LOCATIONS - .map(untildify) - .map(async (condaPath: string) => this.fileSystem.fileExistsAsync(condaPath).then(exists => exists ? condaPath : ''))); + const condaFiles = await this.fileSystem.search(untildify(CondaLocationsGlob)) + .catch(() => []); const validCondaFiles = condaFiles.filter(condaPath => condaPath.length > 0); return validCondaFiles.length === 0 ? 'conda' : validCondaFiles[0]; diff --git a/src/client/interpreter/locators/services/currentPathService.ts b/src/client/interpreter/locators/services/currentPathService.ts index 8b31e1968474..a4e81dbed34d 100644 --- a/src/client/interpreter/locators/services/currentPathService.ts +++ b/src/client/interpreter/locators/services/currentPathService.ts @@ -1,21 +1,23 @@ import { inject, injectable } from 'inversify'; import * as _ from 'lodash'; -import * as path from 'path'; import { Uri } from 'vscode'; -import { PythonSettings } from '../../../common/configSettings'; -import { IProcessService } from '../../../common/process/types'; +import { IFileSystem } from '../../../common/platform/types'; +import { IProcessServiceFactory } from '../../../common/process/types'; +import { IConfigurationService } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; -import { IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../contracts'; +import { IInterpreterHelper, InterpreterType, PythonInterpreter } from '../../contracts'; import { IVirtualEnvironmentManager } from '../../virtualEnvs/types'; import { CacheableLocatorService } from './cacheableLocatorService'; @injectable() export class CurrentPathService extends CacheableLocatorService { + private readonly fs: IFileSystem; public constructor(@inject(IVirtualEnvironmentManager) private virtualEnvMgr: IVirtualEnvironmentManager, - @inject(IInterpreterVersionService) private versionProvider: IInterpreterVersionService, - @inject(IProcessService) private processService: IProcessService, + @inject(IInterpreterHelper) private helper: IInterpreterHelper, + @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, @inject(IServiceContainer) serviceContainer: IServiceContainer) { super('CurrentPathService', serviceContainer); + this.fs = serviceContainer.get(IFileSystem); } // tslint:disable-next-line:no-empty public dispose() { } @@ -23,7 +25,8 @@ export class CurrentPathService extends CacheableLocatorService { return this.suggestionsFromKnownPaths(); } private async suggestionsFromKnownPaths(resource?: Uri) { - const currentPythonInterpreter = this.getInterpreter(PythonSettings.getInstance(resource).pythonPath, '').then(interpreter => [interpreter]); + const configSettings = this.serviceContainer.get(IConfigurationService).getSettings(resource); + const currentPythonInterpreter = this.getInterpreter(configSettings.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]); @@ -32,26 +35,42 @@ export class CurrentPathService extends CacheableLocatorService { .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)))); + .then(interpreters => Promise.all(interpreters.map(interpreter => this.getInterpreterDetails(interpreter, resource)))); } - private async getInterpreterDetails(interpreter: string): Promise { + private async getInterpreterDetails(interpreter: string, resource?: Uri): Promise { return Promise.all([ - this.versionProvider.getVersion(interpreter, path.basename(interpreter)), - this.virtualEnvMgr.getEnvironmentName(interpreter) + this.helper.getInterpreterInformation(interpreter), + this.virtualEnvMgr.getEnvironmentName(interpreter), + this.virtualEnvMgr.getEnvironmentType(interpreter, resource) ]). - then(([displayName, virtualEnvName]) => { - displayName += virtualEnvName.length > 0 ? ` (${virtualEnvName})` : ''; + then(([details, virtualEnvName, type]) => { + if (!details) { + return; + } + const displayName = `${details.version ? details.version : ''}${virtualEnvName.length > 0 ? ` (${virtualEnvName})` : ''}`; return { + ...(details as PythonInterpreter), displayName, + envName: virtualEnvName, path: interpreter, - type: virtualEnvName ? InterpreterType.VirtualEnv : InterpreterType.Unknown + type: type ? type : InterpreterType.Unknown }; }); } private async getInterpreter(pythonPath: string, defaultValue: string) { - return this.processService.exec(pythonPath, ['-c', 'import sys;print(sys.executable)'], {}) - .then(output => output.stdout.trim()) - .then(value => value.length === 0 ? defaultValue : value) - .catch(() => defaultValue); // Ignore exceptions in getting the executable. + try { + const processService = await this.processServiceFactory.create(); + return processService.exec(pythonPath, ['-c', 'import sys;print(sys.executable)'], {}) + .then(output => output.stdout.trim()) + .then(async value => { + if (value.length > 0 && await this.fs.fileExists(value)) { + return value; + } + return defaultValue; + }) + .catch(() => defaultValue); // Ignore exceptions in getting the executable. + } catch { + return defaultValue; // Ignore exceptions in getting the executable. + } } } diff --git a/src/client/interpreter/locators/services/pipEnvService.ts b/src/client/interpreter/locators/services/pipEnvService.ts index 738eba8f605b..708cc544a45a 100644 --- a/src/client/interpreter/locators/services/pipEnvService.ts +++ b/src/client/interpreter/locators/services/pipEnvService.ts @@ -5,31 +5,42 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; import { IApplicationShell, IWorkspaceService } from '../../../common/application/types'; -import { IFileSystem } from '../../../common/platform/types'; -import { IProcessService } from '../../../common/process/types'; -import { getPythonExecutable } from '../../../debugger/Common/Utils'; +import { IFileSystem, IPlatformService } from '../../../common/platform/types'; +import { IProcessServiceFactory } from '../../../common/process/types'; +import { ICurrentProcess, ILogger } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; -import { IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../contracts'; +import { IInterpreterHelper, InterpreterType, IPipEnvService, PythonInterpreter } from '../../contracts'; import { CacheableLocatorService } from './cacheableLocatorService'; const execName = 'pipenv'; +const pipEnvFileNameVariable = 'PIPENV_PIPFILE'; @injectable() -export class PipEnvService extends CacheableLocatorService { - private readonly versionService: IInterpreterVersionService; - private readonly process: IProcessService; +export class PipEnvService extends CacheableLocatorService implements IPipEnvService { + private readonly helper: IInterpreterHelper; + private readonly processServiceFactory: IProcessServiceFactory; private readonly workspace: IWorkspaceService; private readonly fs: IFileSystem; + private readonly logger: ILogger; constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super('PipEnvService', serviceContainer); - this.versionService = this.serviceContainer.get(IInterpreterVersionService); - this.process = this.serviceContainer.get(IProcessService); + this.helper = this.serviceContainer.get(IInterpreterHelper); + this.processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); this.workspace = this.serviceContainer.get(IWorkspaceService); this.fs = this.serviceContainer.get(IFileSystem); + this.logger = this.serviceContainer.get(ILogger); } // tslint:disable-next-line:no-empty public dispose() { } + public async isRelatedPipEnvironment(dir: string, pythonPath: string): Promise { + // In PipEnv, the name of the cwd is used as a prefix in the virtual env. + if (pythonPath.indexOf(`${path.sep}${path.basename(dir)}-`) === -1) { + return false; + } + const envName = await this.getInterpreterPathFromPipenv(dir, true); + return !!envName; + } protected getInterpretersImplementation(resource?: Uri): Promise { const pipenvCwd = this.getPipenvWorkingDirectory(resource); if (!pipenvCwd) { @@ -47,13 +58,15 @@ export class PipEnvService extends CacheableLocatorService { return; } - const pythonExecutablePath = getPythonExecutable(interpreterPath); - const ver = await this.versionService.getVersion(pythonExecutablePath, ''); + const details = await this.helper.getInterpreterInformation(interpreterPath); + if (!details) { + return; + } return { - path: pythonExecutablePath, - displayName: `${ver} (${execName})`, - type: InterpreterType.VirtualEnv, - version: ver + ...(details as PythonInterpreter), + displayName: `${details.version} (${execName})`, + path: interpreterPath, + type: InterpreterType.PipEnv }; } @@ -67,25 +80,65 @@ export class PipEnvService extends CacheableLocatorService { return wsFolder ? wsFolder.uri.fsPath : this.workspace.rootPath; } - private async getInterpreterPathFromPipenv(cwd: string): Promise { + private async getInterpreterPathFromPipenv(cwd: string, ignoreErrors = false): Promise { // Quick check before actually running pipenv - if (!await this.fs.fileExistsAsync(path.join(cwd, 'Pipfile'))) { + if (!await this.checkIfPipFileExists(cwd)) { return; } - const venvFolder = await this.invokePipenv('--venv', cwd); - return venvFolder && await this.fs.directoryExistsAsync(venvFolder) ? venvFolder : undefined; + try { + const pythonPath = await this.invokePipenv('--py', cwd); + // TODO: Why do we need to do this? + return pythonPath && await this.fs.fileExists(pythonPath) ? pythonPath : undefined; + // tslint:disable-next-line:no-empty + } catch (error) { + console.error(error); + if (ignoreErrors) { + return; + } + const errorMessage = error.message || error; + const appShell = this.serviceContainer.get(IApplicationShell); + appShell.showWarningMessage(`Workspace contains pipfile but attempt to run 'pipenv --py' failed with ${errorMessage}. Make sure pipenv is on the PATH.`); + } + } + private async checkIfPipFileExists(cwd: string): Promise { + const currentProcess = this.serviceContainer.get(ICurrentProcess); + const pipFileName = currentProcess.env[pipEnvFileNameVariable]; + if (typeof pipFileName === 'string' && await this.fs.fileExists(path.join(cwd, pipFileName))) { + return true; + } + if (await this.fs.fileExists(path.join(cwd, 'Pipfile'))) { + return true; + } + return false; } private async invokePipenv(arg: string, rootPath: string): Promise { try { - const result = await this.process.exec(execName, [arg], { cwd: rootPath }); - if (result && result.stdout) { - return result.stdout.trim(); + const processService = await this.processServiceFactory.create(Uri.file(rootPath)); + const result = await processService.exec(execName, [arg], { cwd: rootPath }); + if (result) { + const stdout = result.stdout ? result.stdout.trim() : ''; + const stderr = result.stderr ? result.stderr.trim() : ''; + if (stderr.length > 0 && stdout.length === 0) { + throw new Error(stderr); + } + return stdout; } // tslint:disable-next-line:no-empty } catch (error) { + const platformService = this.serviceContainer.get(IPlatformService); + const currentProc = this.serviceContainer.get(ICurrentProcess); + const enviromentVariableValues = { + LC_ALL: currentProc.env.LC_ALL, + LANG: currentProc.env.LANG + }; + enviromentVariableValues[platformService.pathVariableName] = currentProc.env[platformService.pathVariableName]; + + this.logger.logWarning('Error in invoking PipEnv', error); + this.logger.logWarning(`Relevant Environment Variables ${JSON.stringify(enviromentVariableValues, undefined, 4)}`); + const errorMessage = error.message || error; const appShell = this.serviceContainer.get(IApplicationShell); - appShell.showWarningMessage(`Workspace contains pipfile but attempt to run 'pipenv --venv' failed with ${error}. Make sure pipenv is on the PATH.`); + appShell.showWarningMessage(`Workspace contains pipfile but attempt to run 'pipenv --venv' failed with '${errorMessage}'. Make sure pipenv is on the PATH.`); } } } diff --git a/src/client/interpreter/locators/services/windowsRegistryService.ts b/src/client/interpreter/locators/services/windowsRegistryService.ts index dc724fc7e7c8..e4b48b4827a6 100644 --- a/src/client/interpreter/locators/services/windowsRegistryService.ts +++ b/src/client/interpreter/locators/services/windowsRegistryService.ts @@ -4,9 +4,9 @@ import * as _ from 'lodash'; import * as path from 'path'; import { Uri } from 'vscode'; import { Architecture, IRegistry, RegistryHive } from '../../../common/platform/types'; -import { Is64Bit } from '../../../common/types'; +import { IPathUtils, Is64Bit } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; -import { InterpreterType, PythonInterpreter } from '../../contracts'; +import { IInterpreterHelper, InterpreterType, PythonInterpreter } from '../../contracts'; import { CacheableLocatorService } from './cacheableLocatorService'; import { AnacondaCompanyName, AnacondaCompanyNames } from './conda'; @@ -20,17 +20,19 @@ const PythonCoreCompanyDisplayName = 'Python Software Foundation'; const PythonCoreComany = 'PYTHONCORE'; type CompanyInterpreter = { - companyKey: string, - hive: RegistryHive, - arch?: Architecture + companyKey: string; + hive: RegistryHive; + arch?: Architecture; }; @injectable() export class WindowsRegistryService extends CacheableLocatorService { - constructor( @inject(IRegistry) private registry: IRegistry, + private readonly pathUtils: IPathUtils; + constructor(@inject(IRegistry) private registry: IRegistry, @inject(Is64Bit) private is64Bit: boolean, @inject(IServiceContainer) serviceContainer: IServiceContainer) { super('WindowsRegistryService', serviceContainer); + this.pathUtils = serviceContainer.get(IPathUtils); } // tslint:disable-next-line:no-empty public dispose() { } @@ -72,7 +74,7 @@ export class WindowsRegistryService extends CacheableLocatorService { private async getCompanies(hive: RegistryHive, arch?: Architecture): Promise { return this.registry.getKeys('\\Software\\Python', hive, arch) .then(companyKeys => companyKeys - .filter(companyKey => CompaniesToIgnore.indexOf(path.basename(companyKey).toUpperCase()) === -1) + .filter(companyKey => CompaniesToIgnore.indexOf(this.pathUtils.basename(companyKey).toUpperCase()) === -1) .map(companyKey => { return { companyKey, hive, arch }; })); @@ -84,11 +86,11 @@ export class WindowsRegistryService extends CacheableLocatorService { private getInreterpreterDetailsForCompany(tagKey: string, companyKey: string, hive: RegistryHive, arch?: Architecture): Promise { const key = `${tagKey}\\InstallPath`; type InterpreterInformation = null | undefined | { - installPath: string, - executablePath?: string, - displayName?: string, - version?: string, - companyDisplayName?: string + installPath: string; + executablePath?: string; + displayName?: string; + version?: string; + companyDisplayName?: string; }; return this.registry.getValue(key, hive, arch) .then(installPath => { @@ -109,20 +111,26 @@ export class WindowsRegistryService extends CacheableLocatorService { ]) .then(([installedPath, executablePath, displayName, version, companyDisplayName]) => { companyDisplayName = AnacondaCompanyNames.indexOf(companyDisplayName) === -1 ? companyDisplayName : AnacondaCompanyName; - // tslint:disable-next-line:prefer-type-cast + // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion return { installPath: installedPath, executablePath, displayName, version, companyDisplayName } as InterpreterInformation; }); }) - .then((interpreterInfo?: InterpreterInformation) => { + .then(async (interpreterInfo?: InterpreterInformation) => { if (!interpreterInfo) { return; } 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 + const helper = this.serviceContainer.get(IInterpreterHelper); + const details = await helper.getInterpreterInformation(executablePath); + if (!details) { + return; + } + const version = interpreterInfo.version ? this.pathUtils.basename(interpreterInfo.version) : this.pathUtils.basename(tagKey); + // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion return { + ...(details as PythonInterpreter), architecture: arch, displayName, path: executablePath, @@ -149,7 +157,7 @@ export class WindowsRegistryService extends CacheableLocatorService { if (displayName && displayName.length > 0) { return displayName; } - const company = path.basename(companyKey); + const company = this.pathUtils.basename(companyKey); return company.toUpperCase() === PythonCoreComany ? PythonCoreCompanyDisplayName : company; } } diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 18b891608f26..f867bb2ce1ac 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -20,6 +20,7 @@ import { IInterpreterVersionService, IKnownSearchPathsForInterpreters, INTERPRETER_LOCATOR_SERVICE, + IPipEnvService, IShebangCodeLensProvider, IVirtualEnvironmentsSearchPathProvider, KNOWN_PATH_SERVICE, @@ -61,6 +62,7 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IInterpreterLocatorService, GlobalVirtualEnvService, GLOBAL_VIRTUAL_ENV_SERVICE); serviceManager.addSingleton(IInterpreterLocatorService, WorkspaceVirtualEnvService, WORKSPACE_VIRTUAL_ENV_SERVICE); serviceManager.addSingleton(IInterpreterLocatorService, PipEnvService, PIPENV_SERVICE); + serviceManager.addSingleton(IPipEnvService, PipEnvService); const isWindows = serviceManager.get(IsWindows); if (isWindows) { diff --git a/src/client/interpreter/virtualEnvs/index.ts b/src/client/interpreter/virtualEnvs/index.ts index 4f535cb99151..61f14d7f1c6f 100644 --- a/src/client/interpreter/virtualEnvs/index.ts +++ b/src/client/interpreter/virtualEnvs/index.ts @@ -2,25 +2,84 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { IProcessService } from '../../common/process/types'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../common/application/types'; +import { IFileSystem } from '../../common/platform/types'; +import { IProcessServiceFactory } from '../../common/process/types'; import { IServiceContainer } from '../../ioc/types'; +import { InterpreterType, IPipEnvService } from '../contracts'; import { IVirtualEnvironmentManager } from './types'; +const PYENVFILES = ['pyvenv.cfg', path.join('..', 'pyvenv.cfg')]; + @injectable() export class VirtualEnvironmentManager implements IVirtualEnvironmentManager { - private processService: IProcessService; + private processServiceFactory: IProcessServiceFactory; + private pipEnvService: IPipEnvService; + private fs: IFileSystem; + private pyEnvRoot?: string; + private workspaceService: IWorkspaceService; constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - this.processService = serviceContainer.get(IProcessService); + this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); + this.fs = serviceContainer.get(IFileSystem); + this.pipEnvService = serviceContainer.get(IPipEnvService); + this.workspaceService = serviceContainer.get(IWorkspaceService); } public async getEnvironmentName(pythonPath: string): Promise { // https://stackoverflow.com/questions/1871549/determine-if-python-is-running-inside-virtualenv // hasattr(sys, 'real_prefix') works for virtualenv while // '(hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix))' works for venv - const code = 'import sys\nif hasattr(sys, "real_prefix"):\n print("virtualenv")\nelif hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix:\n print("venv")'; - const output = await this.processService.exec(pythonPath, ['-c', code]); - if (output.stdout.length > 0) { - return output.stdout.trim(); + try { + const processService = await this.processServiceFactory.create(); + const code = 'import sys\nif hasattr(sys, "real_prefix"):\n print("virtualenv")\nelif hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix:\n print("venv")'; + const output = await processService.exec(pythonPath, ['-c', code]); + if (output.stdout.length > 0) { + return output.stdout.trim(); + } + } catch { + // do nothing. } return ''; } + public async getEnvironmentType(pythonPath: string, resource?: Uri): Promise { + const dir = path.dirname(pythonPath); + const pyEnvCfgFiles = PYENVFILES.map(file => path.join(dir, file)); + for (const file of pyEnvCfgFiles) { + if (await this.fs.fileExists(file)) { + return InterpreterType.Venv; + } + } + + const pyEnvRoot = await this.getPyEnvRoot(resource); + if (pyEnvRoot && pythonPath.startsWith(pyEnvRoot)) { + return InterpreterType.Pyenv; + } + + const defaultWorkspaceUri = this.workspaceService.hasWorkspaceFolders ? this.workspaceService.workspaceFolders![0].uri : undefined; + const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; + const workspaceUri = workspaceFolder ? workspaceFolder.uri : defaultWorkspaceUri; + if (workspaceUri && this.pipEnvService.isRelatedPipEnvironment(pythonPath, workspaceUri.fsPath)) { + return InterpreterType.PipEnv; + } + + if ((await this.getEnvironmentName(pythonPath)).length > 0) { + return InterpreterType.VirtualEnv; + } + + // Lets not try to determine whether this is a conda environment or not. + return InterpreterType.Unknown; + } + private async getPyEnvRoot(resource?: Uri): Promise { + if (this.pyEnvRoot) { + return this.pyEnvRoot; + } + try { + const processService = await this.processServiceFactory.create(resource); + const output = await processService.exec('pyenv', ['root']); + return this.pyEnvRoot = output.stdout.trim(); + } catch { + return; + } + } } diff --git a/src/client/interpreter/virtualEnvs/types.ts b/src/client/interpreter/virtualEnvs/types.ts index 971772fd009d..6096f88d374c 100644 --- a/src/client/interpreter/virtualEnvs/types.ts +++ b/src/client/interpreter/virtualEnvs/types.ts @@ -1,7 +1,11 @@ + // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { Uri } from 'vscode'; +import { InterpreterType } from '../contracts'; export const IVirtualEnvironmentManager = Symbol('VirtualEnvironmentManager'); export interface IVirtualEnvironmentManager { getEnvironmentName(pythonPath: string): Promise; + getEnvironmentType(pythonPath: string, resource?: Uri): Promise; } diff --git a/src/client/ioc/serviceManager.ts b/src/client/ioc/serviceManager.ts index 792358da06dd..bcd4331dfd13 100644 --- a/src/client/ioc/serviceManager.ts +++ b/src/client/ioc/serviceManager.ts @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Container, interfaces } from 'inversify'; +import { Container, injectable, interfaces } from 'inversify'; import { Abstract, IServiceManager, Newable } from './types'; type identifier = string | symbol | Newable | Abstract; +@injectable() export class ServiceManager implements IServiceManager { constructor(private container: Container) { } // tslint:disable-next-line:no-any diff --git a/src/client/language/characters.ts b/src/client/language/characters.ts index e0bc20ba4131..5a4da26a7b6d 100644 --- a/src/client/language/characters.ts +++ b/src/client/language/characters.ts @@ -83,18 +83,22 @@ export function isLineBreak(ch: number): boolean { return ch === Char.CarriageReturn || ch === Char.LineFeed; } +export function isNumber(ch: number): boolean { + return ch >= Char._0 && ch <= Char._9 || ch === Char.Underscore; +} + export function isDecimal(ch: number): boolean { - return ch >= Char._0 && ch <= Char._9; + return ch >= Char._0 && ch <= Char._9 || ch === Char.Underscore; } export function isHex(ch: number): boolean { - return isDecimal(ch) || (ch >= Char.a && ch <= Char.f) || (ch >= Char.A && ch <= Char.F); + return isDecimal(ch) || (ch >= Char.a && ch <= Char.f) || (ch >= Char.A && ch <= Char.F) || ch === Char.Underscore; } export function isOctal(ch: number): boolean { - return ch >= Char._0 && ch <= Char._7; + return ch >= Char._0 && ch <= Char._7 || ch === Char.Underscore; } export function isBinary(ch: number): boolean { - return ch === Char._0 || ch === Char._1; + return ch === Char._0 || ch === Char._1 || ch === Char.Underscore; } diff --git a/src/client/language/iterableTextRange.ts b/src/client/language/iterableTextRange.ts new file mode 100644 index 000000000000..6f92e1e769de --- /dev/null +++ b/src/client/language/iterableTextRange.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ITextRange, ITextRangeCollection } from './types'; + +export class IterableTextRange implements Iterable{ + constructor(private textRangeCollection: ITextRangeCollection) { + } + public [Symbol.iterator](): Iterator { + let index = -1; + + return { + next: (): IteratorResult => { + if (index < this.textRangeCollection.count - 1) { + return { + done: false, + value: this.textRangeCollection.getItemAt(index += 1) + }; + } else { + return { + done: true, + // tslint:disable-next-line:no-any + value: undefined as any + }; + } + } + }; + } +} diff --git a/src/client/language/textBuilder.ts b/src/client/language/textBuilder.ts index aebe6187696b..e11f2a1299c4 100644 --- a/src/client/language/textBuilder.ts +++ b/src/client/language/textBuilder.ts @@ -16,8 +16,14 @@ export class TextBuilder { return this.segments.join(''); } - public softAppendSpace(): void { - if (!this.isLastWhiteSpace() && this.segments.length > 0) { + public softAppendSpace(count: number = 1): void { + if (this.segments.length === 0) { + return; + } + if (this.isLastWhiteSpace()) { + count = count - 1; + } + for (let i = 0; i < count; i += 1) { this.segments.push(' '); } } diff --git a/src/client/language/tokenizer.ts b/src/client/language/tokenizer.ts index ecd382d96541..50269ab27638 100644 --- a/src/client/language/tokenizer.ts +++ b/src/client/language/tokenizer.ts @@ -4,7 +4,7 @@ // tslint:disable-next-line:import-name import Char from 'typescript-char'; -import { isBinary, isDecimal, isHex, isIdentifierChar, isIdentifierStartChar, isOctal } from './characters'; +import { isBinary, isDecimal, isHex, isIdentifierChar, isIdentifierStartChar, isOctal, isWhiteSpace } from './characters'; import { CharacterStream } from './characterStream'; import { TextRangeCollection } from './textRangeCollection'; import { ICharacterStream, ITextRangeCollection, IToken, ITokenizer, TextRange, TokenizerMode, TokenType } from './types'; @@ -27,21 +27,9 @@ class Token extends TextRange implements IToken { } export class Tokenizer implements ITokenizer { - // private keywords = [ - // 'and', 'assert', 'break', 'class', 'continue', 'def', 'del', - // 'elif', 'else', 'except', 'exec', 'False', 'finally', 'for', 'from', - // 'global', 'if', 'import', 'in', 'is', 'lambda', 'None', 'nonlocal', - // 'not', 'or', 'pass', 'print', 'raise', 'return', 'True', 'try', - // 'while', 'with', 'yield' - // ]; - private cs: ICharacterStream; + private cs: ICharacterStream = new CharacterStream(''); private tokens: IToken[] = []; - private floatRegex = /[-+]?(?:(?:\d*\.\d+)|(?:\d+\.?))(?:[Ee][+-]?\d+)?/; - private mode: TokenizerMode; - - constructor() { - //this.floatRegex.compile(); - } + private mode = TokenizerMode.Full; public tokenize(text: string): ITextRangeCollection; public tokenize(text: string, start: number, length: number, mode: TokenizerMode): ITextRangeCollection; @@ -85,11 +73,19 @@ export class Tokenizer implements ITokenizer { } } + // tslint:disable-next-line:cyclomatic-complexity private handleCharacter(): boolean { - const quoteType = this.getQuoteType(); - if (quoteType !== QuoteType.None) { - this.handleString(quoteType); - return true; + // f-strings, b-strings, etc + const stringPrefixLength = this.getStringPrefixLength(); + if (stringPrefixLength >= 0) { + // Indeed a string + this.cs.advance(stringPrefixLength); + + const quoteType = this.getQuoteType(); + if (quoteType !== QuoteType.None) { + this.handleString(quoteType, stringPrefixLength); + return true; + } } if (this.cs.currentChar === Char.Hash) { this.handleComment(); @@ -127,16 +123,16 @@ export class Tokenizer implements ITokenizer { case Char.Colon: this.tokens.push(new Token(TokenType.Colon, this.cs.position, 1)); break; - case Char.At: - case Char.Period: - this.tokens.push(new Token(TokenType.Operator, this.cs.position, 1)); - break; default: if (this.isPossibleNumber()) { if (this.tryNumber()) { return true; } } + if (this.cs.currentChar === Char.Period) { + this.tokens.push(new Token(TokenType.Operator, this.cs.position, 1)); + break; + } if (!this.tryIdentifier()) { if (!this.tryOperator()) { this.handleUnknown(); @@ -164,29 +160,8 @@ export class Tokenizer implements ITokenizer { return false; } + // tslint:disable-next-line:cyclomatic-complexity private isPossibleNumber(): boolean { - if (this.cs.currentChar === Char.Hyphen || this.cs.currentChar === Char.Plus) { - // Next character must be decimal or a dot otherwise - // it is not a number. No whitespace is allowed. - if (isDecimal(this.cs.nextChar) || this.cs.nextChar === Char.Period) { - // Check what previous token is, if any - if (this.tokens.length === 0) { - // At the start of the file this can only be a number - return true; - } - - const prev = this.tokens[this.tokens.length - 1]; - if (prev.type === TokenType.OpenBrace - || prev.type === TokenType.OpenBracket - || prev.type === TokenType.Comma - || prev.type === TokenType.Semicolon - || prev.type === TokenType.Operator) { - return true; - } - } - return false; - } - if (isDecimal(this.cs.currentChar)) { return true; } @@ -195,57 +170,127 @@ export class Tokenizer implements ITokenizer { return true; } + const next = (this.cs.currentChar === Char.Hyphen || this.cs.currentChar === Char.Plus) ? 1 : 0; + // Next character must be decimal or a dot otherwise + // it is not a number. No whitespace is allowed. + if (isDecimal(this.cs.lookAhead(next)) || this.cs.lookAhead(next) === Char.Period) { + // Check what previous token is, if any + if (this.tokens.length === 0) { + // At the start of the file this can only be a number + return true; + } + + const prev = this.tokens[this.tokens.length - 1]; + if (prev.type === TokenType.OpenBrace + || prev.type === TokenType.OpenBracket + || prev.type === TokenType.Comma + || prev.type === TokenType.Colon + || prev.type === TokenType.Semicolon + || prev.type === TokenType.Operator) { + return true; + } + } + + if (this.cs.lookAhead(next) === Char._0) { + const nextNext = this.cs.lookAhead(next + 1); + if (nextNext === Char.x || nextNext === Char.X) { + return true; + } + if (nextNext === Char.b || nextNext === Char.B) { + return true; + } + if (nextNext === Char.o || nextNext === Char.O) { + return true; + } + } + return false; } // tslint:disable-next-line:cyclomatic-complexity private tryNumber(): boolean { const start = this.cs.position; + let leadingSign = 0; + + if (this.cs.currentChar === Char.Hyphen || this.cs.currentChar === Char.Plus) { + this.cs.moveNext(); // Skip leading +/- + leadingSign = 1; + } if (this.cs.currentChar === Char._0) { let radix = 0; - // Try hex - if (this.cs.nextChar === Char.x || this.cs.nextChar === Char.X) { + // Try hex => hexinteger: "0" ("x" | "X") (["_"] hexdigit)+ + if ((this.cs.nextChar === Char.x || this.cs.nextChar === Char.X) && isHex(this.cs.lookAhead(2))) { this.cs.advance(2); while (isHex(this.cs.currentChar)) { this.cs.moveNext(); } radix = 16; } - // Try binary - if (this.cs.nextChar === Char.b || this.cs.nextChar === Char.B) { + // Try binary => bininteger: "0" ("b" | "B") (["_"] bindigit)+ + if ((this.cs.nextChar === Char.b || this.cs.nextChar === Char.B) && isBinary(this.cs.lookAhead(2))) { this.cs.advance(2); while (isBinary(this.cs.currentChar)) { this.cs.moveNext(); } radix = 2; } - // Try octal - if (this.cs.nextChar === Char.o || this.cs.nextChar === Char.O) { + // Try octal => octinteger: "0" ("o" | "O") (["_"] octdigit)+ + if ((this.cs.nextChar === Char.o || this.cs.nextChar === Char.O) && isOctal(this.cs.lookAhead(2))) { this.cs.advance(2); while (isOctal(this.cs.currentChar)) { this.cs.moveNext(); } radix = 8; } - const text = this.cs.getText().substr(start, this.cs.position - start); - if (radix > 0 && parseInt(text.substr(2), radix)) { - this.tokens.push(new Token(TokenType.Number, start, text.length)); - return true; + if (radix > 0) { + const text = this.cs.getText().substr(start + leadingSign, this.cs.position - start - leadingSign); + if (!isNaN(parseInt(text, radix))) { + this.tokens.push(new Token(TokenType.Number, start, text.length + leadingSign)); + return true; + } + } + } + + let decimal = false; + // Try decimal int => + // decinteger: nonzerodigit (["_"] digit)* | "0" (["_"] "0")* + // nonzerodigit: "1"..."9" + // digit: "0"..."9" + if (this.cs.currentChar >= Char._1 && this.cs.currentChar <= Char._9) { + while (isDecimal(this.cs.currentChar)) { + this.cs.moveNext(); + } + decimal = this.cs.currentChar !== Char.Period && this.cs.currentChar !== Char.e && this.cs.currentChar !== Char.E; + } + + if (this.cs.currentChar === Char._0) { // "0" (["_"] "0")* + while (this.cs.currentChar === Char._0 || this.cs.currentChar === Char.Underscore) { + this.cs.moveNext(); } + decimal = this.cs.currentChar !== Char.Period && this.cs.currentChar !== Char.e && this.cs.currentChar !== Char.E; } - if (isDecimal(this.cs.currentChar) || - this.cs.currentChar === Char.Plus || this.cs.currentChar === Char.Hyphen || this.cs.currentChar === Char.Period) { - const candidate = this.cs.getText().substr(this.cs.position); - const re = this.floatRegex.exec(candidate); - if (re && re.length > 0 && re[0] && candidate.startsWith(re[0])) { - this.tokens.push(new Token(TokenType.Number, start, re[0].length)); - this.cs.position = start + re[0].length; + if (decimal) { + const text = this.cs.getText().substr(start + leadingSign, this.cs.position - start - leadingSign); + if (!isNaN(parseInt(text, 10))) { + this.tokens.push(new Token(TokenType.Number, start, text.length + leadingSign)); return true; } } + // Floating point. Sign was already skipped over. + if ((this.cs.currentChar >= Char._0 && this.cs.currentChar <= Char._9) || + (this.cs.currentChar === Char.Period && this.cs.nextChar >= Char._0 && this.cs.nextChar <= Char._9)) { + if (this.skipFloatingPointCandidate(false)) { + const text = this.cs.getText().substr(start, this.cs.position - start); + if (!isNaN(parseFloat(text))) { + this.tokens.push(new Token(TokenType.Number, start, this.cs.position - start)); + return true; + } + } + } + this.cs.position = start; return false; } @@ -256,15 +301,20 @@ export class Tokenizer implements ITokenizer { const nextChar = this.cs.nextChar; switch (this.cs.currentChar) { case Char.Plus: - case Char.Hyphen: case Char.Ampersand: case Char.Bar: case Char.Caret: case Char.Equal: case Char.ExclamationMark: + case Char.Percent: + case Char.Tilde: length = nextChar === Char.Equal ? 2 : 1; break; + case Char.Hyphen: + length = nextChar === Char.Equal || nextChar === Char.Greater ? 2 : 1; + break; + case Char.Asterisk: if (nextChar === Char.Asterisk) { length = this.cs.lookAhead(2) === Char.Equal ? 3 : 2; @@ -287,7 +337,7 @@ export class Tokenizer implements ITokenizer { } else if (nextChar === Char.Less) { length = this.cs.lookAhead(2) === Char.Equal ? 3 : 2; } else { - length = 1; + length = nextChar === Char.Equal ? 2 : 1; } break; @@ -295,12 +345,12 @@ export class Tokenizer implements ITokenizer { if (nextChar === Char.Greater) { length = this.cs.lookAhead(2) === Char.Equal ? 3 : 2; } else { - length = 1; + length = nextChar === Char.Equal ? 2 : 1; } break; case Char.At: - length = nextChar === Char.Equal ? 2 : 0; + length = nextChar === Char.Equal ? 2 : 1; break; default: @@ -328,6 +378,42 @@ export class Tokenizer implements ITokenizer { this.tokens.push(new Token(TokenType.Comment, start, this.cs.position - start)); } + // tslint:disable-next-line:cyclomatic-complexity + private getStringPrefixLength(): number { + if (this.cs.currentChar === Char.SingleQuote || this.cs.currentChar === Char.DoubleQuote) { + return 0; // Simple string, no prefix + } + + if (this.cs.nextChar === Char.SingleQuote || this.cs.nextChar === Char.DoubleQuote) { + switch (this.cs.currentChar) { + case Char.f: + case Char.F: + case Char.r: + case Char.R: + case Char.b: + case Char.B: + case Char.u: + case Char.U: + return 1; // single-char prefix like u"" or r"" + default: + break; + } + } + + if (this.cs.lookAhead(2) === Char.SingleQuote || this.cs.lookAhead(2) === Char.DoubleQuote) { + const prefix = this.cs.getText().substr(this.cs.position, 2).toLowerCase(); + switch (prefix) { + case 'rf': + case 'ur': + case 'br': + return 2; + default: + break; + } + } + return -1; + } + private getQuoteType(): QuoteType { if (this.cs.currentChar === Char.SingleQuote) { return this.cs.nextChar === Char.SingleQuote && this.cs.lookAhead(2) === Char.SingleQuote @@ -342,8 +428,8 @@ export class Tokenizer implements ITokenizer { return QuoteType.None; } - private handleString(quoteType: QuoteType): void { - const start = this.cs.position; + private handleString(quoteType: QuoteType, stringPrefixLength: number): void { + const start = this.cs.position - stringPrefixLength; if (quoteType === QuoteType.Single || quoteType === QuoteType.Double) { this.cs.moveNext(); this.skipToSingleEndQuote(quoteType === QuoteType.Single @@ -360,6 +446,9 @@ export class Tokenizer implements ITokenizer { private skipToSingleEndQuote(quote: number): void { while (!this.cs.isEndOfStream()) { + if (this.cs.currentChar === Char.LineFeed || this.cs.currentChar === Char.CarriageReturn) { + return; // Unterminated single-line string + } if (this.cs.currentChar === Char.Backslash && this.cs.nextChar === quote) { this.cs.advance(2); continue; @@ -378,4 +467,34 @@ export class Tokenizer implements ITokenizer { } this.cs.advance(3); } + + private skipFloatingPointCandidate(allowSign: boolean): boolean { + // Determine end of the potential floating point number + const start = this.cs.position; + this.skipFractionalNumber(allowSign); + if (this.cs.position > start) { + if (this.cs.currentChar === Char.e || this.cs.currentChar === Char.E) { + this.cs.moveNext(); // Optional exponent sign + } + this.skipDecimalNumber(true); // skip exponent value + } + return this.cs.position > start; + } + + private skipFractionalNumber(allowSign: boolean): void { + this.skipDecimalNumber(allowSign); + if (this.cs.currentChar === Char.Period) { + this.cs.moveNext(); // Optional period + } + this.skipDecimalNumber(false); + } + + private skipDecimalNumber(allowSign: boolean): void { + if (allowSign && (this.cs.currentChar === Char.Hyphen || this.cs.currentChar === Char.Plus)) { + this.cs.moveNext(); // Optional sign + } + while (isDecimal(this.cs.currentChar)) { + this.cs.moveNext(); // skip integer part + } + } } diff --git a/src/client/languageServices/languageServerSurveyBanner.ts b/src/client/languageServices/languageServerSurveyBanner.ts new file mode 100644 index 000000000000..7a375df9bb49 --- /dev/null +++ b/src/client/languageServices/languageServerSurveyBanner.ts @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IApplicationShell } from '../common/application/types'; +import '../common/extensions'; +import { IBrowserService, IPersistentStateFactory, + IPythonExtensionBanner } from '../common/types'; +import { getRandomBetween } from '../common/utils'; + +// persistent state names, exported to make use of in testing +export enum LSSurveyStateKeys { + ShowBanner = 'ShowLSSurveyBanner', + ShowAttemptCounter = 'LSSurveyShowAttempt', + ShowAfterCompletionCount = 'LSSurveyShowCount' +} + +enum LSSurveyLabelIndex { + Yes, + No +} + +/* +This class represents a popup that will ask our users for some feedback after +a specific event occurs N times. +*/ +@injectable() +export class LanguageServerSurveyBanner implements IPythonExtensionBanner { + private disabledInCurrentSession: boolean = false; + private minCompletionsBeforeShow: number; + private maxCompletionsBeforeShow: number; + private isInitialized: boolean = false; + private bannerMessage: string = 'Can you please take 2 minutes to tell us how the Experimental Debugger is working for you?'; + private bannerLabels: string [] = [ 'Yes, take survey now', 'No, thanks']; + + constructor( + @inject(IApplicationShell) private appShell: IApplicationShell, + @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, + @inject(IBrowserService) private browserService: IBrowserService, + showAfterMinimumEventsCount: number = 100, + showBeforeMaximumEventsCount: number = 500) + { + this.minCompletionsBeforeShow = showAfterMinimumEventsCount; + this.maxCompletionsBeforeShow = showBeforeMaximumEventsCount; + this.initialize(); + } + + public initialize(): void { + if (this.isInitialized) { + return; + } + this.isInitialized = true; + + if (this.minCompletionsBeforeShow >= this.maxCompletionsBeforeShow) { + this.disable().ignoreErrors(); + } + } + + public get optionLabels(): string[] { + return this.bannerLabels; + } + + public get shownCount(): Promise { + return this.getPythonLSLaunchCounter(); + } + + public get enabled(): boolean { + return this.persistentState.createGlobalPersistentState(LSSurveyStateKeys.ShowBanner, true).value; + } + + public async showBanner(): Promise { + if (!this.enabled || this.disabledInCurrentSession) { + return; + } + + const launchCounter: number = await this.incrementPythonLanguageServiceLaunchCounter(); + const show = await this.shouldShowBanner(launchCounter); + if (!show) { + return; + } + + const response = await this.appShell.showInformationMessage(this.bannerMessage, ...this.bannerLabels); + switch (response) { + case this.bannerLabels[LSSurveyLabelIndex.Yes]: + { + await this.launchSurvey(); + await this.disable(); + break; + } + case this.bannerLabels[LSSurveyLabelIndex.No]: { + await this.disable(); + break; + } + default: { + // Disable for the current session. + this.disabledInCurrentSession = true; + } + } + } + + public async shouldShowBanner(launchCounter?: number): Promise { + if (!this.enabled || this.disabledInCurrentSession) { + return false; + } + + if (! launchCounter) { + launchCounter = await this.getPythonLSLaunchCounter(); + } + const threshold: number = await this.getPythonLSLaunchThresholdCounter(); + + return launchCounter >= threshold; + } + + public async disable(): Promise { + await this.persistentState.createGlobalPersistentState(LSSurveyStateKeys.ShowBanner, false).updateValue(false); + } + + public async launchSurvey(): Promise { + const launchCounter = await this.getPythonLSLaunchCounter(); + this.browserService.launch(`https://www.research.net/r/LJZV9BZ?n=${launchCounter}`); + } + + private async incrementPythonLanguageServiceLaunchCounter(): Promise { + const state = this.persistentState.createGlobalPersistentState(LSSurveyStateKeys.ShowAttemptCounter, 0); + await state.updateValue(state.value + 1); + return state.value; + } + + private async getPythonLSLaunchCounter(): Promise { + const state = this.persistentState.createGlobalPersistentState(LSSurveyStateKeys.ShowAttemptCounter, 0); + return state.value; + } + + private async getPythonLSLaunchThresholdCounter(): Promise { + const state = this.persistentState.createGlobalPersistentState(LSSurveyStateKeys.ShowAfterCompletionCount, undefined); + if (state.value === undefined) { + await state.updateValue(getRandomBetween(this.minCompletionsBeforeShow, this.maxCompletionsBeforeShow)); + } + return state.value!; + } +} diff --git a/src/client/languageServices/proposeLanguageServerBanner.ts b/src/client/languageServices/proposeLanguageServerBanner.ts new file mode 100644 index 000000000000..7ae5dc6b5b98 --- /dev/null +++ b/src/client/languageServices/proposeLanguageServerBanner.ts @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { ConfigurationTarget } from 'vscode'; +import { IApplicationShell } from '../common/application/types'; +import '../common/extensions'; +import { IConfigurationService, IPersistentStateFactory, + IPythonExtensionBanner } from '../common/types'; +import { getRandomBetween } from '../common/utils'; + +// persistent state names, exported to make use of in testing +export enum ProposeLSStateKeys { + ShowBanner = 'ProposeLSBanner' +} + +enum ProposeLSLabelIndex { + Yes, + No, + Later +} + +/* +This class represents a popup that propose that the user try out a new +feature of the extension, and optionally enable that new feature if they +choose to do so. It is meant to be shown only to a subset of our users, +and will show as soon as it is instructed to do so, if a random sample +function enables the popup for this user. +*/ +@injectable() +export class ProposeLanguageServerBanner implements IPythonExtensionBanner { + private initialized?: boolean; + private disabledInCurrentSession: boolean = false; + private sampleSizePerHundred: number; + private bannerMessage: string = 'Try out Preview of our new Python Language Server to get richer and faster IntelliSense completions, and syntax errors as you type.'; + private bannerLabels: string[] = [ 'Try it now', 'No thanks', 'Remind me Later' ]; + + constructor( + @inject(IApplicationShell) private appShell: IApplicationShell, + @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, + @inject(IConfigurationService) private configuration: IConfigurationService, + sampleSizePerOneHundredUsers: number = 10) + { + this.sampleSizePerHundred = sampleSizePerOneHundredUsers; + this.initialize(); + } + + public initialize() { + if (this.initialized) { + return; + } + this.initialized = true; + + // Don't even bother adding handlers if banner has been turned off. + if (!this.enabled) { + return; + } + + // we only want 10% of folks that use Jedi to see this survey. + const randomSample: number = getRandomBetween(0, 100); + if (randomSample >= this.sampleSizePerHundred) { + this.disable().ignoreErrors(); + return; + } + } + + public get shownCount(): Promise { + return Promise.resolve(-1); // we don't count this popup banner! + } + + public get optionLabels(): string[] { + return this.bannerLabels; + } + + public get enabled(): boolean { + return this.persistentState.createGlobalPersistentState(ProposeLSStateKeys.ShowBanner, true).value; + } + + public async showBanner(): Promise { + if (!this.enabled) { + return; + } + + const show = await this.shouldShowBanner(); + if (!show) { + return; + } + + const response = await this.appShell.showInformationMessage(this.bannerMessage, ...this.bannerLabels); + switch (response) { + case this.bannerLabels[ProposeLSLabelIndex.Yes]: { + await this.enableNewLanguageServer(); + await this.disable(); + break; + } + case this.bannerLabels[ProposeLSLabelIndex.No]: { + await this.disable(); + break; + } + case this.bannerLabels[ProposeLSLabelIndex.Later]: { + this.disabledInCurrentSession = true; + break; + } + default: { + // Disable for the current session. + this.disabledInCurrentSession = true; + } + } + } + + public async shouldShowBanner(): Promise { + return Promise.resolve(this.enabled && !this.disabledInCurrentSession); + } + + public async disable(): Promise { + await this.persistentState.createGlobalPersistentState(ProposeLSStateKeys.ShowBanner, false).updateValue(false); + } + + public async enableNewLanguageServer(): Promise { + await this.configuration.updateSettingAsync('jediEnabled', false, undefined, ConfigurationTarget.Global); + } +} diff --git a/src/client/linters/baseLinter.ts b/src/client/linters/baseLinter.ts index d941f73246b6..51319c0f581b 100644 --- a/src/client/linters/baseLinter.ts +++ b/src/client/linters/baseLinter.ts @@ -3,16 +3,15 @@ import * as vscode from 'vscode'; import { IWorkspaceService } from '../common/application/types'; import '../common/extensions'; import { IPythonToolExecutionService } from '../common/process/types'; -import { IConfigurationService, IPythonSettings } from '../common/types'; -import { ExecutionInfo, ILogger, Product } from '../common/types'; +import { ExecutionInfo, IConfigurationService, ILogger, IPythonSettings, Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { ErrorHandler } from './errorHandlers/errorHandler'; import { ILinter, ILinterInfo, ILinterManager, ILintMessage, LintMessageSeverity } from './types'; // tslint:disable-next-line:no-require-imports no-var-requires const namedRegexp = require('named-js-regexp'); - -const REGEX = '(?\\d+),(?\\d+),(?\\w+),(?\\w\\d+):(?.*)\\r?(\\n|$)'; +// Allow negative column numbers (https://github.com/PyCQA/pylint/issues/1822) +const REGEX = '(?\\d+),(?-?\\d+),(?\\w+),(?\\w\\d+):(?.*)\\r?(\\n|$)'; export interface IRegexGroup { line: number; @@ -36,7 +35,7 @@ export abstract class BaseLinter implements ILinter { protected readonly configService: IConfigurationService; private errorHandler: ErrorHandler; - private _pythonSettings: IPythonSettings; + private _pythonSettings!: IPythonSettings; private _info: ILinterInfo; private workspace: IWorkspaceService; @@ -109,7 +108,7 @@ export abstract class BaseLinter implements ILinter { const cwd = this.getWorkspaceRootPath(document); const pythonToolsExecutionService = this.serviceContainer.get(IPythonToolExecutionService); try { - const result = await pythonToolsExecutionService.exec(executionInfo, { cwd, token: cancellation, mergeStdOutErr: true }, document.uri); + const result = await pythonToolsExecutionService.exec(executionInfo, { cwd, token: cancellation, mergeStdOutErr: false }, document.uri); this.displayLinterResultHeader(result.stdout); return await this.parseMessages(result.stdout, document, cancellation, regEx); } catch (error) { @@ -142,7 +141,7 @@ export abstract class BaseLinter implements ILinter { return { code: match.code, message: match.message, - column: isNaN(match.column) || match.column === 0 ? 0 : match.column - this.columnOffset, + column: isNaN(match.column) || match.column <= 0 ? 0 : match.column - this.columnOffset, line: match.line, type: match.type, provider: this.info.id diff --git a/src/client/linters/errorHandlers/notInstalled.ts b/src/client/linters/errorHandlers/notInstalled.ts index 3ffd90363c58..c7c56c66e7a9 100644 --- a/src/client/linters/errorHandlers/notInstalled.ts +++ b/src/client/linters/errorHandlers/notInstalled.ts @@ -1,5 +1,4 @@ import { OutputChannel, Uri } from 'vscode'; -import { isNotInstalledError } from '../../common/helpers'; import { IPythonExecutionFactory } from '../../common/process/types'; import { ExecutionInfo, Product } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; @@ -11,10 +10,10 @@ export class NotInstalledErrorHandler extends BaseErrorHandler { super(product, outputChannel, serviceContainer); } public async handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { - const pythonExecutionService = await this.serviceContainer.get(IPythonExecutionFactory).create(resource); + const pythonExecutionService = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource }); const isModuleInstalled = await pythonExecutionService.isModuleInstalled(execInfo.moduleName!); if (isModuleInstalled) { - return this.nextHandler ? await this.nextHandler.handleError(error, resource, execInfo) : false; + return this.nextHandler ? this.nextHandler.handleError(error, resource, execInfo) : false; } this.installer.promptToInstall(this.product, resource) diff --git a/src/client/linters/flake8.ts b/src/client/linters/flake8.ts index efc00becf9a9..d2c000a47fb0 100644 --- a/src/client/linters/flake8.ts +++ b/src/client/linters/flake8.ts @@ -1,5 +1,5 @@ -import { OutputChannel } from 'vscode'; -import { CancellationToken, TextDocument } from 'vscode'; +import { CancellationToken, OutputChannel, TextDocument } from 'vscode'; +import '../common/extensions'; import { Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { BaseLinter } from './baseLinter'; diff --git a/src/client/linters/linterInfo.ts b/src/client/linters/linterInfo.ts index 10f18affe708..12f3e1835453 100644 --- a/src/client/linters/linterInfo.ts +++ b/src/client/linters/linterInfo.ts @@ -60,7 +60,6 @@ export class LinterInfo implements ILinterInfo { let moduleName: string | undefined; // If path information is not available, then treat it as a module, - // Except for prospector as that needs to be run as an executable (its a python package). if (path.basename(execPath) === execPath) { moduleName = execPath; } diff --git a/src/client/linters/lintingEngine.ts b/src/client/linters/lintingEngine.ts index 650df50fa913..722dc61c830e 100644 --- a/src/client/linters/lintingEngine.ts +++ b/src/client/linters/lintingEngine.ts @@ -8,12 +8,12 @@ import * as vscode from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../common/application/types'; import { LinterErrors, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; import { IFileSystem } from '../common/platform/types'; +import { StopWatch } from '../common/stopWatch'; import { IConfigurationService, IOutputChannel } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { JupyterProvider } from '../jupyter/provider'; import { sendTelemetryWhenDone } from '../telemetry'; import { LINTING } from '../telemetry/constants'; -import { StopWatch } from '../telemetry/stopWatch'; import { LinterTrigger, LintingTelemetry } from '../telemetry/types'; import { ILinterInfo, ILinterManager, ILintingEngine, ILintMessage, LintMessageSeverity } from './types'; @@ -27,176 +27,175 @@ lintSeverityToVSSeverity.set(LintMessageSeverity.Warning, vscode.DiagnosticSever // tslint:disable-next-line:interface-name interface DocumentHasJupyterCodeCells { - // tslint:disable-next-line:callable-types - (doc: vscode.TextDocument, token: vscode.CancellationToken): Promise; + // tslint:disable-next-line:callable-types + (doc: vscode.TextDocument, token: vscode.CancellationToken): Promise; } @injectable() export class LintingEngine implements ILintingEngine { - private documentHasJupyterCodeCells: DocumentHasJupyterCodeCells; - private workspace: IWorkspaceService; - private documents: IDocumentManager; - private configurationService: IConfigurationService; - private linterManager: ILinterManager; - private diagnosticCollection: vscode.DiagnosticCollection; - private pendingLintings = new Map(); - private outputChannel: vscode.OutputChannel; - private fileSystem: IFileSystem; - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.documentHasJupyterCodeCells = (a, b) => Promise.resolve(false); - this.documents = serviceContainer.get(IDocumentManager); - this.workspace = serviceContainer.get(IWorkspaceService); - this.configurationService = serviceContainer.get(IConfigurationService); - this.outputChannel = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - this.linterManager = serviceContainer.get(ILinterManager); - this.fileSystem = serviceContainer.get(IFileSystem); - this.diagnosticCollection = vscode.languages.createDiagnosticCollection('python'); - } - - public get diagnostics(): vscode.DiagnosticCollection { - return this.diagnosticCollection; - } - - public clearDiagnostics(document: vscode.TextDocument): void { - if (this.diagnosticCollection.has(document.uri)) { - this.diagnosticCollection.delete(document.uri); + private documentHasJupyterCodeCells: DocumentHasJupyterCodeCells; + private workspace: IWorkspaceService; + private documents: IDocumentManager; + private configurationService: IConfigurationService; + private linterManager: ILinterManager; + private diagnosticCollection: vscode.DiagnosticCollection; + private pendingLintings = new Map(); + private outputChannel: vscode.OutputChannel; + private fileSystem: IFileSystem; + + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.documentHasJupyterCodeCells = (a, b) => Promise.resolve(false); + this.documents = serviceContainer.get(IDocumentManager); + this.workspace = serviceContainer.get(IWorkspaceService); + this.configurationService = serviceContainer.get(IConfigurationService); + this.outputChannel = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + this.linterManager = serviceContainer.get(ILinterManager); + this.fileSystem = serviceContainer.get(IFileSystem); + this.diagnosticCollection = vscode.languages.createDiagnosticCollection('python'); } - } - public async lintOpenPythonFiles(): Promise { - this.diagnosticCollection.clear(); - const promises = this.documents.textDocuments.map(async document => await this.lintDocument(document, 'auto')); - await Promise.all(promises); - return this.diagnosticCollection; - } - - public async lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise { - this.diagnosticCollection.set(document.uri, []); + public get diagnostics(): vscode.DiagnosticCollection { + return this.diagnosticCollection; + } - // Check if we need to lint this document - if (!await this.shouldLintDocument(document)) { - return; + public clearDiagnostics(document: vscode.TextDocument): void { + if (this.diagnosticCollection.has(document.uri)) { + this.diagnosticCollection.delete(document.uri); + } } - if (this.pendingLintings.has(document.uri.fsPath)) { - this.pendingLintings.get(document.uri.fsPath)!.cancel(); - this.pendingLintings.delete(document.uri.fsPath); + public async lintOpenPythonFiles(): Promise { + this.diagnosticCollection.clear(); + const promises = this.documents.textDocuments.map(async document => this.lintDocument(document, 'auto')); + await Promise.all(promises); + return this.diagnosticCollection; } - const cancelToken = new vscode.CancellationTokenSource(); - cancelToken.token.onCancellationRequested(() => { - if (this.pendingLintings.has(document.uri.fsPath)) { - this.pendingLintings.delete(document.uri.fsPath); - } - }); - - this.pendingLintings.set(document.uri.fsPath, cancelToken); - this.outputChannel.clear(); - - const promises: Promise[] = this.linterManager.getActiveLinters(document.uri) - .map(info => { - const stopWatch = new StopWatch(); - const linter = this.linterManager.createLinter(info.product, this.outputChannel, this.serviceContainer, document.uri); - const promise = linter.lint(document, cancelToken.token); - this.sendLinterRunTelemetry(info, document.uri, promise, stopWatch, trigger); - return promise; - }); - - const hasJupyterCodeCells = await this.documentHasJupyterCodeCells(document, cancelToken.token); - // linters will resolve asynchronously - keep a track of all - // diagnostics reported as them come in. - let diagnostics: vscode.Diagnostic[] = []; - const settings = this.configurationService.getSettings(document.uri); - - for (const p of promises) { - const msgs = await p; - if (cancelToken.token.isCancellationRequested) { - break; - } - - if (this.isDocumentOpen(document.uri)) { - // Build the message and suffix the message with the name of the linter used. - for (const m of msgs) { - // Ignore magic commands from jupyter. - if (hasJupyterCodeCells && document.lineAt(m.line - 1).text.trim().startsWith('%') && - (m.code === LinterErrors.pylint.InvalidSyntax || - m.code === LinterErrors.prospector.InvalidSyntax || - m.code === LinterErrors.flake8.InvalidSyntax)) { - continue; - } - diagnostics.push(this.createDiagnostics(m, document)); + public async lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise { + this.diagnosticCollection.set(document.uri, []); + + // Check if we need to lint this document + if (!await this.shouldLintDocument(document)) { + return; } - // Limit the number of messages to the max value. - diagnostics = diagnostics.filter((value, index) => index <= settings.linting.maxNumberOfProblems); - } - } - // Set all diagnostics found in this pass, as this method always clears existing diagnostics. - this.diagnosticCollection.set(document.uri, diagnostics); - } - - // tslint:disable-next-line:no-any - public async linkJupiterExtension(jupiter: vscode.Extension | undefined): Promise { - if (!jupiter) { - return; + + if (this.pendingLintings.has(document.uri.fsPath)) { + this.pendingLintings.get(document.uri.fsPath)!.cancel(); + this.pendingLintings.delete(document.uri.fsPath); + } + + const cancelToken = new vscode.CancellationTokenSource(); + cancelToken.token.onCancellationRequested(() => { + if (this.pendingLintings.has(document.uri.fsPath)) { + this.pendingLintings.delete(document.uri.fsPath); + } + }); + + this.pendingLintings.set(document.uri.fsPath, cancelToken); + + const promises: Promise[] = this.linterManager.getActiveLinters(document.uri) + .map(info => { + const stopWatch = new StopWatch(); + const linter = this.linterManager.createLinter(info.product, this.outputChannel, this.serviceContainer, document.uri); + const promise = linter.lint(document, cancelToken.token); + this.sendLinterRunTelemetry(info, document.uri, promise, stopWatch, trigger); + return promise; + }); + + const hasJupyterCodeCells = await this.documentHasJupyterCodeCells(document, cancelToken.token); + // linters will resolve asynchronously - keep a track of all + // diagnostics reported as them come in. + let diagnostics: vscode.Diagnostic[] = []; + const settings = this.configurationService.getSettings(document.uri); + + for (const p of promises) { + const msgs = await p; + if (cancelToken.token.isCancellationRequested) { + break; + } + + if (this.isDocumentOpen(document.uri)) { + // Build the message and suffix the message with the name of the linter used. + for (const m of msgs) { + // Ignore magic commands from jupyter. + if (hasJupyterCodeCells && document.lineAt(m.line - 1).text.trim().startsWith('%') && + (m.code === LinterErrors.pylint.InvalidSyntax || + m.code === LinterErrors.prospector.InvalidSyntax || + m.code === LinterErrors.flake8.InvalidSyntax)) { + continue; + } + diagnostics.push(this.createDiagnostics(m, document)); + } + // Limit the number of messages to the max value. + diagnostics = diagnostics.filter((value, index) => index <= settings.linting.maxNumberOfProblems); + } + } + // Set all diagnostics found in this pass, as this method always clears existing diagnostics. + this.diagnosticCollection.set(document.uri, diagnostics); } - if (!jupiter.isActive) { - await jupiter.activate(); + + // tslint:disable-next-line:no-any + public async linkJupiterExtension(jupiter: vscode.Extension | undefined): Promise { + if (!jupiter) { + return; + } + if (!jupiter.isActive) { + await jupiter.activate(); + } + // tslint:disable-next-line:no-unsafe-any + jupiter.exports.registerLanguageProvider(PYTHON.language, new JupyterProvider()); + // tslint:disable-next-line:no-unsafe-any + this.documentHasJupyterCodeCells = jupiter.exports.hasCodeCells; } - // tslint:disable-next-line:no-unsafe-any - jupiter.exports.registerLanguageProvider(PYTHON.language, new JupyterProvider()); - // tslint:disable-next-line:no-unsafe-any - this.documentHasJupyterCodeCells = jupiter.exports.hasCodeCells; - } - - private sendLinterRunTelemetry(info: ILinterInfo, resource: vscode.Uri, promise: Promise, stopWatch: StopWatch, trigger: LinterTrigger): void { - const linterExecutablePathName = info.pathName(resource); - const properties: LintingTelemetry = { - tool: info.id, - hasCustomArgs: info.linterArgs(resource).length > 0, - trigger, - executableSpecified: linterExecutablePathName.length > 0 - }; - sendTelemetryWhenDone(LINTING, promise, stopWatch, properties); - } - - private isDocumentOpen(uri: vscode.Uri): boolean { - return this.documents.textDocuments.some(document => document.uri.fsPath === uri.fsPath); - } - - private createDiagnostics(message: ILintMessage, document: vscode.TextDocument): vscode.Diagnostic { - const position = new vscode.Position(message.line - 1, message.column); - const range = new vscode.Range(position, position); - - const severity = lintSeverityToVSSeverity.get(message.severity!)!; - const diagnostic = new vscode.Diagnostic(range, `${message.code}:${message.message}`, severity); - diagnostic.code = message.code; - diagnostic.source = message.provider; - return diagnostic; - } - - private async shouldLintDocument(document: vscode.TextDocument): Promise { - if (!this.linterManager.isLintingEnabled(document.uri)) { - this.diagnosticCollection.set(document.uri, []); - return false; + + private sendLinterRunTelemetry(info: ILinterInfo, resource: vscode.Uri, promise: Promise, stopWatch: StopWatch, trigger: LinterTrigger): void { + const linterExecutablePathName = info.pathName(resource); + const properties: LintingTelemetry = { + tool: info.id, + hasCustomArgs: info.linterArgs(resource).length > 0, + trigger, + executableSpecified: linterExecutablePathName.length > 0 + }; + sendTelemetryWhenDone(LINTING, promise, stopWatch, properties); } - if (document.languageId !== PYTHON.language) { - return false; + private isDocumentOpen(uri: vscode.Uri): boolean { + return this.documents.textDocuments.some(document => document.uri.fsPath === uri.fsPath); } - const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); - const workspaceRootPath = (workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string') ? workspaceFolder.uri.fsPath : undefined; - const relativeFileName = typeof workspaceRootPath === 'string' ? path.relative(workspaceRootPath, document.fileName) : document.fileName; + private createDiagnostics(message: ILintMessage, document: vscode.TextDocument): vscode.Diagnostic { + const position = new vscode.Position(message.line - 1, message.column); + const range = new vscode.Range(position, position); - const settings = this.configurationService.getSettings(document.uri); - const ignoreMinmatches = settings.linting.ignorePatterns.map(pattern => new Minimatch(pattern)); - if (ignoreMinmatches.some(matcher => matcher.match(document.fileName) || matcher.match(relativeFileName))) { - return false; + const severity = lintSeverityToVSSeverity.get(message.severity!)!; + const diagnostic = new vscode.Diagnostic(range, `${message.code}:${message.message}`, severity); + diagnostic.code = message.code; + diagnostic.source = message.provider; + return diagnostic; } - if (document.uri.scheme !== 'file' || !document.uri.fsPath) { - return false; + + private async shouldLintDocument(document: vscode.TextDocument): Promise { + if (!this.linterManager.isLintingEnabled(document.uri)) { + this.diagnosticCollection.set(document.uri, []); + return false; + } + + if (document.languageId !== PYTHON.language) { + return false; + } + + const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); + const workspaceRootPath = (workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string') ? workspaceFolder.uri.fsPath : undefined; + const relativeFileName = typeof workspaceRootPath === 'string' ? path.relative(workspaceRootPath, document.fileName) : document.fileName; + + const settings = this.configurationService.getSettings(document.uri); + const ignoreMinmatches = settings.linting.ignorePatterns.map(pattern => new Minimatch(pattern)); + if (ignoreMinmatches.some(matcher => matcher.match(document.fileName) || matcher.match(relativeFileName))) { + return false; + } + if (document.uri.scheme !== 'file' || !document.uri.fsPath) { + return false; + } + return this.fileSystem.fileExists(document.uri.fsPath); } - return await this.fileSystem.fileExistsAsync(document.uri.fsPath); - } } diff --git a/src/client/linters/mypy.ts b/src/client/linters/mypy.ts index 15c8046a08d3..5b0930e660bd 100644 --- a/src/client/linters/mypy.ts +++ b/src/client/linters/mypy.ts @@ -1,5 +1,5 @@ -import { OutputChannel } from 'vscode'; -import { CancellationToken, TextDocument } from 'vscode'; +import { CancellationToken, OutputChannel, TextDocument } from 'vscode'; +import '../common/extensions'; import { Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { BaseLinter } from './baseLinter'; diff --git a/src/client/linters/pep8.ts b/src/client/linters/pep8.ts index 24bcaa9abeb2..959923c6ad5e 100644 --- a/src/client/linters/pep8.ts +++ b/src/client/linters/pep8.ts @@ -1,5 +1,5 @@ -import { OutputChannel } from 'vscode'; -import { CancellationToken, TextDocument } from 'vscode'; +import { CancellationToken, OutputChannel, TextDocument } from 'vscode'; +import '../common/extensions'; import { Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { BaseLinter } from './baseLinter'; diff --git a/src/client/linters/prospector.ts b/src/client/linters/prospector.ts index 930fa91c458a..0bd9873d5b8a 100644 --- a/src/client/linters/prospector.ts +++ b/src/client/linters/prospector.ts @@ -1,5 +1,6 @@ -import { OutputChannel } from 'vscode'; -import { CancellationToken, TextDocument } from 'vscode'; +import * as path from 'path'; +import { CancellationToken, OutputChannel, TextDocument } from 'vscode'; +import '../common/extensions'; import { Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { BaseLinter } from './baseLinter'; @@ -28,7 +29,9 @@ export class Prospector extends BaseLinter { } protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - return await this.run(['--absolute-paths', '--output-format=json', document.uri.fsPath], document, cancellation); + const cwd = this.getWorkspaceRootPath(document); + const relativePath = path.relative(cwd, document.uri.fsPath); + return this.run(['--absolute-paths', '--output-format=json', relativePath], document, cancellation); } protected async parseMessages(output: string, document: TextDocument, token: CancellationToken, regEx: string) { let parsedData: IProspectorResponse; diff --git a/src/client/linters/pydocstyle.ts b/src/client/linters/pydocstyle.ts index f0b05bb16726..b23d52f66945 100644 --- a/src/client/linters/pydocstyle.ts +++ b/src/client/linters/pydocstyle.ts @@ -1,6 +1,6 @@ import * as path from 'path'; -import { OutputChannel } from 'vscode'; -import { CancellationToken, TextDocument } from 'vscode'; +import { CancellationToken, OutputChannel, TextDocument } from 'vscode'; +import '../common/extensions'; import { Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { IS_WINDOWS } from './../common/utils'; @@ -61,6 +61,7 @@ export class PyDocStyle extends BaseLinter { const trmmedSourceLine = sourceLine.trim(); const sourceStart = sourceLine.indexOf(trmmedSourceLine); + // tslint:disable-next-line:no-object-literal-type-assertion return { code: code, message: message, diff --git a/src/client/linters/pylama.ts b/src/client/linters/pylama.ts index ab29fd9c55ec..edee2b44898f 100644 --- a/src/client/linters/pylama.ts +++ b/src/client/linters/pylama.ts @@ -1,5 +1,5 @@ -import { OutputChannel } from 'vscode'; -import { CancellationToken, TextDocument } from 'vscode'; +import { CancellationToken, OutputChannel, TextDocument } from 'vscode'; +import '../common/extensions'; import { Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { BaseLinter } from './baseLinter'; diff --git a/src/client/linters/pylint.ts b/src/client/linters/pylint.ts index 4111c17f83bc..ef015a260c46 100644 --- a/src/client/linters/pylint.ts +++ b/src/client/linters/pylint.ts @@ -4,8 +4,8 @@ import * as os from 'os'; import * as path from 'path'; -import { OutputChannel } from 'vscode'; -import { CancellationToken, TextDocument } from 'vscode'; +import { CancellationToken, OutputChannel, TextDocument } from 'vscode'; +import '../common/extensions'; import { IFileSystem, IPlatformService } from '../common/platform/types'; import { Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; @@ -99,17 +99,17 @@ export class Pylint extends BaseLinter { return true; } - if (await fs.fileExistsAsync(path.join(folder, pylintrc)) || await fs.fileExistsAsync(path.join(folder, dotPylintrc))) { + if (await fs.fileExists(path.join(folder, pylintrc)) || await fs.fileExists(path.join(folder, dotPylintrc))) { return true; } let current = folder; let above = path.dirname(folder); do { - if (!await fs.fileExistsAsync(path.join(current, '__init__.py'))) { + if (!await fs.fileExists(path.join(current, '__init__.py'))) { break; } - if (await fs.fileExistsAsync(path.join(current, pylintrc)) || await fs.fileExistsAsync(path.join(current, dotPylintrc))) { + if (await fs.fileExists(path.join(current, pylintrc)) || await fs.fileExists(path.join(current, dotPylintrc))) { return true; } current = above; @@ -117,15 +117,15 @@ export class Pylint extends BaseLinter { } while (!fs.arePathsSame(current, above)); const home = os.homedir(); - if (await fs.fileExistsAsync(path.join(home, dotPylintrc))) { + if (await fs.fileExists(path.join(home, dotPylintrc))) { return true; } - if (await fs.fileExistsAsync(path.join(home, '.config', pylintrc))) { + if (await fs.fileExists(path.join(home, '.config', pylintrc))) { return true; } if (!platformService.isWindows) { - if (await fs.fileExistsAsync(path.join('/etc', pylintrc))) { + if (await fs.fileExists(path.join('/etc', pylintrc))) { return true; } } @@ -138,7 +138,7 @@ export class Pylint extends BaseLinter { let current = folder; let above = path.dirname(current); do { - if (await fs.fileExistsAsync(path.join(current, pylintrc)) || await fs.fileExistsAsync(path.join(current, dotPylintrc))) { + if (await fs.fileExists(path.join(current, pylintrc)) || await fs.fileExists(path.join(current, dotPylintrc))) { return true; } current = above; diff --git a/src/client/providers/codeActionsProvider.ts b/src/client/providers/codeActionsProvider.ts new file mode 100644 index 000000000000..cd113223b819 --- /dev/null +++ b/src/client/providers/codeActionsProvider.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as vscode from 'vscode'; + +export class PythonCodeActionProvider implements vscode.CodeActionProvider { + public provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken): vscode.ProviderResult { + const sortImports = new vscode.CodeAction( + 'Sort imports', + vscode.CodeActionKind.SourceOrganizeImports + ); + sortImports.command = { + title: 'Sort imports', + command: 'python.sortImports' + }; + + return [sortImports]; + } +} diff --git a/src/client/providers/completionProvider.ts b/src/client/providers/completionProvider.ts index fb0ae33bfb1a..89c89f732091 100644 --- a/src/client/providers/completionProvider.ts +++ b/src/client/providers/completionProvider.ts @@ -7,13 +7,14 @@ import { JediFactory } from '../languageServices/jediProxyFactory'; import { captureTelemetry } from '../telemetry'; import { COMPLETION } from '../telemetry/constants'; import { CompletionSource } from './completionSource'; +import { ItemInfoSource } from './itemInfoSource'; export class PythonCompletionItemProvider implements vscode.CompletionItemProvider { private completionSource: CompletionSource; private configService: IConfigurationService; constructor(jediFactory: JediFactory, serviceContainer: IServiceContainer) { - this.completionSource = new CompletionSource(jediFactory); + this.completionSource = new CompletionSource(jediFactory, serviceContainer, new ItemInfoSource(jediFactory)); this.configService = serviceContainer.get(IConfigurationService); } diff --git a/src/client/providers/completionSource.ts b/src/client/providers/completionSource.ts index 3496f38a6f99..c287d4fa858c 100644 --- a/src/client/providers/completionSource.ts +++ b/src/client/providers/completionSource.ts @@ -3,9 +3,10 @@ 'use strict'; import * as vscode from 'vscode'; -import { PythonSettings } from '../common/configSettings'; +import { IConfigurationService } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; import { JediFactory } from '../languageServices/jediProxyFactory'; -import { ItemInfoSource, LanguageItemInfo } from './itemInfoSource'; +import { IItemInfoSource, LanguageItemInfo } from './itemInfoSource'; import * as proxy from './jediProxy'; import { isPositionInsideStringOrComment } from './providerUtilities'; @@ -25,11 +26,10 @@ class DocumentPosition { export class CompletionSource { private jediFactory: JediFactory; - private itemInfoSource: ItemInfoSource; - constructor(jediFactory: JediFactory) { + constructor(jediFactory: JediFactory, private serviceContainer: IServiceContainer, + private itemInfoSource: IItemInfoSource) { this.jediFactory = jediFactory; - this.itemInfoSource = new ItemInfoSource(jediFactory); } public async getVsCodeCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken) @@ -50,47 +50,39 @@ export class CompletionSource { // Supply hover source with simulated document text where item in question was 'already typed'. const document = documentPosition.document; const position = documentPosition.position; - const itemText = completionItem.insertText ? completionItem.insertText : completionItem.label; const wordRange = document.getWordRangeAtPosition(position); const leadingRange = wordRange !== undefined ? new vscode.Range(new vscode.Position(0, 0), wordRange.start) : new vscode.Range(new vscode.Position(0, 0), position); - const itemString = `${itemText}`; + const itemString = completionItem.label; const sourceText = `${document.getText(leadingRange)}${itemString}`; const range = new vscode.Range(leadingRange.end, leadingRange.end.translate(0, itemString.length)); - return await this.itemInfoSource.getItemInfoFromText(document.uri, document.fileName, range, sourceText, token); + return this.itemInfoSource.getItemInfoFromText(document.uri, document.fileName, range, sourceText, token); } private async getCompletionResult(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken) : Promise { - if (position.character <= 0) { - return undefined; - } - const filename = document.fileName; - const lineText = document.lineAt(position.line).text; - if (lineText.match(/^\s*\/\//)) { - return undefined; - } - // Suppress completion inside string and comments. - if (isPositionInsideStringOrComment(document, position)) { + if (position.character <= 0 || + isPositionInsideStringOrComment(document, position)) { return undefined; } + const type = proxy.CommandType.Completions; const columnIndex = position.character; const source = document.getText(); const cmd: proxy.ICommand = { command: type, - fileName: filename, + fileName: document.fileName, columnIndex: columnIndex, lineIndex: position.line, source: source }; - return await this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token); + return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token); } private toVsCodeCompletions(documentPosition: DocumentPosition, data: proxy.ICompletionResult, resource: vscode.Uri): vscode.CompletionItem[] { @@ -100,7 +92,9 @@ export class CompletionSource { private toVsCodeCompletion(documentPosition: DocumentPosition, item: proxy.IAutoCompleteItem, resource: vscode.Uri): vscode.CompletionItem { const completionItem = new vscode.CompletionItem(item.text); completionItem.kind = item.type; - if (PythonSettings.getInstance(resource).autoComplete.addBrackets === true && + const configurationService = this.serviceContainer.get(IConfigurationService); + const pythonSettings = configurationService.getSettings(resource); + if (pythonSettings.autoComplete.addBrackets === true && (item.kind === vscode.SymbolKind.Function || item.kind === vscode.SymbolKind.Method)) { completionItem.insertText = new vscode.SnippetString(item.text).appendText('(').appendTabstop().appendText(')'); } diff --git a/src/client/providers/docStringFoldingProvider.ts b/src/client/providers/docStringFoldingProvider.ts new file mode 100644 index 000000000000..2b163cade5d1 --- /dev/null +++ b/src/client/providers/docStringFoldingProvider.ts @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { CancellationToken, FoldingContext, FoldingRange, FoldingRangeKind, FoldingRangeProvider, ProviderResult, Range, TextDocument } from 'vscode'; +import { IterableTextRange } from '../language/iterableTextRange'; +import { IToken, TokenizerMode, TokenType } from '../language/types'; +import { getDocumentTokens } from './providerUtilities'; + +export class DocStringFoldingProvider implements FoldingRangeProvider { + public provideFoldingRanges(document: TextDocument, _context: FoldingContext, token: CancellationToken): ProviderResult { + return this.getFoldingRanges(document); + } + + private getFoldingRanges(document: TextDocument) { + const tokenCollection = getDocumentTokens(document, document.lineAt(document.lineCount - 1).range.end, TokenizerMode.CommentsAndStrings); + const tokens = new IterableTextRange(tokenCollection); + + const docStringRanges: FoldingRange[] = []; + const commentRanges: FoldingRange[] = []; + + for (const token of tokens) { + const docstringRange = this.getDocStringFoldingRange(document, token); + if (docstringRange) { + docStringRanges.push(docstringRange); + continue; + } + + const commentRange = this.getSingleLineCommentRange(document, token); + if (commentRange) { + this.buildMultiLineCommentRange(commentRange, commentRanges); + } + } + + this.removeLastSingleLineComment(commentRanges); + return docStringRanges.concat(commentRanges); + } + private buildMultiLineCommentRange(commentRange: FoldingRange, commentRanges: FoldingRange[]) { + if (commentRanges.length === 0) { + commentRanges.push(commentRange); + return; + } + const previousComment = commentRanges[commentRanges.length - 1]; + if (previousComment.end + 1 === commentRange.start) { + previousComment.end = commentRange.end; + return; + } + if (previousComment.start === previousComment.end) { + commentRanges[commentRanges.length - 1] = commentRange; + return; + } + commentRanges.push(commentRange); + } + private removeLastSingleLineComment(commentRanges: FoldingRange[]) { + // Remove last comment folding range if its a single line entry. + if (commentRanges.length === 0) { + return; + } + const lastComment = commentRanges[commentRanges.length - 1]; + if (lastComment.start === lastComment.end) { + commentRanges.pop(); + } + } + private getDocStringFoldingRange(document: TextDocument, token: IToken) { + if (token.type !== TokenType.String) { + return; + } + + const startPosition = document.positionAt(token.start); + const endPosition = document.positionAt(token.end); + if (startPosition.line === endPosition.line) { + return; + } + + const startLine = document.lineAt(startPosition); + if (startLine.firstNonWhitespaceCharacterIndex !== startPosition.character) { + return; + } + const startIndex1 = startLine.text.indexOf('\'\'\''); + const startIndex2 = startLine.text.indexOf('"""'); + if (startIndex1 !== startPosition.character && startIndex2 !== startPosition.character) { + return; + } + + const range = new Range(startPosition, endPosition); + + return new FoldingRange(range.start.line, range.end.line); + } + private getSingleLineCommentRange(document: TextDocument, token: IToken) { + if (token.type !== TokenType.Comment) { + return; + } + + const startPosition = document.positionAt(token.start); + const endPosition = document.positionAt(token.end); + if (startPosition.line !== endPosition.line) { + return; + } + if (document.lineAt(startPosition).firstNonWhitespaceCharacterIndex !== startPosition.character) { + return; + } + + const range = new Range(startPosition, endPosition); + return new FoldingRange(range.start.line, range.end.line, FoldingRangeKind.Comment); + } +} diff --git a/src/client/providers/formatProvider.ts b/src/client/providers/formatProvider.ts index cabab87018e8..9398fd7b0e58 100644 --- a/src/client/providers/formatProvider.ts +++ b/src/client/providers/formatProvider.ts @@ -8,6 +8,7 @@ import { IConfigurationService } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { AutoPep8Formatter } from './../formatters/autoPep8Formatter'; import { BaseFormatter } from './../formatters/baseFormatter'; +import { BlackFormatter } from './../formatters/blackFormatter'; import { DummyFormatter } from './../formatters/dummyFormatter'; import { YapfFormatter } from './../formatters/yapfFormatter'; @@ -27,8 +28,10 @@ export class PythonFormattingEditProvider implements vscode.DocumentFormattingEd public constructor(context: vscode.ExtensionContext, serviceContainer: IServiceContainer) { const yapfFormatter = new YapfFormatter(serviceContainer); const autoPep8 = new AutoPep8Formatter(serviceContainer); + const black = new BlackFormatter(serviceContainer); const dummy = new DummyFormatter(serviceContainer); this.formatters.set(yapfFormatter.Id, yapfFormatter); + this.formatters.set(black.Id, black); this.formatters.set(autoPep8.Id, autoPep8); this.formatters.set(dummy.Id, dummy); @@ -36,7 +39,7 @@ export class PythonFormattingEditProvider implements vscode.DocumentFormattingEd this.workspace = serviceContainer.get(IWorkspaceService); this.documentManager = serviceContainer.get(IDocumentManager); this.config = serviceContainer.get(IConfigurationService); - this.disposables.push(this.documentManager.onDidSaveTextDocument(async document => await this.onSaveDocument(document))); + this.disposables.push(this.documentManager.onDidSaveTextDocument(async document => this.onSaveDocument(document))); } public dispose() { diff --git a/src/client/providers/importSortProvider.ts b/src/client/providers/importSortProvider.ts index e7e6d3347133..90733f15bfe1 100644 --- a/src/client/providers/importSortProvider.ts +++ b/src/client/providers/importSortProvider.ts @@ -3,14 +3,19 @@ import * as path from 'path'; import { TextDocument, TextEdit } from 'vscode'; import { PythonSettings } from '../common/configSettings'; import { getTempFileWithDocumentContents, getTextEditsFromPatch } from '../common/editor'; -import { ExecutionResult, IProcessService, IPythonExecutionFactory } from '../common/process/types'; +import { ExecutionResult, IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; +import { IServiceContainer } from '../ioc/types'; import { captureTelemetry } from '../telemetry'; import { FORMAT_SORT_IMPORTS } from '../telemetry/constants'; // tslint:disable-next-line:completed-docs export class PythonImportSortProvider { - constructor(private pythonExecutionFactory: IPythonExecutionFactory, - private processService: IProcessService) { } + private readonly processServiceFactory: IProcessServiceFactory; + private readonly pythonExecutionFactory: IPythonExecutionFactory; + constructor(serviceContainer: IServiceContainer) { + this.pythonExecutionFactory = serviceContainer.get(IPythonExecutionFactory); + this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); + } @captureTelemetry(FORMAT_SORT_IMPORTS) public async sortImports(extensionDir: string, document: TextDocument): Promise { if (document.lineCount === 1) { @@ -30,9 +35,10 @@ export class PythonImportSortProvider { if (typeof isort === 'string' && isort.length > 0) { // Lets just treat this as a standard tool. - promise = this.processService.exec(isort, args, { throwOnStdErr: true }); + const processService = await this.processServiceFactory.create(document.uri); + promise = processService.exec(isort, args, { throwOnStdErr: true }); } else { - promise = this.pythonExecutionFactory.create(document.uri) + promise = this.pythonExecutionFactory.create({ resource: document.uri }) .then(executionService => executionService.exec([importScript].concat(args), { throwOnStdErr: true })); } diff --git a/src/client/providers/itemInfoSource.ts b/src/client/providers/itemInfoSource.ts index b851f61b533d..5effbe7483e4 100644 --- a/src/client/providers/itemInfoSource.ts +++ b/src/client/providers/itemInfoSource.ts @@ -15,7 +15,15 @@ export class LanguageItemInfo { public signature: vscode.MarkdownString) { } } -export class ItemInfoSource { +export interface IItemInfoSource { + getItemInfoFromText(documentUri: vscode.Uri, fileName: string, + range: vscode.Range, sourceText: string, + token: vscode.CancellationToken): Promise; + getItemInfoFromDocument(document: vscode.TextDocument, position: vscode.Position, + token: vscode.CancellationToken): Promise; +} + +export class ItemInfoSource implements IItemInfoSource { private textConverter = new RestTextConverter(); constructor(private jediFactory: JediFactory) { } @@ -51,7 +59,7 @@ export class ItemInfoSource { if (!range || range.isEmpty) { return; } - return await this.getHoverResultFromDocumentRange(document, range, token); + return this.getHoverResultFromDocumentRange(document, range, token); } private async getHoverResultFromDocumentRange(document: vscode.TextDocument, range: vscode.Range, token: vscode.CancellationToken) @@ -65,7 +73,7 @@ export class ItemInfoSource { if (document.isDirty) { cmd.source = document.getText(); } - return await this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token); + return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token); } private async getHoverResultFromTextRange(documentUri: vscode.Uri, fileName: string, range: vscode.Range, sourceText: string, token: vscode.CancellationToken) @@ -77,7 +85,7 @@ export class ItemInfoSource { lineIndex: range.end.line, source: sourceText }; - return await this.jediFactory.getJediProxyHandler(documentUri).sendCommand(cmd, token); + return this.jediFactory.getJediProxyHandler(documentUri).sendCommand(cmd, token); } private getItemInfoFromHoverResult(data: proxy.IHoverResult, currentWord: string): LanguageItemInfo[] { diff --git a/src/client/providers/jediProxy.ts b/src/client/providers/jediProxy.ts index fcf9b7c7e523..962b6dc8c0b0 100644 --- a/src/client/providers/jediProxy.ts +++ b/src/client/providers/jediProxy.ts @@ -6,111 +6,112 @@ import { ChildProcess } from 'child_process'; import * as fs from 'fs-extra'; import * as path from 'path'; import * as pidusage from 'pidusage'; -import { setInterval } from 'timers'; -import { Uri } from 'vscode'; -import * as vscode from 'vscode'; +import { CancellationToken, CancellationTokenSource, CompletionItemKind, + Disposable, SymbolKind, Uri } from 'vscode'; import { PythonSettings } from '../common/configSettings'; +import { isTestExecution } from '../common/constants'; import { debounce, swallowExceptions } from '../common/decorators'; import '../common/extensions'; import { createDeferred, Deferred } from '../common/helpers'; import { IPythonExecutionFactory } from '../common/process/types'; -import { ILogger } from '../common/types'; +import { StopWatch } from '../common/stopWatch'; +import { BANNER_NAME_PROPOSE_LS, ILogger, IPythonExtensionBanner } from '../common/types'; import { IEnvironmentVariablesProvider } from '../common/variables/types'; import { IServiceContainer } from '../ioc/types'; import * as logger from './../common/logger'; const IS_WINDOWS = /^win/.test(process.platform); -const pythonVSCodeTypeMappings = new Map(); -pythonVSCodeTypeMappings.set('none', vscode.CompletionItemKind.Value); -pythonVSCodeTypeMappings.set('type', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('tuple', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('dict', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('dictionary', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('function', vscode.CompletionItemKind.Function); -pythonVSCodeTypeMappings.set('lambda', vscode.CompletionItemKind.Function); -pythonVSCodeTypeMappings.set('generator', vscode.CompletionItemKind.Function); -pythonVSCodeTypeMappings.set('class', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('instance', vscode.CompletionItemKind.Reference); -pythonVSCodeTypeMappings.set('method', vscode.CompletionItemKind.Method); -pythonVSCodeTypeMappings.set('builtin', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('builtinfunction', vscode.CompletionItemKind.Function); -pythonVSCodeTypeMappings.set('module', vscode.CompletionItemKind.Module); -pythonVSCodeTypeMappings.set('file', vscode.CompletionItemKind.File); -pythonVSCodeTypeMappings.set('xrange', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('slice', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('traceback', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('frame', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('buffer', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('dictproxy', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('funcdef', vscode.CompletionItemKind.Function); -pythonVSCodeTypeMappings.set('property', vscode.CompletionItemKind.Property); -pythonVSCodeTypeMappings.set('import', vscode.CompletionItemKind.Module); -pythonVSCodeTypeMappings.set('keyword', vscode.CompletionItemKind.Keyword); -pythonVSCodeTypeMappings.set('constant', vscode.CompletionItemKind.Variable); -pythonVSCodeTypeMappings.set('variable', vscode.CompletionItemKind.Variable); -pythonVSCodeTypeMappings.set('value', vscode.CompletionItemKind.Value); -pythonVSCodeTypeMappings.set('param', vscode.CompletionItemKind.Variable); -pythonVSCodeTypeMappings.set('statement', vscode.CompletionItemKind.Keyword); - -const pythonVSCodeSymbolMappings = new Map(); -pythonVSCodeSymbolMappings.set('none', vscode.SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('type', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('tuple', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('dict', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('dictionary', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('function', vscode.SymbolKind.Function); -pythonVSCodeSymbolMappings.set('lambda', vscode.SymbolKind.Function); -pythonVSCodeSymbolMappings.set('generator', vscode.SymbolKind.Function); -pythonVSCodeSymbolMappings.set('class', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('instance', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('method', vscode.SymbolKind.Method); -pythonVSCodeSymbolMappings.set('builtin', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('builtinfunction', vscode.SymbolKind.Function); -pythonVSCodeSymbolMappings.set('module', vscode.SymbolKind.Module); -pythonVSCodeSymbolMappings.set('file', vscode.SymbolKind.File); -pythonVSCodeSymbolMappings.set('xrange', vscode.SymbolKind.Array); -pythonVSCodeSymbolMappings.set('slice', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('traceback', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('frame', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('buffer', vscode.SymbolKind.Array); -pythonVSCodeSymbolMappings.set('dictproxy', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('funcdef', vscode.SymbolKind.Function); -pythonVSCodeSymbolMappings.set('property', vscode.SymbolKind.Property); -pythonVSCodeSymbolMappings.set('import', vscode.SymbolKind.Module); -pythonVSCodeSymbolMappings.set('keyword', vscode.SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('constant', vscode.SymbolKind.Constant); -pythonVSCodeSymbolMappings.set('variable', vscode.SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('value', vscode.SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('param', vscode.SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('statement', vscode.SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('boolean', vscode.SymbolKind.Boolean); -pythonVSCodeSymbolMappings.set('int', vscode.SymbolKind.Number); -pythonVSCodeSymbolMappings.set('longlean', vscode.SymbolKind.Number); -pythonVSCodeSymbolMappings.set('float', vscode.SymbolKind.Number); -pythonVSCodeSymbolMappings.set('complex', vscode.SymbolKind.Number); -pythonVSCodeSymbolMappings.set('string', vscode.SymbolKind.String); -pythonVSCodeSymbolMappings.set('unicode', vscode.SymbolKind.String); -pythonVSCodeSymbolMappings.set('list', vscode.SymbolKind.Array); - -function getMappedVSCodeType(pythonType: string): vscode.CompletionItemKind { +const pythonVSCodeTypeMappings = new Map(); +pythonVSCodeTypeMappings.set('none', CompletionItemKind.Value); +pythonVSCodeTypeMappings.set('type', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('tuple', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('dict', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('dictionary', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('function', CompletionItemKind.Function); +pythonVSCodeTypeMappings.set('lambda', CompletionItemKind.Function); +pythonVSCodeTypeMappings.set('generator', CompletionItemKind.Function); +pythonVSCodeTypeMappings.set('class', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('instance', CompletionItemKind.Reference); +pythonVSCodeTypeMappings.set('method', CompletionItemKind.Method); +pythonVSCodeTypeMappings.set('builtin', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('builtinfunction', CompletionItemKind.Function); +pythonVSCodeTypeMappings.set('module', CompletionItemKind.Module); +pythonVSCodeTypeMappings.set('file', CompletionItemKind.File); +pythonVSCodeTypeMappings.set('xrange', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('slice', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('traceback', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('frame', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('buffer', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('dictproxy', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('funcdef', CompletionItemKind.Function); +pythonVSCodeTypeMappings.set('property', CompletionItemKind.Property); +pythonVSCodeTypeMappings.set('import', CompletionItemKind.Module); +pythonVSCodeTypeMappings.set('keyword', CompletionItemKind.Keyword); +pythonVSCodeTypeMappings.set('constant', CompletionItemKind.Variable); +pythonVSCodeTypeMappings.set('variable', CompletionItemKind.Variable); +pythonVSCodeTypeMappings.set('value', CompletionItemKind.Value); +pythonVSCodeTypeMappings.set('param', CompletionItemKind.Variable); +pythonVSCodeTypeMappings.set('statement', CompletionItemKind.Keyword); + +const pythonVSCodeSymbolMappings = new Map(); +pythonVSCodeSymbolMappings.set('none', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('type', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('tuple', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('dict', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('dictionary', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('function', SymbolKind.Function); +pythonVSCodeSymbolMappings.set('lambda', SymbolKind.Function); +pythonVSCodeSymbolMappings.set('generator', SymbolKind.Function); +pythonVSCodeSymbolMappings.set('class', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('instance', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('method', SymbolKind.Method); +pythonVSCodeSymbolMappings.set('builtin', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('builtinfunction', SymbolKind.Function); +pythonVSCodeSymbolMappings.set('module', SymbolKind.Module); +pythonVSCodeSymbolMappings.set('file', SymbolKind.File); +pythonVSCodeSymbolMappings.set('xrange', SymbolKind.Array); +pythonVSCodeSymbolMappings.set('slice', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('traceback', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('frame', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('buffer', SymbolKind.Array); +pythonVSCodeSymbolMappings.set('dictproxy', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('funcdef', SymbolKind.Function); +pythonVSCodeSymbolMappings.set('property', SymbolKind.Property); +pythonVSCodeSymbolMappings.set('import', SymbolKind.Module); +pythonVSCodeSymbolMappings.set('keyword', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('constant', SymbolKind.Constant); +pythonVSCodeSymbolMappings.set('variable', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('value', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('param', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('statement', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('boolean', SymbolKind.Boolean); +pythonVSCodeSymbolMappings.set('int', SymbolKind.Number); +pythonVSCodeSymbolMappings.set('longlean', SymbolKind.Number); +pythonVSCodeSymbolMappings.set('float', SymbolKind.Number); +pythonVSCodeSymbolMappings.set('complex', SymbolKind.Number); +pythonVSCodeSymbolMappings.set('string', SymbolKind.String); +pythonVSCodeSymbolMappings.set('unicode', SymbolKind.String); +pythonVSCodeSymbolMappings.set('list', SymbolKind.Array); + +function getMappedVSCodeType(pythonType: string): CompletionItemKind { if (pythonVSCodeTypeMappings.has(pythonType)) { const value = pythonVSCodeTypeMappings.get(pythonType); if (value) { return value; } } - return vscode.CompletionItemKind.Keyword; + return CompletionItemKind.Keyword; } -function getMappedVSCodeSymbol(pythonType: string): vscode.SymbolKind { +function getMappedVSCodeSymbol(pythonType: string): SymbolKind { if (pythonVSCodeSymbolMappings.has(pythonType)) { const value = pythonVSCodeSymbolMappings.get(pythonType); if (value) { return value; } } - return vscode.SymbolKind.Variable; + return SymbolKind.Variable; } export enum CommandType { @@ -130,8 +131,8 @@ commandNames.set(CommandType.Hover, 'tooltip'); commandNames.set(CommandType.Usages, 'usages'); commandNames.set(CommandType.Symbols, 'names'); -export class JediProxy implements vscode.Disposable { - private proc: ChildProcess | null; +export class JediProxy implements Disposable { + private proc?: ChildProcess; private pythonSettings: PythonSettings; private cmdId: number = 0; private lastKnownPythonInterpreter: string; @@ -141,24 +142,28 @@ export class JediProxy implements vscode.Disposable { private spawnRetryAttempts = 0; private additionalAutoCompletePaths: string[] = []; private workspacePath: string; - private languageServerStarted: Deferred; + private languageServerStarted!: Deferred; private initialized: Deferred; - private environmentVariablesProvider: IEnvironmentVariablesProvider; + private environmentVariablesProvider!: IEnvironmentVariablesProvider; private logger: ILogger; + private ignoreJediMemoryFootprint: boolean = false; + private pidUsageFailures = { timer: new StopWatch(), counter: 0 }; + private lastCmdIdProcessed?: number; + private lastCmdIdProcessedForPidUsage?: number; + private proposeNewLanguageServerPopup: IPythonExtensionBanner; public constructor(private extensionRootDir: string, workspacePath: string, private serviceContainer: IServiceContainer) { this.workspacePath = workspacePath; - this.pythonSettings = PythonSettings.getInstance(vscode.Uri.file(workspacePath)); + this.pythonSettings = PythonSettings.getInstance(Uri.file(workspacePath)); this.lastKnownPythonInterpreter = this.pythonSettings.pythonPath; this.logger = serviceContainer.get(ILogger); this.pythonSettings.on('change', () => this.pythonSettingsChangeHandler()); this.initialized = createDeferred(); this.startLanguageServer().then(() => this.initialized.resolve()).ignoreErrors(); - // Check memory footprint periodically. Do not check on every request due to - // the performance impact. See https://github.com/soyuka/pidusage - on Windows - // it is using wmic which means spawning cmd.exe process on every request. - setInterval(() => this.checkJediMemoryFootprint(), 2000); + this.proposeNewLanguageServerPopup = serviceContainer.get(IPythonExtensionBanner, BANNER_NAME_PROPOSE_LS); + + this.checkJediMemoryFootprint().ignoreErrors(); } private static getProperty(o: object, name: string): T { @@ -212,21 +217,62 @@ export class JediProxy implements vscode.Disposable { this.handleError('spawnProcess', ex); }); } + private shouldCheckJediMemoryFootprint() { + if (this.ignoreJediMemoryFootprint || this.pythonSettings.jediMemoryLimit === -1) { + return false; + } + if (this.lastCmdIdProcessedForPidUsage && this.lastCmdIdProcessed && + this.lastCmdIdProcessedForPidUsage === this.lastCmdIdProcessed) { + // If no more commands were processed since the last time, + // then there's no need to check again. + return false; + } + return true; + } + private async checkJediMemoryFootprint() { + // Check memory footprint periodically. Do not check on every request due to + // the performance impact. See https://github.com/soyuka/pidusage - on Windows + // it is using wmic which means spawning cmd.exe process on every request. + if (this.pythonSettings.jediMemoryLimit === -1) { + return; + } - private checkJediMemoryFootprint() { + await this.checkJediMemoryFootprintImpl(); + setTimeout(() => this.checkJediMemoryFootprint(), 15 * 1000); + } + private async checkJediMemoryFootprintImpl(): Promise { if (!this.proc || this.proc.killed) { return; } + if (!this.shouldCheckJediMemoryFootprint()) { + return; + } + this.lastCmdIdProcessedForPidUsage = this.lastCmdIdProcessed; + + // Do not run pidusage over and over, wait for it to finish. + const deferred = createDeferred(); pidusage.stat(this.proc.pid, async (err, result) => { if (err) { - return console.error('Python Extension: (pidusage)', err); - } - const limit = Math.min(Math.max(this.pythonSettings.jediMemoryLimit, 1024), 8192); - if (result && result.memory > limit * 1024 * 1024) { - this.logger.logWarning(`IntelliSense process memory consumption exceeded limit of ${limit} MB and process will be restarted.\nThe limit is controlled by the 'python.jediMemoryLimit' setting.`); - await this.restartLanguageServer(); + this.pidUsageFailures.counter += 1; + // If this function fails 2 times in the last 60 seconds, lets not try ever again. + if (this.pidUsageFailures.timer.elapsedTime > 60 * 1000) { + this.ignoreJediMemoryFootprint = this.pidUsageFailures.counter > 2; + this.pidUsageFailures.counter = 0; + this.pidUsageFailures.timer.reset(); + } + console.error('Python Extension: (pidusage)', err); + } else { + const limit = Math.min(Math.max(this.pythonSettings.jediMemoryLimit, 1024), 8192); + if (result && result.memory > limit * 1024 * 1024) { + this.logger.logWarning(`IntelliSense process memory consumption exceeded limit of ${limit} MB and process will be restarted.\nThe limit is controlled by the 'python.jediMemoryLimit' setting.`); + await this.restartLanguageServer(); + } } + + deferred.resolve(); }); + + return deferred.promise; } @swallowExceptions('JediProxy') @@ -251,6 +297,9 @@ export class JediProxy implements vscode.Disposable { private async startLanguageServer(): Promise { const newAutoComletePaths = await this.buildAutoCompletePaths(); this.additionalAutoCompletePaths = newAutoComletePaths; + if (!isTestExecution()) { + await this.proposeNewLanguageServerPopup.showBanner(); + } return this.restartLanguageServer(); } private restartLanguageServer(): Promise { @@ -276,7 +325,7 @@ export class JediProxy implements vscode.Disposable { } // tslint:disable-next-line:no-empty } catch (ex) { } - this.proc = null; + this.proc = undefined; } private handleError(source: string, errorMessage: string) { @@ -286,16 +335,21 @@ export class JediProxy implements vscode.Disposable { // tslint:disable-next-line:max-func-body-length private async spawnProcess(cwd: string) { if (this.languageServerStarted && !this.languageServerStarted.completed) { - this.languageServerStarted.reject(); + this.languageServerStarted.reject(new Error('Language Server not started.')); } this.languageServerStarted = createDeferred(); - const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create(Uri.file(this.workspacePath)); + const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource: Uri.file(this.workspacePath) }); + // Check if the python path is valid. + if ((await pythonProcess.getExecutablePath().catch(() => '')).length === 0) { + return; + } const args = ['completion.py']; if (typeof this.pythonSettings.jediPath === 'string' && this.pythonSettings.jediPath.length > 0) { args.push('custom'); args.push(this.pythonSettings.jediPath); } - if (Array.isArray(this.pythonSettings.autoComplete.preloadModules) && + if (this.pythonSettings.autoComplete && + Array.isArray(this.pythonSettings.autoComplete.preloadModules) && this.pythonSettings.autoComplete.preloadModules.length > 0) { const modules = this.pythonSettings.autoComplete.preloadModules.filter(m => m.trim().length > 0).join(','); args.push(modules); @@ -345,12 +399,18 @@ export class JediProxy implements vscode.Disposable { } responses.forEach((response) => { + if (!response) { + return; + } const responseId = JediProxy.getProperty(response, 'id'); - const cmd = >this.commands.get(responseId); - if (cmd === null) { + if (!this.commands.has(responseId)) { return; } - + const cmd = this.commands.get(responseId); + if (!cmd) { + return; + } + this.lastCmdIdProcessed = cmd.id; if (JediProxy.getProperty(response, 'arguments')) { this.commandQueue.splice(this.commandQueue.indexOf(cmd.id), 1); return; @@ -512,6 +572,7 @@ export class JediProxy implements vscode.Disposable { private onArguments(command: IExecutionCommand, response: object): void { // tslint:disable-next-line:no-any const defs = JediProxy.getProperty(response, 'results'); + // tslint:disable-next-line:no-object-literal-type-assertion this.safeResolve(command, { requestId: command.id, definitions: defs @@ -559,7 +620,7 @@ export class JediProxy implements vscode.Disposable { private async getPathFromPythonCommand(args: string[]): Promise { try { - const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create(Uri.file(this.workspacePath)); + const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource: Uri.file(this.workspacePath) }); const result = await pythonProcess.exec(args, { cwd: this.workspacePath }); const lines = result.stdout.trim().splitLines(); if (lines.length === 0) { @@ -615,15 +676,16 @@ export class JediProxy implements vscode.Disposable { } private getConfig() { // Add support for paths relative to workspace. - const 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); - }); + const extraPaths = this.pythonSettings.autoComplete ? + 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') { @@ -665,7 +727,7 @@ export interface ICommand { interface IExecutionCommand extends ICommand { id: number; deferred?: Deferred; - token: vscode.CancellationToken; + token: CancellationToken; delay?: number; } @@ -718,9 +780,9 @@ export interface IReference { } export interface IAutoCompleteItem { - type: vscode.CompletionItemKind; - rawType: vscode.CompletionItemKind; - kind: vscode.SymbolKind; + type: CompletionItemKind; + rawType: CompletionItemKind; + kind: SymbolKind; text: string; description: string; raw_docstring: string; @@ -734,8 +796,8 @@ export interface IDefinitionRange { } export interface IDefinition { rawType: string; - type: vscode.CompletionItemKind; - kind: vscode.SymbolKind; + type: CompletionItemKind; + kind: SymbolKind; text: string; fileName: string; container: string; @@ -743,22 +805,22 @@ export interface IDefinition { } export interface IHoverItem { - kind: vscode.SymbolKind; + kind: SymbolKind; text: string; description: string; docstring: string; signature: string; } -export class JediProxyHandler implements vscode.Disposable { - private commandCancellationTokenSources: Map; +export class JediProxyHandler implements Disposable { + private commandCancellationTokenSources: Map; public get JediProxy(): JediProxy { return this.jediProxy; } public constructor(private jediProxy: JediProxy) { - this.commandCancellationTokenSources = new Map(); + this.commandCancellationTokenSources = new Map(); } public dispose() { @@ -767,7 +829,7 @@ export class JediProxyHandler implements vscode.Dispos } } - public sendCommand(cmd: ICommand, token?: vscode.CancellationToken): Promise { + public sendCommand(cmd: ICommand, token?: CancellationToken): Promise { const executionCmd = >cmd; executionCmd.id = executionCmd.id || this.jediProxy.getNextCommandId(); @@ -778,7 +840,7 @@ export class JediProxyHandler implements vscode.Dispos } } - const cancellation = new vscode.CancellationTokenSource(); + const cancellation = new CancellationTokenSource(); this.commandCancellationTokenSources.set(cmd.command, cancellation); executionCmd.token = cancellation.token; @@ -789,7 +851,7 @@ export class JediProxyHandler implements vscode.Dispos }); } - public sendCommandNonCancellableCommand(cmd: ICommand, token?: vscode.CancellationToken): Promise { + public sendCommandNonCancellableCommand(cmd: ICommand, token?: CancellationToken): Promise { const executionCmd = >cmd; executionCmd.id = executionCmd.id || this.jediProxy.getNextCommandId(); if (token) { diff --git a/src/client/providers/providerUtilities.ts b/src/client/providers/providerUtilities.ts index 4b113c61eceb..7ee45ab8e25a 100644 --- a/src/client/providers/providerUtilities.ts +++ b/src/client/providers/providerUtilities.ts @@ -1,22 +1,28 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as vscode from 'vscode'; +import { Position, Range, TextDocument } from 'vscode'; import { Tokenizer } from '../language/tokenizer'; import { ITextRangeCollection, IToken, TokenizerMode, TokenType } from '../language/types'; -export function getDocumentTokens(document: vscode.TextDocument, tokenizeTo: vscode.Position, mode: TokenizerMode): ITextRangeCollection { - const text = document.getText(new vscode.Range(new vscode.Position(0, 0), tokenizeTo)); +export function getDocumentTokens(document: TextDocument, tokenizeTo: Position, mode: TokenizerMode): ITextRangeCollection { + const text = document.getText(new Range(new Position(0, 0), tokenizeTo)); return new Tokenizer().tokenize(text, 0, text.length, mode); } -export function isPositionInsideStringOrComment(document: vscode.TextDocument, position: vscode.Position): boolean { +export function isPositionInsideStringOrComment(document: TextDocument, position: Position): boolean { const tokenizeTo = position.translate(1, 0); const tokens = getDocumentTokens(document, tokenizeTo, TokenizerMode.CommentsAndStrings); - const index = tokens.getItemContaining(document.offsetAt(position)); + const offset = document.offsetAt(position); + const index = tokens.getItemContaining(offset - 1); if (index >= 0) { const token = tokens.getItemAt(index); return token.type === TokenType.String || token.type === TokenType.Comment; } + if (offset > 0 && index >= 0) { + // In case position is at the every end of the comment or unterminated string + const token = tokens.getItemAt(index); + return token.end === offset && token.type === TokenType.Comment; + } return false; } diff --git a/src/client/providers/signatureProvider.ts b/src/client/providers/signatureProvider.ts index 12dad261c39b..f6ea0d65fd6e 100644 --- a/src/client/providers/signatureProvider.ts +++ b/src/client/providers/signatureProvider.ts @@ -1,12 +1,20 @@ 'use strict'; import { EOL } from 'os'; -import * as vscode from 'vscode'; -import { CancellationToken, Position, SignatureHelp, TextDocument } from 'vscode'; +import { + CancellationToken, + ParameterInformation, + Position, + SignatureHelp, + SignatureHelpProvider, + SignatureInformation, + TextDocument +} from 'vscode'; import { JediFactory } from '../languageServices/jediProxyFactory'; import { captureTelemetry } from '../telemetry'; import { SIGNATURE } from '../telemetry/constants'; import * as proxy from './jediProxy'; +import { isPositionInsideStringOrComment } from './providerUtilities'; const DOCSTRING_PARAM_PATTERNS = [ '\\s*:type\\s*PARAMNAME:\\s*([^\\n, ]+)', // Sphinx @@ -45,9 +53,9 @@ function extractParamDocString(paramName: string, docString: string): string { return paramDocString.trim(); } -export class PythonSignatureProvider implements vscode.SignatureHelpProvider { +export class PythonSignatureProvider implements SignatureHelpProvider { public constructor(private jediFactory: JediFactory) { } - private static parseData(data: proxy.IArgumentsResult): vscode.SignatureHelp { + private static parseData(data: proxy.IArgumentsResult): SignatureHelp { if (data && Array.isArray(data.definitions) && data.definitions.length > 0) { const signature = new SignatureHelp(); signature.activeSignature = 0; @@ -60,29 +68,36 @@ export class PythonSignatureProvider implements vscode.SignatureHelpProvider { // Some functions do not come with parameter docs let label: string; let documentation: string; - const validParamInfo = def.params && def.params.length > 0 && def.docstring.startsWith(`${def.name}(`); + const validParamInfo = def.params && def.params.length > 0 && def.docstring && def.docstring.startsWith(`${def.name}(`); if (validParamInfo) { const docLines = def.docstring.splitLines(); - label = docLines.shift().trim(); + label = docLines.shift()!.trim(); documentation = docLines.join(EOL).trim(); } else { - label = def.description; - documentation = def.docstring; + if (def.params && def.params.length > 0) { + label = `${def.name}(${def.params.map(p => p.name).join(', ')})`; + documentation = def.docstring; + } else { + label = def.description; + documentation = def.docstring; + } } - const sig = { + // tslint:disable-next-line:no-object-literal-type-assertion + const sig = { label, documentation, parameters: [] }; - if (validParamInfo) { + if (def.params && def.params.length) { sig.parameters = def.params.map(arg => { if (arg.docstring.length === 0) { arg.docstring = extractParamDocString(arg.name, def.docstring); } - return { + // tslint:disable-next-line:no-object-literal-type-assertion + return { documentation: arg.docstring.length > 0 ? arg.docstring : arg.description, label: arg.name.trim() }; @@ -97,6 +112,13 @@ export class PythonSignatureProvider implements vscode.SignatureHelpProvider { } @captureTelemetry(SIGNATURE) public provideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken): Thenable { + // early exit if we're in a string or comment (or in an undefined position) + if (position.character <= 0 || + isPositionInsideStringOrComment(document, position)) + { + return Promise.resolve(new SignatureHelp()); + } + const cmd: proxy.ICommand = { command: proxy.CommandType.Arguments, fileName: document.fileName, diff --git a/src/client/providers/simpleRefactorProvider.ts b/src/client/providers/simpleRefactorProvider.ts index f6b1772dc19f..513ae8ff9c85 100644 --- a/src/client/providers/simpleRefactorProvider.ts +++ b/src/client/providers/simpleRefactorProvider.ts @@ -1,12 +1,12 @@ import * as vscode from 'vscode'; import { PythonSettings } from '../common/configSettings'; import { getTextEditsFromPatch } from '../common/editor'; +import { StopWatch } from '../common/stopWatch'; import { IInstaller, Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { RefactorProxy } from '../refactor/proxy'; import { sendTelemetryWhenDone } from '../telemetry'; import { REFACTOR_EXTRACT_FUNCTION, REFACTOR_EXTRACT_VAR } from '../telemetry/constants'; -import { StopWatch } from '../telemetry/stopWatch'; type RenameResponse = { results: [{ diff: string }]; diff --git a/src/client/providers/symbolProvider.ts b/src/client/providers/symbolProvider.ts index 9c296edda3df..5f279e0f787a 100644 --- a/src/client/providers/symbolProvider.ts +++ b/src/client/providers/symbolProvider.ts @@ -1,48 +1,69 @@ 'use strict'; -import * as vscode from 'vscode'; +import { CancellationToken, DocumentSymbolProvider, Location, Range, SymbolInformation, TextDocument, Uri } from 'vscode'; +import { createDeferred, Deferred } from '../common/helpers'; +import { IFileSystem } from '../common/platform/types'; +import { IServiceContainer } from '../ioc/types'; import { JediFactory } from '../languageServices/jediProxyFactory'; import { captureTelemetry } from '../telemetry'; import { SYMBOL } from '../telemetry/constants'; import * as proxy from './jediProxy'; -export class PythonSymbolProvider implements vscode.DocumentSymbolProvider { - public constructor(private jediFactory: JediFactory) { } - private static parseData(document: vscode.TextDocument, data: proxy.ISymbolResult): vscode.SymbolInformation[] { - if (data) { - const symbols = data.definitions.filter(sym => sym.fileName === document.fileName); - return symbols.map(sym => { - const symbol = sym.kind; - const range = new vscode.Range( - sym.range.startLine, sym.range.startColumn, - sym.range.endLine, sym.range.endColumn); - const uri = vscode.Uri.file(sym.fileName); - const location = new vscode.Location(uri, range); - return new vscode.SymbolInformation(sym.text, symbol, sym.container, location); - }); - } - return []; +export class PythonSymbolProvider implements DocumentSymbolProvider { + private debounceRequest: Map }>; + private readonly fs: IFileSystem; + public constructor(serviceContainer: IServiceContainer, private jediFactory: JediFactory, private readonly debounceTimeoutMs = 500) { + this.debounceRequest = new Map }>(); + this.fs = serviceContainer.get(IFileSystem); } @captureTelemetry(SYMBOL) - public provideDocumentSymbols(document: vscode.TextDocument, token: vscode.CancellationToken): Thenable { - const filename = document.fileName; + public provideDocumentSymbols(document: TextDocument, token: CancellationToken): Thenable { + const key = `${document.uri.fsPath}`; + if (this.debounceRequest.has(key)) { + const item = this.debounceRequest.get(key)!; + clearTimeout(item.timer); + item.deferred.resolve([]); + } - const cmd: proxy.ICommand = { - command: proxy.CommandType.Symbols, - fileName: filename, - columnIndex: 0, - lineIndex: 0 - }; + const deferred = createDeferred(); + const timer = setTimeout(() => { + if (token.isCancellationRequested) { + return deferred.resolve([]); + } - if (document.isDirty) { - cmd.source = document.getText(); - } + const filename = document.fileName; + const cmd: proxy.ICommand = { + command: proxy.CommandType.Symbols, + fileName: filename, + columnIndex: 0, + lineIndex: 0 + }; + + if (document.isDirty) { + cmd.source = document.getText(); + } + + this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token) + .then(data => this.parseData(document, data)) + .then(items => deferred.resolve(items)) + .catch(ex => deferred.reject(ex)); - return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token).then(data => { - return PythonSymbolProvider.parseData(document, data); + }, this.debounceTimeoutMs); + + token.onCancellationRequested(() => { + clearTimeout(timer); + deferred.resolve([]); + this.debounceRequest.delete(key); }); + + // When a document is not saved on FS, we cannot uniquely identify it, so lets not debounce, but delay the symbol provider. + if (!document.isUntitled) { + this.debounceRequest.set(key, { timer, deferred }); + } + + return deferred.promise; } - public provideDocumentSymbolsForInternalUse(document: vscode.TextDocument, token: vscode.CancellationToken): Thenable { + public provideDocumentSymbolsForInternalUse(document: TextDocument, token: CancellationToken): Thenable { const filename = document.fileName; const cmd: proxy.ICommand = { @@ -56,8 +77,22 @@ export class PythonSymbolProvider implements vscode.DocumentSymbolProvider { cmd.source = document.getText(); } - return this.jediFactory.getJediProxyHandler(document.uri).sendCommandNonCancellableCommand(cmd, token).then(data => { - return PythonSymbolProvider.parseData(document, data); - }); + return this.jediFactory.getJediProxyHandler(document.uri).sendCommandNonCancellableCommand(cmd, token) + .then(data => this.parseData(document, data)); + } + private parseData(document: TextDocument, data?: proxy.ISymbolResult): SymbolInformation[] { + if (data) { + const symbols = data.definitions.filter(sym => this.fs.arePathsSame(sym.fileName, document.fileName)); + return symbols.map(sym => { + const symbol = sym.kind; + const range = new Range( + sym.range.startLine, sym.range.startColumn, + sym.range.endLine, sym.range.endColumn); + const uri = Uri.file(sym.fileName); + const location = new Location(uri, range); + return new SymbolInformation(sym.text, symbol, sym.container, location); + }); + } + return []; } } diff --git a/src/client/providers/terminalProvider.ts b/src/client/providers/terminalProvider.ts index 7bfb5bb4d08f..87d60070e328 100644 --- a/src/client/providers/terminalProvider.ts +++ b/src/client/providers/terminalProvider.ts @@ -6,6 +6,8 @@ import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/ import { Commands } from '../common/constants'; import { ITerminalServiceFactory } from '../common/terminal/types'; import { IServiceContainer } from '../ioc/types'; +import { captureTelemetry } from '../telemetry'; +import { TERMINAL_CREATE } from '../telemetry/constants'; export class TerminalProvider implements Disposable { private disposables: Disposable[] = []; @@ -21,10 +23,11 @@ export class TerminalProvider implements Disposable { this.disposables.push(disposable); } + @captureTelemetry(TERMINAL_CREATE, { triggeredBy: 'commandpalette' }) private async onCreateTerminal() { const terminalService = this.serviceContainer.get(ITerminalServiceFactory); const activeResource = this.getActiveResource(); - await terminalService.createTerminalService(activeResource, 'Python').show(); + await terminalService.createTerminalService(activeResource, 'Python').show(false); } private getActiveResource(): Uri | undefined { const documentManager = this.serviceContainer.get(IDocumentManager); diff --git a/src/client/refactor/proxy.ts b/src/client/refactor/proxy.ts index 523b4e726084..d5268d1f931f 100644 --- a/src/client/refactor/proxy.ts +++ b/src/client/refactor/proxy.ts @@ -2,8 +2,7 @@ import { ChildProcess } from 'child_process'; import * as path from 'path'; -import * as vscode from 'vscode'; -import { Uri } from 'vscode'; +import { Disposable, Position, Range, TextDocument, TextEditorOptions, Uri, window } from 'vscode'; import '../common/extensions'; import { createDeferred, Deferred } from '../common/helpers'; import { IPythonExecutionFactory } from '../common/process/types'; @@ -11,15 +10,15 @@ import { IPythonSettings } from '../common/types'; import { getWindowsLineEndingCount, IS_WINDOWS } from '../common/utils'; import { IServiceContainer } from '../ioc/types'; -export class RefactorProxy extends vscode.Disposable { +export class RefactorProxy extends Disposable { private _process?: ChildProcess; private _extensionDir: string; private _previousOutData: string = ''; private _previousStdErrData: string = ''; private _startedSuccessfully: boolean = false; private _commandResolve?: (value?: any | PromiseLike) => void; - private _commandReject: (reason?: any) => void; - private initialized: Deferred; + private _commandReject!: (reason?: any) => void; + private initialized!: Deferred; constructor(extensionDir: string, private pythonSettings: IPythonSettings, private workspaceRoot: string, private serviceContainer: IServiceContainer) { super(() => { }); @@ -33,7 +32,7 @@ export class RefactorProxy extends vscode.Disposable { } this._process = undefined; } - private getOffsetAt(document: vscode.TextDocument, position: vscode.Position): number { + private getOffsetAt(document: TextDocument, position: Position): number { if (!IS_WINDOWS) { return document.offsetAt(position); } @@ -47,9 +46,9 @@ export class RefactorProxy extends vscode.Disposable { return offset - winEols; } - public rename(document: vscode.TextDocument, name: string, filePath: string, range: vscode.Range, options?: vscode.TextEditorOptions): Promise { + public rename(document: TextDocument, name: string, filePath: string, range: Range, options?: TextEditorOptions): Promise { if (!options) { - options = vscode.window.activeTextEditor!.options; + options = window.activeTextEditor!.options; } const command = { lookup: 'rename', @@ -62,9 +61,9 @@ export class RefactorProxy extends vscode.Disposable { return this.sendCommand(JSON.stringify(command)); } - public extractVariable(document: vscode.TextDocument, name: string, filePath: string, range: vscode.Range, options?: vscode.TextEditorOptions): Promise { + public extractVariable(document: TextDocument, name: string, filePath: string, range: Range, options?: TextEditorOptions): Promise { if (!options) { - options = vscode.window.activeTextEditor!.options; + options = window.activeTextEditor!.options; } const command = { lookup: 'extract_variable', @@ -77,9 +76,9 @@ export class RefactorProxy extends vscode.Disposable { }; return this.sendCommand(JSON.stringify(command)); } - public extractMethod(document: vscode.TextDocument, name: string, filePath: string, range: vscode.Range, options?: vscode.TextEditorOptions): Promise { + public extractMethod(document: TextDocument, name: string, filePath: string, range: Range, options?: TextEditorOptions): Promise { if (!options) { - options = vscode.window.activeTextEditor!.options; + options = window.activeTextEditor!.options; } // Ensure last line is an empty line if (!document.lineAt(document.lineCount - 1).isEmptyOrWhitespace && range.start.line === document.lineCount - 1) { @@ -107,7 +106,7 @@ export class RefactorProxy extends vscode.Disposable { }); } private async initialize(pythonPath: string): Promise { - const pythonProc = await this.serviceContainer.get(IPythonExecutionFactory).create(Uri.file(this.workspaceRoot)); + const pythonProc = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource: Uri.file(this.workspaceRoot) }); this.initialized = createDeferred(); const args = ['refactor.py', this.workspaceRoot]; const cwd = path.join(this._extensionDir, 'pythonFiles'); @@ -131,7 +130,7 @@ export class RefactorProxy extends vscode.Disposable { // Possible there was an exception in parsing the data returned // So append the data then parse it let dataStr = this._previousStdErrData = this._previousStdErrData + data + ''; - let errorResponse: { message: string, traceback: string, type: string }[]; + let errorResponse: { message: string; traceback: string; type: string }[]; try { errorResponse = dataStr.split(/\r?\n/g).filter(line => line.length > 0).map(resp => JSON.parse(resp)); this._previousStdErrData = ''; diff --git a/src/client/sortImports.ts b/src/client/sortImports.ts index 53fadc8769a9..b32488ec1ed3 100644 --- a/src/client/sortImports.ts +++ b/src/client/sortImports.ts @@ -1,6 +1,5 @@ import * as os from 'os'; import * as vscode from 'vscode'; -import { IProcessService, IPythonExecutionFactory } from './common/process/types'; import { IServiceContainer } from './ioc/types'; import * as sortProvider from './providers/importSortProvider'; @@ -29,15 +28,14 @@ export function activate(context: vscode.ExtensionContext, outChannel: vscode.Ou }); } return emptyLineAdded.then(() => { - const processService = serviceContainer.get(IProcessService); - const pythonExecutionFactory = serviceContainer.get(IPythonExecutionFactory); - return new sortProvider.PythonImportSortProvider(pythonExecutionFactory, processService).sortImports(rootDir, activeEditor.document); + return new sortProvider.PythonImportSortProvider(serviceContainer).sortImports(rootDir, activeEditor.document); }).then(changes => { if (!changes || changes!.length === 0) { return; } - return new Promise((resolve, reject) => activeEditor.edit(builder => changes.forEach(change => builder.replace(change.range, change.newText))).then(resolve, reject)); + // tslint:disable-next-line:no-any + return new Promise((resolve, reject) => activeEditor.edit(builder => changes.forEach(change => builder.replace(change.range, change.newText))).then(resolve, reject)); }).catch(error => { const message = typeof error === 'string' ? error : (error.message ? error.message : error); outChannel.appendLine(error); diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index bf02b07c63c7..edd932725a08 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -4,6 +4,7 @@ 'use strict'; export const COMPLETION = 'COMPLETION'; +export const COMPLETION_ADD_BRACKETS = 'COMPLETION.ADD_BRACKETS'; export const DEFINITION = 'DEFINITION'; export const HOVER_DEFINITION = 'HOVER_DEFINITION'; export const REFERENCE = 'REFERENCE'; @@ -11,6 +12,7 @@ export const SIGNATURE = 'SIGNATURE'; export const SYMBOL = 'SYMBOL'; export const FORMAT_SORT_IMPORTS = 'FORMAT.SORT_IMPORTS'; export const FORMAT = 'FORMAT.FORMAT'; +export const FORMAT_ON_TYPE = 'FORMAT.FORMAT_ON_TYPE'; export const EDITOR_LOAD = 'EDITOR.LOAD'; export const LINTING = 'LINTING'; export const GO_TO_OBJECT_DEFINITION = 'GO_TO_OBJECT_DEFINITION'; @@ -30,3 +32,8 @@ export const UNITTEST_STOP = 'UNITTEST.STOP'; export const UNITTEST_RUN = 'UNITTEST.RUN'; export const UNITTEST_DISCOVER = 'UNITTEST.DISCOVER'; export const UNITTEST_VIEW_OUTPUT = 'UNITTEST.VIEW_OUTPUT'; +export const PYTHON_LANGUAGE_SERVER_ENABLED = 'PYTHON_LANGUAGE_SERVER.ENABLED'; +export const PYTHON_LANGUAGE_SERVER_DOWNLOADED = 'PYTHON_LANGUAGE_SERVER.DOWNLOADED'; +export const PYTHON_LANGUAGE_SERVER_ERROR = 'PYTHON_LANGUAGE_SERVER.ERROR'; +export const PYTHON_LANGUAGE_SERVER_STARTUP = 'PYTHON_LANGUAGE_SERVER.STARTUP'; +export const TERMINAL_CREATE = 'TERMINAL.CREATE'; diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 559e7b61d99a..adf0952fa7d5 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { StopWatch } from './stopWatch'; +import { StopWatch } from '../common/stopWatch'; import { getTelemetryReporter } from './telemetry'; import { TelemetryProperties } from './types'; diff --git a/src/client/telemetry/types.ts b/src/client/telemetry/types.ts index ed1cedaa1777..7865a5ea4c94 100644 --- a/src/client/telemetry/types.ts +++ b/src/client/telemetry/types.ts @@ -1,13 +1,16 @@ -import { LinterId } from '../linters/types'; - // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { TerminalShellType } from '../common/terminal/types'; +import { InterpreterType } from '../interpreter/contracts'; +import { LinterId } from '../linters/types'; + export type EditorLoadTelemetry = { - condaVersion: string; + condaVersion: string | undefined; + terminal: TerminalShellType; }; export type FormatTelemetry = { - tool: 'autopep8' | 'yapf'; + tool: 'autopep8' | 'black' | 'yapf'; hasCustomArgs: boolean; formatSelection: boolean; }; @@ -30,29 +33,52 @@ export type CodeExecutionTelemetry = { scope: 'file' | 'selection'; }; export type DebuggerTelemetry = { - trigger: 'launch' | 'attach' + trigger: 'launch' | 'attach'; console?: 'none' | 'integratedTerminal' | 'externalTerminal'; debugOptions?: string; pyspark?: boolean; hasEnvVars?: boolean; }; +export type DebuggerTelemetryV2 = { + trigger: 'launch' | 'attach'; + console?: 'none' | 'integratedTerminal' | 'externalTerminal'; + hasEnvVars: boolean; + hasArgs: boolean; + django: boolean; + flask: boolean; + jinja: boolean; + isLocalhost: boolean; + isModule: boolean; + isSudo: boolean; + stopOnEntry: boolean; + pyramid: boolean; +}; export type DebuggerPerformanceTelemetry = { duration: number; action: 'stepIn' | 'stepOut' | 'continue' | 'next' | 'launch'; }; export type TestRunTelemetry = { - tool: 'nosetest' | 'pytest' | 'unittest' + tool: 'nosetest' | 'pytest' | 'unittest'; scope: 'currentFile' | 'all' | 'file' | 'class' | 'function' | 'failed'; debugging: boolean; - trigger: 'ui' | 'codelens' | 'commandpalette' | 'auto'; + triggeredBy: 'ui' | 'codelens' | 'commandpalette' | 'auto'; failed: boolean; }; export type TestDiscoverytTelemetry = { - tool: 'nosetest' | 'pytest' | 'unittest' + tool: 'nosetest' | 'pytest' | 'unittest'; trigger: 'ui' | 'commandpalette'; failed: boolean; }; export type FeedbackTelemetry = { action: 'accepted' | 'dismissed' | 'doNotShowAgain'; }; -export type TelemetryProperties = FormatTelemetry | LintingTelemetry | EditorLoadTelemetry | PythonInterpreterTelemetry | CodeExecutionTelemetry | TestRunTelemetry | TestDiscoverytTelemetry | FeedbackTelemetry; +export type SettingsTelemetry = { + enabled: boolean; +}; +export type TerminalTelemetry = { + terminal?: TerminalShellType; + triggeredBy?: 'commandpalette'; + pythonVersion?: string; + interpreterType?: InterpreterType; +}; +export type TelemetryProperties = FormatTelemetry | LintingTelemetry | EditorLoadTelemetry | PythonInterpreterTelemetry | CodeExecutionTelemetry | TestRunTelemetry | TestDiscoverytTelemetry | FeedbackTelemetry | TerminalTelemetry | DebuggerTelemetryV2 | SettingsTelemetry; diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index 5a6159a50aae..04ccf5e61c19 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -35,6 +35,7 @@ export class CodeExecutionManager implements ICodeExecutionManager { if (!fileToExecute) { return; } + await codeExecutionHelper.saveFileIfDirty(fileToExecute); const executionService = this.serviceContainer.get(ICodeExecutionService, 'standard'); await executionService.executeFile(fileToExecute); } @@ -59,11 +60,11 @@ export class CodeExecutionManager implements ICodeExecutionManager { } const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); const codeToExecute = await codeExecutionHelper.getSelectedTextToExecute(activeEditor!); - const normalizedCode = codeExecutionHelper.normalizeLines(codeToExecute!); + const normalizedCode = await codeExecutionHelper.normalizeLines(codeToExecute!); if (!normalizedCode || normalizedCode.trim().length === 0) { return; } - await executionService.execute(codeToExecute!, activeEditor!.document.uri); + await executionService.execute(normalizedCode, activeEditor!.document.uri); } } diff --git a/src/client/terminals/codeExecution/djangoContext.ts b/src/client/terminals/codeExecution/djangoContext.ts index 00a5dd8e437b..d6f8755d447b 100644 --- a/src/client/terminals/codeExecution/djangoContext.ts +++ b/src/client/terminals/codeExecution/djangoContext.ts @@ -54,7 +54,7 @@ export class DjangoContextInitializer implements Disposable { private async ensureContextStateIsSet(): Promise { const activeWorkspace = this.getActiveWorkspace(); if (!activeWorkspace) { - return await this.isDjangoProject.set(false); + return this.isDjangoProject.set(false); } if (this.lastCheckedWorkspace === activeWorkspace) { return; @@ -62,7 +62,7 @@ export class DjangoContextInitializer implements Disposable { if (this.workspaceContextKeyValues.has(activeWorkspace)) { await this.isDjangoProject.set(this.workspaceContextKeyValues.get(activeWorkspace)!); } else { - const exists = await this.fileSystem.fileExistsAsync(path.join(activeWorkspace, 'manage.py')); + const exists = await this.fileSystem.fileExists(path.join(activeWorkspace, 'manage.py')); await this.isDjangoProject.set(exists); this.workspaceContextKeyValues.set(activeWorkspace, exists); this.lastCheckedWorkspace = activeWorkspace; diff --git a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts index 5fbe2ef2d19f..3066ec27fb71 100644 --- a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts +++ b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts @@ -10,14 +10,13 @@ import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../comm import '../../common/extensions'; import { IFileSystem, IPlatformService } from '../../common/platform/types'; import { ITerminalServiceFactory } from '../../common/terminal/types'; -import { IConfigurationService } from '../../common/types'; -import { IDisposableRegistry } from '../../common/types'; +import { IConfigurationService, IDisposableRegistry } from '../../common/types'; import { DjangoContextInitializer } from './djangoContext'; import { TerminalCodeExecutionProvider } from './terminalCodeExecution'; @injectable() export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvider { - constructor( @inject(ITerminalServiceFactory) terminalServiceFactory: ITerminalServiceFactory, + constructor(@inject(ITerminalServiceFactory) terminalServiceFactory: ITerminalServiceFactory, @inject(IConfigurationService) configurationService: IConfigurationService, @inject(IWorkspaceService) workspace: IWorkspaceService, @inject(IDocumentManager) documentManager: IDocumentManager, @@ -30,7 +29,7 @@ export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvi this.terminalTitle = 'Django Shell'; disposableRegistry.push(new DjangoContextInitializer(documentManager, workspace, fileSystem, commandManager)); } - public getReplCommandArgs(resource?: Uri): { command: string, args: string[] } { + public getReplCommandArgs(resource?: Uri): { command: string; args: string[] } { const pythonSettings = this.configurationService.getSettings(resource); const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath; const args = pythonSettings.terminal.launchArgs.slice(); diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts index efa468dcbc95..94380e8f247c 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -2,23 +2,42 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { EOL } from 'os'; +import * as path from 'path'; import { Range, TextEditor, Uri } from 'vscode'; import { IApplicationShell, IDocumentManager } from '../../common/application/types'; -import { PythonLanguage } from '../../common/constants'; +import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../common/constants'; import '../../common/extensions'; +import { IProcessServiceFactory } from '../../common/process/types'; +import { IConfigurationService } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; import { ICodeExecutionHelper } from '../types'; @injectable() export class CodeExecutionHelper implements ICodeExecutionHelper { - constructor( @inject(IDocumentManager) private documentManager: IDocumentManager, - @inject(IApplicationShell) private applicationShell: IApplicationShell) { - + private readonly documentManager: IDocumentManager; + private readonly applicationShell: IApplicationShell; + private readonly processServiceFactory: IProcessServiceFactory; + private readonly configurationService: IConfigurationService; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.documentManager = serviceContainer.get(IDocumentManager); + this.applicationShell = serviceContainer.get(IApplicationShell); + this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); + this.configurationService = serviceContainer.get(IConfigurationService); } - public normalizeLines(code: string): string { - const codeLines = code.splitLines({ trim: false, removeEmptyEntries: false }); - const codeLinesWithoutEmptyLines = codeLines.filter(line => line.trim().length > 0); - return codeLinesWithoutEmptyLines.join(EOL); + public async normalizeLines(code: string, resource?: Uri): Promise { + try { + if (code.trim().length === 0) { + return ''; + } + const pythonPath = this.configurationService.getSettings(resource).pythonPath; + const args = [path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'normalizeForInterpreter.py'), code]; + const processService = await this.processServiceFactory.create(resource); + const proc = await processService.exec(pythonPath, args, { throwOnStdErr: true }); + return proc.stdout; + } catch (ex) { + console.error(ex, 'Python: Failed to normalize code for execution in terminal'); + return code; + } } public async getFileToExecute(): Promise { @@ -31,10 +50,13 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { this.applicationShell.showErrorMessage('The active file needs to be saved before it can be run'); return; } - if (activeEditor.document.languageId !== PythonLanguage.language) { + if (activeEditor.document.languageId !== PYTHON_LANGUAGE) { this.applicationShell.showErrorMessage('The active file is not a Python source file'); return; } + if (activeEditor.document.isDirty) { + await activeEditor.document.save(); + } return activeEditor.document.uri; } @@ -53,4 +75,10 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { } return code; } + public async saveFileIfDirty(file: Uri): Promise { + const docs = this.documentManager.textDocuments.filter(d => d.uri.path === file.path); + if (docs.length === 1 && docs[0].isDirty) { + await docs[0].save(); + } + } } diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index d6ab0601c885..7e541ee12126 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -10,16 +10,15 @@ import { IWorkspaceService } from '../../common/application/types'; import '../../common/extensions'; import { IPlatformService } from '../../common/platform/types'; import { ITerminalService, ITerminalServiceFactory } from '../../common/terminal/types'; -import { IConfigurationService } from '../../common/types'; -import { IDisposableRegistry } from '../../common/types'; +import { IConfigurationService, IDisposableRegistry } from '../../common/types'; import { ICodeExecutionService } from '../../terminals/types'; @injectable() export class TerminalCodeExecutionProvider implements ICodeExecutionService { - protected terminalTitle: string; - private _terminalService: ITerminalService; + protected terminalTitle!: string; + private _terminalService!: ITerminalService; private replActive?: Promise; - constructor( @inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, + constructor(@inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, @inject(IConfigurationService) protected readonly configurationService: IConfigurationService, @inject(IWorkspaceService) protected readonly workspace: IWorkspaceService, @inject(IDisposableRegistry) protected readonly disposables: Disposable[], @@ -60,7 +59,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { await this.replActive; } - public getReplCommandArgs(resource?: Uri): { command: string, args: string[] } { + public getReplCommandArgs(resource?: Uri): { command: string; args: string[] } { const pythonSettings = this.configurationService.getSettings(resource); const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath; const args = pythonSettings.terminal.launchArgs.slice(); diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index 67c50d4b887b..cd9e1bc96a9b 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -14,8 +14,9 @@ export interface ICodeExecutionService { export const ICodeExecutionHelper = Symbol('ICodeExecutionHelper'); export interface ICodeExecutionHelper { - normalizeLines(code: string): string; + normalizeLines(code: string): Promise; getFileToExecute(): Promise; + saveFileIfDirty(file: Uri): Promise; getSelectedTextToExecute(textEditor: TextEditor): Promise; } diff --git a/src/client/typeFormatters/onEnterFormatter.ts b/src/client/typeFormatters/onEnterFormatter.ts index 013b2d2a85f9..3e17e714d6ee 100644 --- a/src/client/typeFormatters/onEnterFormatter.ts +++ b/src/client/typeFormatters/onEnterFormatter.ts @@ -1,20 +1,20 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as vscode from 'vscode'; +import { CancellationToken, FormattingOptions, OnTypeFormattingEditProvider, Position, TextDocument, TextEdit } from 'vscode'; import { LineFormatter } from '../formatters/lineFormatter'; import { TokenizerMode, TokenType } from '../language/types'; import { getDocumentTokens } from '../providers/providerUtilities'; -export class OnEnterFormatter implements vscode.OnTypeFormattingEditProvider { +export class OnEnterFormatter implements OnTypeFormattingEditProvider { private readonly formatter = new LineFormatter(); public provideOnTypeFormattingEdits( - document: vscode.TextDocument, - position: vscode.Position, + document: TextDocument, + position: Position, ch: string, - options: vscode.FormattingOptions, - cancellationToken: vscode.CancellationToken): vscode.TextEdit[] { + options: FormattingOptions, + cancellationToken: CancellationToken): TextEdit[] { if (position.line === 0) { return []; } @@ -30,10 +30,10 @@ export class OnEnterFormatter implements vscode.OnTypeFormattingEditProvider { return []; } } - const formatted = this.formatter.formatLine(prevLine.text); + const formatted = this.formatter.formatLine(document, prevLine.lineNumber); if (formatted === prevLine.text) { return []; } - return [new vscode.TextEdit(prevLine.range, formatted)]; + return [new TextEdit(prevLine.range, formatted)]; } } diff --git a/src/client/unittests/codeLenses/main.ts b/src/client/unittests/codeLenses/main.ts index 3efd30091227..654152ca4e58 100644 --- a/src/client/unittests/codeLenses/main.ts +++ b/src/client/unittests/codeLenses/main.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import * as constants from '../../common/constants'; +import { PYTHON } from '../../common/constants'; import { PythonSymbolProvider } from '../../providers/symbolProvider'; import { ITestCollectionStorageService } from '../common/types'; import { TestFileCodeLensProvider } from './testFiles'; @@ -9,7 +9,7 @@ export function activateCodeLenses(onDidChange: vscode.EventEmitter, const disposables: vscode.Disposable[] = []; const codeLensProvider = new TestFileCodeLensProvider(onDidChange, symboldProvider, testCollectionStorage); - disposables.push(vscode.languages.registerCodeLensProvider(constants.PythonLanguage, codeLensProvider)); + disposables.push(vscode.languages.registerCodeLensProvider(PYTHON, codeLensProvider)); return { dispose: () => { disposables.forEach(d => d.dispose()); } diff --git a/src/client/unittests/codeLenses/testFiles.ts b/src/client/unittests/codeLenses/testFiles.ts index 10f115148594..90b7367140f1 100644 --- a/src/client/unittests/codeLenses/testFiles.ts +++ b/src/client/unittests/codeLenses/testFiles.ts @@ -1,7 +1,8 @@ 'use strict'; -import { CancellationToken, CancellationTokenSource, CodeLens, CodeLensProvider, Event, EventEmitter, Position, Range, SymbolInformation, SymbolKind, TextDocument, workspace } from 'vscode'; -import { Uri } from 'vscode'; +// tslint:disable:no-object-literal-type-assertion + +import { CancellationToken, CancellationTokenSource, CodeLens, CodeLensProvider, Event, EventEmitter, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri, workspace } from 'vscode'; import * as constants from '../../common/constants'; import { PythonSymbolProvider } from '../../providers/symbolProvider'; import { CommandSource } from '../common/constants'; @@ -108,12 +109,12 @@ export class TestFileCodeLensProvider implements CodeLensProvider { new CodeLens(range, { title: getTestStatusIcon(cls.status) + constants.Text.CodeLensRunUnitTest, command: constants.Commands.Tests_Run, - arguments: [CommandSource.codelens, file, { testSuite: [cls] }] + arguments: [undefined, CommandSource.codelens, file, { testSuite: [cls] }] }), new CodeLens(range, { title: getTestStatusIcon(cls.status) + constants.Text.CodeLensDebugUnitTest, command: constants.Commands.Tests_Debug, - arguments: [CommandSource.codelens, file, { testSuite: [cls] }] + arguments: [undefined, CommandSource.codelens, file, { testSuite: [cls] }] }) ]; } @@ -182,12 +183,12 @@ function getFunctionCodeLens(file: Uri, functionsAndSuites: FunctionsAndSuites, new CodeLens(range, { title: getTestStatusIcon(fn.status) + constants.Text.CodeLensRunUnitTest, command: constants.Commands.Tests_Run, - arguments: [CommandSource.codelens, file, { testFunction: [fn] }] + arguments: [undefined, CommandSource.codelens, file, { testFunction: [fn] }] }), new CodeLens(range, { title: getTestStatusIcon(fn.status) + constants.Text.CodeLensDebugUnitTest, command: constants.Commands.Tests_Debug, - arguments: [CommandSource.codelens, file, { testFunction: [fn] }] + arguments: [undefined, CommandSource.codelens, file, { testFunction: [fn] }] }) ]; } @@ -203,12 +204,12 @@ function getFunctionCodeLens(file: Uri, functionsAndSuites: FunctionsAndSuites, new CodeLens(range, { title: constants.Text.CodeLensRunUnitTest, command: constants.Commands.Tests_Run, - arguments: [CommandSource.codelens, file, { testFunction: functions }] + arguments: [undefined, CommandSource.codelens, file, { testFunction: functions }] }), new CodeLens(range, { title: constants.Text.CodeLensDebugUnitTest, command: constants.Commands.Tests_Debug, - arguments: [CommandSource.codelens, file, { testFunction: functions }] + arguments: [undefined, CommandSource.codelens, file, { testFunction: functions }] }) ]; } @@ -218,12 +219,12 @@ function getFunctionCodeLens(file: Uri, functionsAndSuites: FunctionsAndSuites, new CodeLens(range, { title: `${getTestStatusIcons(functions)}${constants.Text.CodeLensRunUnitTest} (Multiple)`, command: constants.Commands.Tests_Picker_UI, - arguments: [CommandSource.codelens, file, functions] + arguments: [undefined, CommandSource.codelens, file, functions] }), new CodeLens(range, { title: `${getTestStatusIcons(functions)}${constants.Text.CodeLensDebugUnitTest} (Multiple)`, command: constants.Commands.Tests_Picker_UI_Debug, - arguments: [CommandSource.codelens, file, functions] + arguments: [undefined, CommandSource.codelens, file, functions] }) ]; } diff --git a/src/client/unittests/common/argumentsHelper.ts b/src/client/unittests/common/argumentsHelper.ts new file mode 100644 index 000000000000..3842a9a6874e --- /dev/null +++ b/src/client/unittests/common/argumentsHelper.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { ILogger } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { IArgumentsHelper } from '../types'; + +@injectable() +export class ArgumentsHelper implements IArgumentsHelper { + private readonly logger: ILogger; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.logger = serviceContainer.get(ILogger); + } + public getOptionValues(args: string[], option: string): string | string[] | undefined { + const values: string[] = []; + let returnNextValue = false; + for (const arg of args) { + if (returnNextValue) { + values.push(arg); + returnNextValue = false; + continue; + } + if (arg.startsWith(`${option}=`)) { + values.push(arg.substring(`${option}=`.length)); + continue; + } + if (arg === option) { + returnNextValue = true; + } + } + switch (values.length) { + case 0: { + return; + } + case 1: { + return values[0]; + } + default: { + return values; + } + } + } + public getPositionalArguments(args: string[], optionsWithArguments: string[] = [], optionsWithoutArguments: string[] = []): string[] { + let lastIndexOfOption = -1; + args.forEach((arg, index) => { + if (optionsWithoutArguments.indexOf(arg) !== -1) { + lastIndexOfOption = index; + return; + } else if (optionsWithArguments.indexOf(arg) !== -1) { + // Cuz the next item is the value. + lastIndexOfOption = index + 1; + } else if (optionsWithArguments.findIndex(item => arg.startsWith(`${item}=`)) !== -1) { + lastIndexOfOption = index; + return; + } else if (arg.startsWith('-')) { + // Ok this is an unknown option, lets treat this as one without values. + this.logger.logWarning(`Unknown command line option passed into args parser for tests '${arg}'. Please report on https://github.com/Microsoft/vscode-python/issues/new`); + lastIndexOfOption = index; + return; + } else if (args.indexOf('=') > 0) { + // Ok this is an unknown option with a value + this.logger.logWarning(`Unknown command line option passed into args parser for tests '${arg}'. Please report on https://github.com/Microsoft/vscode-python/issues/new`); + lastIndexOfOption = index; + } + }); + return args.slice(lastIndexOfOption + 1); + } + public filterArguments(args: string[], optionsWithArguments: string[] = [], optionsWithoutArguments: string[] = []): string[] { + let ignoreIndex = -1; + return args.filter((arg, index) => { + if (ignoreIndex === index) { + return false; + } + // Options can use willd cards (with trailing '*') + if (optionsWithoutArguments.indexOf(arg) >= 0 || + optionsWithoutArguments.filter(option => option.endsWith('*') && arg.startsWith(option.slice(0, -1))).length > 0) { + return false; + } + // Ignore args that match exactly. + if (optionsWithArguments.indexOf(arg) >= 0) { + ignoreIndex = index + 1; + return false; + } + // Ignore args that match exactly with wild cards & do not have inline values. + if (optionsWithArguments.filter(option => arg.startsWith(`${option}=`)).length > 0) { + return false; + } + // Ignore args that match a wild card (ending with *) and no ineline values. + // Eg. arg='--log-cli-level' and optionsArguments=['--log-*'] + if (arg.indexOf('=') === -1 && optionsWithoutArguments.filter(option => option.endsWith('*') && arg.startsWith(option.slice(0, -1))).length > 0) { + ignoreIndex = index + 1; + return false; + } + // Ignore args that match a wild card (ending with *) and have ineline values. + // Eg. arg='--log-cli-level=XYZ' and optionsArguments=['--log-*'] + if (arg.indexOf('=') >= 0 && optionsWithoutArguments.filter(option => option.endsWith('*') && arg.startsWith(option.slice(0, -1))).length > 0) { + return false; + } + return true; + }); + } +} diff --git a/src/client/unittests/common/debugLauncher.ts b/src/client/unittests/common/debugLauncher.ts index 6b2a513d4502..934ab9e6b2e0 100644 --- a/src/client/unittests/common/debugLauncher.ts +++ b/src/client/unittests/common/debugLauncher.ts @@ -1,33 +1,74 @@ -import { injectable } from 'inversify'; -import { debug, Uri, workspace } from 'vscode'; -import { ITestDebugLauncher, launchOptions } from './types'; +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IDebugService, IWorkspaceService } from '../../common/application/types'; +import { EXTENSION_ROOT_DIR } from '../../common/constants'; +import { IConfigurationService } from '../../common/types'; +import { ExperimentalDebuggerType } from '../../debugger/Common/constants'; +import { DebugOptions } from '../../debugger/Common/Contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { ITestDebugLauncher, LaunchOptions, TestProvider } from './types'; @injectable() export class DebugLauncher implements ITestDebugLauncher { - public async launchDebugger(options: launchOptions) { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } + public async launchDebugger(options: LaunchOptions) { if (options.token && options.token!.isCancellationRequested) { return; } const cwdUri = options.cwd ? Uri.file(options.cwd) : undefined; - - if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + const workspaceService = this.serviceContainer.get(IWorkspaceService); + if (!workspaceService.hasWorkspaceFolders) { throw new Error('Please open a workspace'); } - let workspaceFolder = workspace.getWorkspaceFolder(cwdUri!); + let workspaceFolder = workspaceService.getWorkspaceFolder(cwdUri!); if (!workspaceFolder) { - workspaceFolder = workspace.workspaceFolders[0]; + workspaceFolder = workspaceService.workspaceFolders![0]; } - const args = options.args.slice(); - const program = args.shift(); - return debug.startDebugging(workspaceFolder, { + + const cwd = cwdUri ? cwdUri.fsPath : workspaceFolder.uri.fsPath; + const configSettings = this.serviceContainer.get(IConfigurationService).getSettings(Uri.file(cwd)); + const useExperimentalDebugger = configSettings.unitTest.useExperimentalDebugger === true; + const debugManager = this.serviceContainer.get(IDebugService); + const debuggerType = useExperimentalDebugger ? ExperimentalDebuggerType : 'python'; + const debugArgs = this.fixArgs(options.args, options.testProvider, useExperimentalDebugger); + const program = this.getTestLauncherScript(options.testProvider, useExperimentalDebugger); + return debugManager.startDebugging(workspaceFolder, { name: 'Debug Unit Test', - type: 'python', + type: debuggerType, request: 'launch', program, - cwd: cwdUri ? cwdUri.fsPath : workspaceFolder.uri.fsPath, - args, + cwd, + args: debugArgs, console: 'none', - debugOptions: ['RedirectOutput'] + envFile: configSettings.envFile, + debugOptions: [DebugOptions.RedirectOutput] }).then(() => void (0)); } + private fixArgs(args: string[], testProvider: TestProvider, useExperimentalDebugger: boolean): string[] { + if (testProvider === 'unittest' && useExperimentalDebugger) { + return args.filter(item => item !== '--debug'); + } else { + return args; + } + } + private getTestLauncherScript(testProvider: TestProvider, useExperimentalDebugger: boolean) { + switch (testProvider) { + case 'unittest': { + return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'PythonTools', 'visualstudio_py_testlauncher.py'); + } + case 'pytest': + case 'nosetest': { + if (useExperimentalDebugger) { + return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'testlauncher.py'); + } else { + return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'PythonTools', 'testlauncher.py'); + } + + } + default: { + throw new Error(`Unknown test provider '${testProvider}'`); + } + } + } } diff --git a/src/client/unittests/common/managers/baseTestManager.ts b/src/client/unittests/common/managers/baseTestManager.ts index 7ee714e718ad..89a54b12e40e 100644 --- a/src/client/unittests/common/managers/baseTestManager.ts +++ b/src/client/unittests/common/managers/baseTestManager.ts @@ -1,17 +1,13 @@ -import * as vscode from 'vscode'; -import { Disposable, OutputChannel, Uri, workspace } from 'vscode'; -import { PythonSettings } from '../../../common/configSettings'; +import { CancellationToken, CancellationTokenSource, Disposable, OutputChannel, Uri } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; import { isNotInstalledError } from '../../../common/helpers'; -import { IPythonSettings } from '../../../common/types'; -import { IDisposableRegistry, IInstaller, IOutputChannel, Product } from '../../../common/types'; +import { IConfigurationService, IDisposableRegistry, IInstaller, IOutputChannel, IPythonSettings, Product } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { UNITTEST_DISCOVER, UNITTEST_RUN } from '../../../telemetry/constants'; import { sendTelemetryEvent } from '../../../telemetry/index'; import { TestDiscoverytTelemetry, TestRunTelemetry } from '../../../telemetry/types'; import { CANCELLATION_REASON, CommandSource, TEST_OUTPUT_CHANNEL } from './../constants'; -import { displayTestErrorMessage } from './../testUtils'; -import { ITestCollectionStorageService, ITestDiscoveryService, ITestManager, ITestResultsService } from './../types'; -import { TestDiscoveryOptions, TestProvider, Tests, TestStatus, TestsToRun } from './../types'; +import { ITestCollectionStorageService, ITestDiscoveryService, ITestManager, ITestResultsService, ITestsHelper, TestDiscoveryOptions, TestProvider, Tests, TestStatus, TestsToRun } from './../types'; enum CancellationTokenType { testDiscovery, @@ -29,13 +25,13 @@ export abstract class BaseTestManager implements ITestManager { } private testCollectionStorage: ITestCollectionStorageService; private _testResultsService: ITestResultsService; + private workspaceService: IWorkspaceService; private _outputChannel: OutputChannel; private tests?: Tests; - // tslint:disable-next-line:variable-name private _status: TestStatus = TestStatus.Unknown; - private testDiscoveryCancellationTokenSource?: vscode.CancellationTokenSource; - private testRunnerCancellationTokenSource?: vscode.CancellationTokenSource; - private _installer: IInstaller; + private testDiscoveryCancellationTokenSource?: CancellationTokenSource; + private testRunnerCancellationTokenSource?: CancellationTokenSource; + private _installer!: IInstaller; private discoverTestsPromise?: Promise; private get installer(): IInstaller { if (!this._installer) { @@ -46,17 +42,19 @@ export abstract class BaseTestManager implements ITestManager { constructor(public readonly testProvider: TestProvider, private product: Product, public readonly workspaceFolder: Uri, protected rootDirectory: string, protected serviceContainer: IServiceContainer) { this._status = TestStatus.Unknown; - this.settings = PythonSettings.getInstance(this.rootDirectory ? Uri.file(this.rootDirectory) : undefined); + const configService = serviceContainer.get(IConfigurationService); + this.settings = configService.getSettings(this.rootDirectory ? Uri.file(this.rootDirectory) : undefined); const disposables = serviceContainer.get(IDisposableRegistry); - disposables.push(this); this._outputChannel = this.serviceContainer.get(IOutputChannel, TEST_OUTPUT_CHANNEL); this.testCollectionStorage = this.serviceContainer.get(ITestCollectionStorageService); this._testResultsService = this.serviceContainer.get(ITestResultsService); + this.workspaceService = this.serviceContainer.get(IWorkspaceService); + disposables.push(this); } - protected get testDiscoveryCancellationToken(): vscode.CancellationToken | undefined { + protected get testDiscoveryCancellationToken(): CancellationToken | undefined { return this.testDiscoveryCancellationTokenSource ? this.testDiscoveryCancellationTokenSource.token : undefined; } - protected get testRunnerCancellationToken(): vscode.CancellationToken | undefined { + protected get testRunnerCancellationToken(): CancellationToken | undefined { return this.testRunnerCancellationTokenSource ? this.testRunnerCancellationTokenSource.token : undefined; } public dispose() { @@ -66,8 +64,7 @@ export abstract class BaseTestManager implements ITestManager { 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; + return this.settings.unitTest.cwd && this.settings.unitTest.cwd.length > 0 ? this.settings.unitTest.cwd : this.rootDirectory; } public stop() { if (this.testDiscoveryCancellationTokenSource) { @@ -133,9 +130,10 @@ export abstract class BaseTestManager implements ITestManager { } }); if (haveErrorsInDiscovering && !quietMode) { - displayTestErrorMessage('There were some errors in discovering unit tests'); + const testsHelper = this.serviceContainer.get(ITestsHelper); + testsHelper.displayTestErrorMessage('There were some errors in discovering unit tests'); } - const wkspace = workspace.getWorkspaceFolder(vscode.Uri.file(this.rootDirectory))!.uri; + const wkspace = this.workspaceService.getWorkspaceFolder(Uri.file(this.rootDirectory))!.uri; this.testCollectionStorage.storeTests(wkspace, tests); this.disposeCancellationToken(CancellationTokenType.testDiscovery); sendTelemetryEvent(UNITTEST_DISCOVER, undefined, telementryProperties); @@ -159,7 +157,7 @@ export abstract class BaseTestManager implements ITestManager { // tslint:disable-next-line:prefer-template this.outputChannel.appendLine(reason.toString()); } - const wkspace = workspace.getWorkspaceFolder(vscode.Uri.file(this.rootDirectory))!.uri; + const wkspace = this.workspaceService.getWorkspaceFolder(Uri.file(this.rootDirectory))!.uri; this.testCollectionStorage.storeTests(wkspace, null); this.disposeCancellationToken(CancellationTokenType.testDiscovery); return Promise.reject(reason); @@ -177,7 +175,7 @@ export abstract class BaseTestManager implements ITestManager { tool: this.testProvider, scope: 'all', debugging: debug === true, - trigger: cmdSource, + triggeredBy: cmdSource, failed: false }; if (runFailedTests === true) { @@ -217,8 +215,9 @@ export abstract class BaseTestManager implements ITestManager { if (this.testDiscoveryCancellationToken && this.testDiscoveryCancellationToken.isCancellationRequested) { return Promise.reject(reason); } - displayTestErrorMessage('Errors in discovering tests, continuing with tests'); - return { + const testsHelper = this.serviceContainer.get(ITestsHelper); + testsHelper.displayTestErrorMessage('Errors in discovering tests, continuing with tests'); + return { rootTestFolders: [], testFiles: [], testFolders: [], testFunctions: [], testSuites: [], summary: { errors: 0, failures: 0, passed: 0, skipped: 0 } }; @@ -250,9 +249,9 @@ export abstract class BaseTestManager implements ITestManager { private createCancellationToken(tokenType: CancellationTokenType) { this.disposeCancellationToken(tokenType); if (tokenType === CancellationTokenType.testDiscovery) { - this.testDiscoveryCancellationTokenSource = new vscode.CancellationTokenSource(); + this.testDiscoveryCancellationTokenSource = new CancellationTokenSource(); } else { - this.testRunnerCancellationTokenSource = new vscode.CancellationTokenSource(); + this.testRunnerCancellationTokenSource = new CancellationTokenSource(); } } private disposeCancellationToken(tokenType: CancellationTokenType) { diff --git a/src/client/unittests/common/managers/testConfigurationManager.ts b/src/client/unittests/common/managers/testConfigurationManager.ts index 38586977016f..b6351c6bb335 100644 --- a/src/client/unittests/common/managers/testConfigurationManager.ts +++ b/src/client/unittests/common/managers/testConfigurationManager.ts @@ -1,39 +1,54 @@ import * as path from 'path'; -import * as vscode from 'vscode'; -import { Uri } from 'vscode'; +import { OutputChannel, QuickPickItem, Uri, window } from 'vscode'; import { createDeferred } from '../../../common/helpers'; -import { IInstaller } from '../../../common/types'; +import { IInstaller, IOutputChannel, Product } from '../../../common/types'; import { getSubDirectories } from '../../../common/utils'; +import { IServiceContainer } from '../../../ioc/types'; +import { ITestConfigurationManager } from '../../types'; +import { TEST_OUTPUT_CHANNEL } from '../constants'; import { ITestConfigSettingsService, UnitTestProduct } from './../types'; -export abstract class TestConfigurationManager { +export abstract class TestConfigurationManager implements ITestConfigurationManager { + protected readonly outputChannel: OutputChannel; + protected readonly installer: IInstaller; + protected readonly testConfigSettingsService: ITestConfigSettingsService; constructor(protected workspace: Uri, protected product: UnitTestProduct, - protected readonly outputChannel: vscode.OutputChannel, - protected installer: IInstaller, - protected testConfigSettingsService: ITestConfigSettingsService) { } - // tslint:disable-next-line:no-any - public abstract configure(wkspace: Uri): Promise; + protected readonly serviceContainer: IServiceContainer) { + this.outputChannel = serviceContainer.get(IOutputChannel, TEST_OUTPUT_CHANNEL); + this.installer = serviceContainer.get(IInstaller); + this.testConfigSettingsService = serviceContainer.get(ITestConfigSettingsService); + } + public abstract configure(wkspace: Uri): Promise; + public abstract requiresUserToConfigure(wkspace: Uri): Promise; public async enable() { + // Disable other test frameworks. + const testProducsToDisable = [Product.pytest, Product.unittest, Product.nosetest] + .filter(item => item !== this.product) as UnitTestProduct[]; + + for (const prod of testProducsToDisable) { + await this.testConfigSettingsService.disable(this.workspace, prod); + } + 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 { + protected selectTestDir(rootDir: string, subDirs: string[], customOptions: QuickPickItem[] = []): Promise { const options = { matchOnDescription: true, matchOnDetail: true, placeHolder: 'Select the directory containing the unit tests' }; - let items: vscode.QuickPickItem[] = subDirs + let items: QuickPickItem[] = subDirs .map(dir => { const dirName = path.relative(rootDir, dir); if (dirName.indexOf('.') === 0) { return; } - return { + return { label: dirName, description: '' }; @@ -44,7 +59,7 @@ export abstract class TestConfigurationManager { items = [{ label: '.', description: 'Root directory' }, ...items]; items = customOptions.concat(items); const def = createDeferred(); - vscode.window.showQuickPick(items, options).then(item => { + window.showQuickPick(items, options).then(item => { if (!item) { return def.resolve(); } @@ -61,7 +76,7 @@ export abstract class TestConfigurationManager { matchOnDetail: true, placeHolder: 'Select the pattern to identify test files' }; - const items: vscode.QuickPickItem[] = [ + const items: 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\'' }, @@ -70,7 +85,7 @@ export abstract class TestConfigurationManager { ]; const def = createDeferred(); - vscode.window.showQuickPick(items, options).then(item => { + window.showQuickPick(items, options).then(item => { if (!item) { return def.resolve(); } diff --git a/src/client/unittests/common/runner.ts b/src/client/unittests/common/runner.ts index 56f7fba07eab..a8b1d216423e 100644 --- a/src/client/unittests/common/runner.ts +++ b/src/client/unittests/common/runner.ts @@ -1,28 +1,29 @@ +import { inject, injectable } from 'inversify'; import * as path from 'path'; import { CancellationToken, OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { ErrorUtils } from '../../common/errors/errorUtils'; import { ModuleNotInstalledError } from '../../common/errors/moduleNotInstalledError'; -import { IPythonToolExecutionService } from '../../common/process/types'; import { IPythonExecutionFactory, IPythonExecutionService, + IPythonToolExecutionService, ObservableExecutionResult, SpawnOptions } from '../../common/process/types'; -import { IPythonSettings } from '../../common/types'; -import { ExecutionInfo } from '../../common/types'; +import { ExecutionInfo, IPythonSettings } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from './constants'; -import { ITestsHelper, TestProvider } from './types'; +import { ITestRunner, ITestsHelper, Options, TestProvider } from './types'; +export { Options } from './types'; -export type Options = { - workspaceFolder: Uri; - cwd: string; - args: string[]; - outChannel?: OutputChannel; - token: CancellationToken; -}; +@injectable() +export class TestRunner implements ITestRunner { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } + public run(testProvider: TestProvider, options: Options): Promise { + return run(this.serviceContainer, testProvider, options); + } +} export async function run(serviceContainer: IServiceContainer, testProvider: TestProvider, options: Options): Promise { const testExecutablePath = getExecutablePath(testProvider, PythonSettings.getInstance(options.workspaceFolder)); @@ -36,7 +37,7 @@ export async function run(serviceContainer: IServiceContainer, testProvider: Tes if (!testExecutablePath && testProvider === UNITTEST_PROVIDER) { // Unit tests have a special way of being executed const pythonServiceFactory = serviceContainer.get(IPythonExecutionFactory); - pythonExecutionServicePromise = pythonServiceFactory.create(options.workspaceFolder); + pythonExecutionServicePromise = pythonServiceFactory.create({ resource: options.workspaceFolder }); promise = pythonExecutionServicePromise.then(executionService => executionService.execObservable(options.args, { ...spawnOptions })); } else { const pythonToolsExecutionService = serviceContainer.get(IPythonToolExecutionService); diff --git a/src/client/unittests/common/services/configSettingService.ts b/src/client/unittests/common/services/configSettingService.ts index d7dd923e9f10..483ffa35058b 100644 --- a/src/client/unittests/common/services/configSettingService.ts +++ b/src/client/unittests/common/services/configSettingService.ts @@ -1,9 +1,31 @@ -import { Uri, workspace, WorkspaceConfiguration } from 'vscode'; +import { inject, injectable } from 'inversify'; +import { Uri, WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; import { Product } from '../../../common/types'; +import { IServiceContainer } from '../../../ioc/types'; import { ITestConfigSettingsService, UnitTestProduct } from './../types'; +@injectable() export class TestConfigSettingsService implements ITestConfigSettingsService { - private static getTestArgSetting(product: UnitTestProduct) { + private readonly workspaceService: IWorkspaceService; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.workspaceService = serviceContainer.get(IWorkspaceService); + } + public async updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]) { + const setting = this.getTestArgSetting(product); + return this.updateSetting(testDirectory, setting, args); + } + + public async enable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + const setting = this.getTestEnablingSetting(product); + return this.updateSetting(testDirectory, setting, true); + } + + public async disable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + const setting = this.getTestEnablingSetting(product); + return this.updateSetting(testDirectory, setting, false); + } + private getTestArgSetting(product: UnitTestProduct) { switch (product) { case Product.unittest: return 'unitTest.unittestArgs'; @@ -15,7 +37,7 @@ export class TestConfigSettingsService implements ITestConfigSettingsService { throw new Error('Invalid Test Product'); } } - private static getTestEnablingSetting(product: UnitTestProduct) { + private getTestEnablingSetting(product: UnitTestProduct) { switch (product) { case Product.unittest: return 'unitTest.unittestEnabled'; @@ -28,36 +50,22 @@ export class TestConfigSettingsService implements ITestConfigSettingsService { } } // tslint:disable-next-line:no-any - private static async updateSetting(testDirectory: string | Uri, setting: string, value: any) { + private async updateSetting(testDirectory: string | Uri, setting: string, value: any) { let pythonConfig: WorkspaceConfiguration; const resource = typeof testDirectory === 'string' ? Uri.file(testDirectory) : testDirectory; - if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { - pythonConfig = workspace.getConfiguration('python'); - } else if (workspace.workspaceFolders.length === 1) { - pythonConfig = workspace.getConfiguration('python', workspace.workspaceFolders[0].uri); + if (!this.workspaceService.hasWorkspaceFolders) { + pythonConfig = this.workspaceService.getConfiguration('python'); + } else if (this.workspaceService.workspaceFolders!.length === 1) { + pythonConfig = this.workspaceService.getConfiguration('python', this.workspaceService.workspaceFolders![0].uri); } else { - const workspaceFolder = workspace.getWorkspaceFolder(resource); + const workspaceFolder = this.workspaceService.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); + pythonConfig = this.workspaceService.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/services/storageService.ts b/src/client/unittests/common/services/storageService.ts index 14945216bf78..859cf2939cbf 100644 --- a/src/client/unittests/common/services/storageService.ts +++ b/src/client/unittests/common/services/storageService.ts @@ -6,7 +6,7 @@ import { ITestCollectionStorageService, Tests } from './../types'; @injectable() export class TestCollectionStorageService implements ITestCollectionStorageService { private testsIndexedByWorkspaceUri = new Map(); - constructor( @inject(IDisposableRegistry) disposables: Disposable[]) { + constructor(@inject(IDisposableRegistry) disposables: Disposable[]) { disposables.push(this); } public getTests(wkspace: Uri): Tests | undefined { diff --git a/src/client/unittests/common/testUtils.ts b/src/client/unittests/common/testUtils.ts index 385185d5555c..feb4d7c9be50 100644 --- a/src/client/unittests/common/testUtils.ts +++ b/src/client/unittests/common/testUtils.ts @@ -1,15 +1,13 @@ import { inject, injectable, named } from 'inversify'; import * as path from 'path'; -import * as vscode from 'vscode'; -import { Uri, workspace } from 'vscode'; -import { window } from 'vscode'; +import { Uri, window, workspace } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../../common/application/types'; import * as constants from '../../common/constants'; -import { IUnitTestSettings } from '../../common/types'; -import { Product } from '../../common/types'; +import { IUnitTestSettings, Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; import { CommandSource } from './constants'; import { TestFlatteningVisitor } from './testVisitors/flatteningVisitor'; -import { ITestVisitor, TestFile, TestFolder, TestProvider, Tests, TestSettingsPropertyNames, TestsToRun, UnitTestProduct } from './types'; -import { ITestsHelper } from './types'; +import { ITestsHelper, ITestVisitor, TestFile, TestFolder, TestProvider, Tests, TestSettingsPropertyNames, TestsToRun, UnitTestProduct } from './types'; export async function selectTestWorkspace(): Promise { if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { @@ -23,15 +21,6 @@ export async function selectTestWorkspace(): Promise { } } -export function displayTestErrorMessage(message: string) { - vscode.window.showErrorMessage(message, constants.Button_Text_Tests_View_Output).then(action => { - if (action === constants.Button_Text_Tests_View_Output) { - vscode.commands.executeCommand(constants.Commands.Tests_ViewOutput, CommandSource.ui); - } - }); - -} - 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)); @@ -44,7 +33,13 @@ export function convertFileToPackage(filePath: string): string { @injectable() export class TestsHelper implements ITestsHelper { - constructor( @inject(ITestVisitor) @named('TestFlatteningVisitor') private flatteningVisitor: TestFlatteningVisitor) { } + private readonly appShell: IApplicationShell; + private readonly commandManager: ICommandManager; + constructor(@inject(ITestVisitor) @named('TestFlatteningVisitor') private flatteningVisitor: TestFlatteningVisitor, + @inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.appShell = serviceContainer.get(IApplicationShell); + this.commandManager = serviceContainer.get(ICommandManager); + } public parseProviderName(product: UnitTestProduct): TestProvider { switch (product) { case Product.nosetest: return 'nosetest'; @@ -96,6 +91,7 @@ export class TestsHelper implements ITestsHelper { public flattenTestFiles(testFiles: TestFile[]): Tests { testFiles.forEach(testFile => this.flatteningVisitor.visitTestFile(testFile)); + // tslint:disable-next-line:no-object-literal-type-assertion const tests = { testFiles: testFiles, testFunctions: this.flatteningVisitor.flattenedTestFunctions, @@ -165,6 +161,49 @@ export class TestsHelper implements ITestsHelper { if (testFns.length > 0) { return { testFunction: testFns }; } // Just return this as a test file. + // tslint:disable-next-line:no-object-literal-type-assertion return { testFile: [{ name: name, nameToRun: name, functions: [], suites: [], xmlName: name, fullPath: '', time: 0 }] }; } + public displayTestErrorMessage(message: string) { + this.appShell.showErrorMessage(message, constants.Button_Text_Tests_View_Output).then(action => { + if (action === constants.Button_Text_Tests_View_Output) { + this.commandManager.executeCommand(constants.Commands.Tests_ViewOutput, undefined, CommandSource.ui); + } + }); + } + public mergeTests(items: Tests[]): Tests { + return items.reduce((tests, otherTests, index) => { + if (index === 0) { + return tests; + } + + tests.summary.errors += otherTests.summary.errors; + tests.summary.failures += otherTests.summary.failures; + tests.summary.passed += otherTests.summary.passed; + tests.summary.skipped += otherTests.summary.skipped; + tests.rootTestFolders.push(...otherTests.rootTestFolders); + tests.testFiles.push(...otherTests.testFiles); + tests.testFolders.push(...otherTests.testFolders); + tests.testFunctions.push(...otherTests.testFunctions); + tests.testSuites.push(...otherTests.testSuites); + + return tests; + }, items[0]); + } + + public shouldRunAllTests(testsToRun?: TestsToRun) { + if (!testsToRun) { + return true; + } + if ( + (Array.isArray(testsToRun.testFile) && testsToRun.testFile.length > 0) || + (Array.isArray(testsToRun.testFolder) && testsToRun.testFolder.length > 0) || + (Array.isArray(testsToRun.testFunction) && testsToRun.testFunction.length > 0) || + (Array.isArray(testsToRun.testSuite) && testsToRun.testSuite.length > 0) + ) { + return false; + } + + return true; + } } diff --git a/src/client/unittests/common/types.ts b/src/client/unittests/common/types.ts index 67dff3bb1427..21995b62655e 100644 --- a/src/client/unittests/common/types.ts +++ b/src/client/unittests/common/types.ts @@ -1,6 +1,5 @@ import { CancellationToken, Disposable, OutputChannel, Uri } from 'vscode'; -import { IUnitTestSettings } from '../../common/types'; -import { Product } from '../../common/types'; +import { IUnitTestSettings, Product } from '../../common/types'; import { CommandSource } from './constants'; export type TestProvider = 'nosetest' | 'pytest' | 'unittest'; @@ -25,6 +24,8 @@ export type TestRunOptions = { debug?: boolean; }; +export type UnitTestParserOptions = TestDiscoveryOptions & { startDirectory: string }; + export type TestFolder = TestResult & { name: string; testFiles: TestFile[]; @@ -126,8 +127,9 @@ export type TestsToRun = { export type UnitTestProduct = Product.nosetest | Product.pytest | Product.unittest; +export const ITestConfigSettingsService = Symbol('ITestConfigSettingsService'); export interface ITestConfigSettingsService { - updateTestArgs(testDirectory: string, product: UnitTestProduct, args: string[]): Promise; + updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]): Promise; enable(testDirectory: string | Uri, product: UnitTestProduct): Promise; disable(testDirectory: string | Uri, product: UnitTestProduct): Promise; } @@ -160,6 +162,9 @@ export interface ITestsHelper { getSettingsPropertyNames(product: Product): TestSettingsPropertyNames; flattenTestFiles(testFiles: TestFile[]): Tests; placeTestFilesIntoFolders(tests: Tests): void; + displayTestErrorMessage(message: string): void; + shouldRunAllTests(testsToRun?: TestsToRun): boolean; + mergeTests(items: Tests[]): Tests; } export const ITestVisitor = Symbol('ITestVisitor'); @@ -185,9 +190,10 @@ export interface ITestResultsService { updateResults(tests: Tests): void; } -export type launchOptions = { +export type LaunchOptions = { cwd: string; args: string[]; + testProvider: TestProvider; token?: CancellationToken; outChannel?: OutputChannel; }; @@ -195,7 +201,7 @@ export type launchOptions = { export const ITestDebugLauncher = Symbol('ITestDebugLauncher'); export interface ITestDebugLauncher { - launchDebugger(options: launchOptions): Promise; + launchDebugger(options: LaunchOptions): Promise; } export const ITestManagerFactory = Symbol('ITestManagerFactory'); @@ -239,6 +245,34 @@ export type ParserOptions = TestDiscoveryOptions; export const IUnitTestSocketServer = Symbol('IUnitTestSocketServer'); export interface IUnitTestSocketServer extends Disposable { on(event: string | symbol, listener: Function): this; - start(options?: { port?: number, host?: string }): Promise; + start(options?: { port?: number; host?: string }): Promise; stop(): void; } + +export type Options = { + workspaceFolder: Uri; + cwd: string; + args: string[]; + outChannel?: OutputChannel; + token: CancellationToken; +}; + +export const ITestRunner = Symbol('ITestRunner'); +export interface ITestRunner { + run(testProvider: TestProvider, options: Options): Promise; +} + +export enum PassCalculationFormulae { + pytest, + nosetests +} + +export const IXUnitParser = Symbol('IXUnitParser'); +export interface IXUnitParser { + updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, passCalculationFormulae: PassCalculationFormulae): Promise; +} + +export type PythonVersionInformation = { + major: number; + minor: number; +}; diff --git a/src/client/unittests/common/xUnitParser.ts b/src/client/unittests/common/xUnitParser.ts index 6318060a94f7..091ad9e87db2 100644 --- a/src/client/unittests/common/xUnitParser.ts +++ b/src/client/unittests/common/xUnitParser.ts @@ -1,11 +1,7 @@ import * as fs from 'fs'; +import { injectable } from 'inversify'; import * as xml2js from 'xml2js'; -import { Tests, TestStatus } from './types'; - -export enum PassCalculationFormulae { - pytest, - nosetests -} +import { IXUnitParser, PassCalculationFormulae, Tests, TestStatus } from './types'; type TestSuiteResult = { $: { errors: string; @@ -28,15 +24,15 @@ type TestCaseResult = { }; failure: { _: string; - $: { message: string, type: string } + $: { message: string; type: string }; }[]; error: { _: string; - $: { message: string, type: string } + $: { message: string; type: string }; }[]; skipped: { _: string; - $: { message: string, type: string } + $: { message: string; type: string }; }[]; }; @@ -46,7 +42,14 @@ function getSafeInt(value: string, defaultValue: any = 0): number { if (isNaN(num)) { return defaultValue; } return num; } -export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, passCalculationFormulae: PassCalculationFormulae): Promise<{}> { + +@injectable() +export class XUnitParser implements IXUnitParser { + public updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, passCalculationFormulae: PassCalculationFormulae): Promise { + return updateResultsFromXmlLogFile(tests, outputXmlFile, passCalculationFormulae); + } +} +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) => { @@ -127,7 +130,7 @@ export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, if (testcase.skipped) { result.testFunction.status = TestStatus.Skipped; - result.testFunction.passed = null; + result.testFunction.passed = undefined; result.testFunction.message = testcase.skipped[0].$.message; result.testFunction.traceback = ''; } diff --git a/src/client/unittests/configuration.ts b/src/client/unittests/configuration.ts index 850a39ae42cf..3772a90241a3 100644 --- a/src/client/unittests/configuration.ts +++ b/src/client/unittests/configuration.ts @@ -1,154 +1,110 @@ 'use strict'; -import * as path from 'path'; -import * as vscode from 'vscode'; + +import { inject, injectable } from 'inversify'; import { OutputChannel, Uri } from 'vscode'; -import { PythonSettings } from '../common/configSettings'; -import { IInstaller, Product } from '../common/types'; -import { getSubDirectories } from '../common/utils'; -import { TestConfigurationManager } from './common/managers/testConfigurationManager'; -import { TestConfigSettingsService } from './common/services/configSettingService'; +import { IApplicationShell, IWorkspaceService } from '../common/application/types'; +import { IConfigurationService, IInstaller, IOutputChannel, Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { TEST_OUTPUT_CHANNEL } from './common/constants'; 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 { ITestConfigurationManagerFactory, IUnitTestConfigurationService } from './types'; -// tslint:disable-next-line:no-any -async function promptToEnableAndConfigureTestFramework(wkspace: Uri, installer: IInstaller, 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, installer); - if (enableOnly) { - // Ensure others are disabled - [Product.unittest, Product.pytest, Product.nosetest] - .filter(prod => selectedTestRunner !== prod) - .forEach(prod => { - createTestConfigurationManager(wkspace, prod, outputChannel, installer).disable() - .catch(ex => console.error('Python Extension: createTestConfigurationManager.disable', ex)); - }); - return configMgr.enable(); +@injectable() +export class UnitTestConfigurationService implements IUnitTestConfigurationService { + private readonly configurationService: IConfigurationService; + private readonly appShell: IApplicationShell; + private readonly installer: IInstaller; + private readonly outputChannel: OutputChannel; + private readonly workspaceService: IWorkspaceService; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.configurationService = serviceContainer.get(IConfigurationService); + this.appShell = serviceContainer.get(IApplicationShell); + this.installer = serviceContainer.get(IInstaller); + this.outputChannel = serviceContainer.get(IOutputChannel, TEST_OUTPUT_CHANNEL); + this.workspaceService = serviceContainer.get(IWorkspaceService); } - - return configMgr.configure(wkspace).then(() => { - return enableTest(wkspace, configMgr); - }).catch(reason => { - return enableTest(wkspace, configMgr).then(() => Promise.reject(reason)); - }); -} -export function displayTestFrameworkError(wkspace: Uri, outputChannel: vscode.OutputChannel, installer: IInstaller) { - 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(wkspace, installer, 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 => { + public async displayTestFrameworkError(wkspace: Uri): Promise { + const settings = this.configurationService.getSettings(wkspace); + let enabledCount = settings.unitTest.pyTestEnabled ? 1 : 0; + enabledCount += settings.unitTest.nosetestsEnabled ? 1 : 0; + enabledCount += settings.unitTest.unittestEnabled ? 1 : 0; + if (enabledCount > 1) { + return this.promptToEnableAndConfigureTestFramework(wkspace, this.installer, this.outputChannel, 'Enable only one of the test frameworks (unittest, pytest or nosetest).', true); + } else { + const option = 'Enable and configure a Test Framework'; + const item = await this.appShell.showInformationMessage('No test framework configured (unittest, pytest or nosetest)', option); if (item === option) { - return promptToEnableAndConfigureTestFramework(wkspace, installer, outputChannel); + return this.promptToEnableAndConfigureTestFramework(wkspace, this.installer, this.outputChannel); } return Promise.reject(null); - }); - } -} -export async function displayPromptToEnableTests(rootDir: string, outputChannel: vscode.OutputChannel, installer: IInstaller) { - const settings = PythonSettings.getInstance(vscode.Uri.file(rootDir)); - if (settings.unitTest.pyTestEnabled || - settings.unitTest.nosetestsEnabled || - settings.unitTest.unittestEnabled) { - return; - } - - if (!settings.unitTest.promptToConfigure) { - return; - } - - const yes = 'Yes'; - const no = 'Later'; - const noNotAgain = 'No, don\'t ask again'; - - 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; + public async selectTestRunner(placeHolderMessage: string): Promise { + const items = [{ + label: 'unittest', + product: Product.unittest, + description: 'Standard Python test framework', + detail: 'https://docs.python.org/3/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/' + }, + { + label: 'nose', + product: Product.nosetest, + description: 'nose framework', + detail: 'https://nose.readthedocs.io/' + }]; + const options = { + matchOnDescription: true, + matchOnDetail: true, + placeHolder: placeHolderMessage + }; + const selectedTestRunner = await this.appShell.showQuickPick(items, options); + // tslint:disable-next-line:prefer-type-cast + return selectedTestRunner ? selectedTestRunner.product as UnitTestProduct : undefined; } - if (item === yes) { - await promptToEnableAndConfigureTestFramework(vscode.workspace.getWorkspaceFolder(vscode.Uri.file(rootDir))!.uri, installer, outputChannel); - } else { - const pythonConfig = vscode.workspace.getConfiguration('python'); - await pythonConfig.update('unitTest.promptToConfigure', false); + public enableTest(wkspace: Uri, product: UnitTestProduct) { + const factory = this.serviceContainer.get(ITestConfigurationManagerFactory); + const configMgr = factory.create(wkspace, product); + const pythonConfig = this.workspaceService.getConfiguration('python', wkspace); + 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)); + }); } -} -// 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 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; - }); -} -function createTestConfigurationManager(wkspace: Uri, product: Product, outputChannel: OutputChannel, installer: IInstaller) { - 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); + private async promptToEnableAndConfigureTestFramework(wkspace: Uri, installer: IInstaller, outputChannel: OutputChannel, messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false) { + const selectedTestRunner = await this.selectTestRunner(messageToDisplay); + if (typeof selectedTestRunner !== 'number') { + return Promise.reject(null); } - default: { - throw new Error('Invalid test configuration'); + const factory = this.serviceContainer.get(ITestConfigurationManagerFactory); + const configMgr = factory.create(wkspace, selectedTestRunner); + if (enableOnly) { + // Ensure others are disabled + [Product.unittest, Product.pytest, Product.nosetest] + .filter(prod => selectedTestRunner !== prod) + .forEach(prod => { + factory.create(wkspace, prod).disable() + .catch(ex => console.error('Python Extension: createTestConfigurationManager.disable', ex)); + }); + 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. + return configMgr.configure(wkspace) + .then(() => this.enableTest(wkspace, selectedTestRunner)) + .catch(reason => { return this.enableTest(wkspace, selectedTestRunner).then(() => Promise.reject(reason)); }); } } -async function selectTestRunner(placeHolderMessage: string): Promise { - const items = [{ - label: 'unittest', - product: Product.unittest, - description: 'Standard Python test framework', - detail: 'https://docs.python.org/3/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/' - }, - { - label: 'nose', - product: Product.nosetest, - description: 'nose framework', - detail: 'https://nose.readthedocs.io/' - }]; - 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/configurationFactory.ts b/src/client/unittests/configurationFactory.ts new file mode 100644 index 000000000000..ee29d6d8c1d6 --- /dev/null +++ b/src/client/unittests/configurationFactory.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import * as nose from './nosetest/testConfigurationManager'; +import * as pytest from './pytest/testConfigurationManager'; +import { ITestConfigurationManagerFactory } from './types'; +import * as unittest from './unittest/testConfigurationManager'; + +@injectable() +export class TestConfigurationManagerFactory implements ITestConfigurationManagerFactory { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } + public create(wkspace: Uri, product: Product) { + switch (product) { + case Product.unittest: { + return new unittest.ConfigurationManager(wkspace, this.serviceContainer); + } + case Product.pytest: { + return new pytest.ConfigurationManager(wkspace, this.serviceContainer); + } + case Product.nosetest: { + return new nose.ConfigurationManager(wkspace, this.serviceContainer); + } + default: { + throw new Error('Invalid test configuration'); + } + } + } + +} diff --git a/src/client/unittests/display/main.ts b/src/client/unittests/display/main.ts index b6f03f11c62f..dc2a130e1064 100644 --- a/src/client/unittests/display/main.ts +++ b/src/client/unittests/display/main.ts @@ -1,25 +1,46 @@ 'use strict'; -import * as vscode from 'vscode'; +import { inject, injectable } from 'inversify'; +import { Event, EventEmitter, StatusBarAlignment, StatusBarItem } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; import * as constants from '../../common/constants'; -import { createDeferred, isNotInstalledError } from '../../common/helpers'; +import { noop } from '../../common/core.utils'; +import { isNotInstalledError } from '../../common/helpers'; +import { IConfigurationService } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; import { CANCELLATION_REASON } from '../common/constants'; -import { displayTestErrorMessage } from '../common/testUtils'; -import { Tests } from '../common/types'; +import { ITestsHelper, Tests } from '../common/types'; +import { ITestResultDisplay } from '../types'; -export class TestResultDisplay { - private statusBar: vscode.StatusBarItem; +@injectable() +export class TestResultDisplay implements ITestResultDisplay { + private statusBar: StatusBarItem; private discoverCounter = 0; private ticker = ['|', '/', '-', '|', '/', '-', '\\']; private progressTimeout; - private progressPrefix: string; + private _enabled: boolean = false; + private progressPrefix!: string; + private readonly didChange = new EventEmitter(); + private readonly appShell: IApplicationShell; + private readonly testsHelper: ITestsHelper; + public get onDidChange(): Event { + return this.didChange.event; + } + // tslint:disable-next-line:no-any - constructor(private outputChannel: vscode.OutputChannel, private onDidChange?: vscode.EventEmitter) { - this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.appShell = serviceContainer.get(IApplicationShell); + this.statusBar = this.appShell.createStatusBarItem(StatusBarAlignment.Left); + this.testsHelper = serviceContainer.get(ITestsHelper); } public dispose() { + this.clearProgressTicker(); this.statusBar.dispose(); } + public get enabled() { + return this._enabled; + } public set enabled(enable: boolean) { + this._enabled = enable; if (enable) { this.statusBar.show(); } else { @@ -32,13 +53,12 @@ export class TestResultDisplay { .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(() => { }); + .catch(noop); } - public displayDiscoverStatus(testDiscovery: Promise) { - this.displayProgress('Discovering Tests', 'Discovering Tests (Click to Stop)', constants.Commands.Tests_Ask_To_Stop_Discovery); + public displayDiscoverStatus(testDiscovery: Promise, quietMode: boolean = false) { + this.displayProgress('Discovering Tests', 'Discovering tests (click to stop)', constants.Commands.Tests_Ask_To_Stop_Discovery); return testDiscovery.then(tests => { - this.updateWithDiscoverSuccess(tests); + this.updateWithDiscoverSuccess(tests, quietMode); return tests; }).catch(reason => { this.updateWithDiscoverFailure(reason); @@ -78,11 +98,9 @@ export class TestResultDisplay { this.statusBar.text = statusText.length === 0 ? 'No Tests Ran' : statusText.join(' '); this.statusBar.color = foreColor; this.statusBar.command = constants.Commands.Tests_View_UI; - if (this.onDidChange) { - this.onDidChange.fire(); - } + this.didChange.fire(); if (statusText.length === 0 && !debug) { - vscode.window.showWarningMessage('No tests ran, please check the configuration settings for the tests.'); + this.appShell.showWarningMessage('No tests ran, please check the configuration settings for the tests.'); } return tests; } @@ -97,7 +115,7 @@ export class TestResultDisplay { } else { this.statusBar.text = '$(alert) Tests Failed'; this.statusBar.tooltip = 'Running Tests Failed'; - displayTestErrorMessage('There was an error in running the tests.'); + this.testsHelper.displayTestErrorMessage('There was an error in running the tests.'); } return Promise.reject(reason); } @@ -124,38 +142,29 @@ export class TestResultDisplay { } // tslint:disable-next-line:no-any - private disableTests(): Promise { - // tslint:disable-next-line:no-any - const def = createDeferred(); - const pythonConfig = vscode.workspace.getConfiguration('python'); + private async disableTests(): Promise { + const configurationService = this.serviceContainer.get(IConfigurationService); const settingsToDisable = ['unitTest.promptToConfigure', 'unitTest.pyTestEnabled', 'unitTest.unittestEnabled', 'unitTest.nosetestsEnabled']; - function disableTest() { - if (settingsToDisable.length === 0) { - return def.resolve(); - } - pythonConfig.update(settingsToDisable.shift()!, false) - .then(disableTest.bind(this), disableTest.bind(this)); + for (const setting of settingsToDisable) { + await configurationService.updateSettingAsync(setting, false).catch(noop); } - - disableTest(); - return def.promise; } - private updateWithDiscoverSuccess(tests: Tests) { + private updateWithDiscoverSuccess(tests: Tests, quietMode: boolean = false) { this.clearProgressTicker(); const haveTests = tests && (tests.testFunctions.length > 0); this.statusBar.text = '$(zap) Run Tests'; this.statusBar.tooltip = 'Run Tests'; this.statusBar.command = constants.Commands.Tests_View_UI; this.statusBar.show(); - if (this.onDidChange) { - this.onDidChange.fire(); + if (this.didChange) { + this.didChange.fire(); } - if (!haveTests) { - vscode.window.showInformationMessage('No tests discovered, please check the configuration settings for the tests.', 'Disable Tests').then(item => { + if (!haveTests && !quietMode) { + this.appShell.showInformationMessage('No tests discovered, please check the configuration settings for the tests.', 'Disable Tests').then(item => { if (item === 'Disable Tests') { this.disableTests() .catch(ex => console.error('Python Extension: disableTests', ex)); @@ -181,7 +190,7 @@ export class TestResultDisplay { // tslint:disable-next-line:no-suspicious-comment // 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.'); + this.appShell.showErrorMessage('Test discovery error, please check the configuration settings for the tests.'); } } } diff --git a/src/client/unittests/display/picker.ts b/src/client/unittests/display/picker.ts index 2b3d47652abf..4544609322af 100644 --- a/src/client/unittests/display/picker.ts +++ b/src/client/unittests/display/picker.ts @@ -1,27 +1,37 @@ +import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { QuickPickItem, Uri, window } from 'vscode'; -import * as vscode from 'vscode'; +import { commands, QuickPickItem, Uri } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; import * as constants from '../../common/constants'; +import { noop } from '../../common/core.utils'; +import { IServiceContainer } from '../../ioc/types'; import { CommandSource } from '../common/constants'; import { FlattenedTestFunction, ITestCollectionStorageService, TestFile, TestFunction, Tests, TestStatus, TestsToRun } from '../common/types'; +import { ITestDisplay } from '../types'; -export class TestDisplay { - constructor(private testCollectionStorage: ITestCollectionStorageService) { } +@injectable() +export class TestDisplay implements ITestDisplay { + private readonly testCollectionStorage: ITestCollectionStorageService; + private readonly appShell: IApplicationShell; + constructor(@inject(IServiceContainer) serviceRegistry: IServiceContainer) { + this.testCollectionStorage = serviceRegistry.get(ITestCollectionStorageService); + this.appShell = serviceRegistry.get(IApplicationShell); + } public displayStopTestUI(workspace: Uri, message: string) { - window.showQuickPick([message]).then(item => { + this.appShell.showQuickPick([message]).then(item => { if (item === message) { - vscode.commands.executeCommand(constants.Commands.Tests_Stop, workspace); + commands.executeCommand(constants.Commands.Tests_Stop, undefined, workspace); } }); } public displayTestUI(cmdSource: CommandSource, wkspace: Uri) { const tests = this.testCollectionStorage.getTests(wkspace); - window.showQuickPick(buildItems(tests), { matchOnDescription: true, matchOnDetail: true }) - .then(item => onItemSelected(cmdSource, wkspace, item, false)); + this.appShell.showQuickPick(buildItems(tests), { matchOnDescription: true, matchOnDetail: true }) + .then(item => item ? onItemSelected(cmdSource, wkspace, item, false) : noop()); } public selectTestFunction(rootDirectory: string, tests: Tests): Promise { return new Promise((resolve, reject) => { - window.showQuickPick(buildItemsForFunctions(rootDirectory, tests.testFunctions), { matchOnDescription: true, matchOnDetail: true }) + this.appShell.showQuickPick(buildItemsForFunctions(rootDirectory, tests.testFunctions), { matchOnDescription: true, matchOnDetail: true }) .then(item => { if (item && item.fn) { return resolve(item.fn); @@ -32,7 +42,7 @@ export class TestDisplay { } public selectTestFile(rootDirectory: string, tests: Tests): Promise { return new Promise((resolve, reject) => { - window.showQuickPick(buildItemsForTestFiles(rootDirectory, tests.testFiles), { matchOnDescription: true, matchOnDetail: true }) + this.appShell.showQuickPick(buildItemsForTestFiles(rootDirectory, tests.testFiles), { matchOnDescription: true, matchOnDetail: true }) .then(item => { if (item && item.testFile) { return resolve(item.testFile); @@ -56,10 +66,9 @@ export class TestDisplay { testFunctions.some(testFunc => testFunc.nameToRun === fn.testFunction.nameToRun); }); - window.showQuickPick(buildItemsForFunctions(rootDirectory, flattenedFunctions, undefined, undefined, debug), - { matchOnDescription: true, matchOnDetail: true }).then(testItem => { - return onItemSelected(cmdSource, wkspace, testItem, debug); - }); + this.appShell.showQuickPick(buildItemsForFunctions(rootDirectory, flattenedFunctions, undefined, undefined, debug), + { matchOnDescription: true, matchOnDetail: true }) + .then(testItem => testItem ? onItemSelected(cmdSource, wkspace, testItem, debug) : noop()); } } @@ -96,7 +105,7 @@ function getSummary(tests?: Tests) { if (!tests || !tests.summary) { return ''; } - const statusText = []; + const statusText: string[] = []; if (tests.summary.passed > 0) { statusText.push(`${constants.Octicons.Test_Pass} ${tests.summary.passed} Passed`); } @@ -138,7 +147,7 @@ function buildItemsForFunctions(rootDirectory: string, tests: FlattenedTestFunct const functionItems: TestItem[] = []; tests.forEach(fn => { let icon = ''; - if (displayStatusIcons && statusIconMapping.has(fn.testFunction.status)) { + if (displayStatusIcons && fn.testFunction.status && statusIconMapping.has(fn.testFunction.status)) { icon = `${statusIconMapping.get(fn.testFunction.status)} `; } @@ -153,7 +162,7 @@ function buildItemsForFunctions(rootDirectory: string, tests: FlattenedTestFunct functionItems.sort((a, b) => { let sortAPrefix = '5-'; let sortBPrefix = '5-'; - if (sortBasedOnResults) { + if (sortBasedOnResults && a.fn && a.fn.testFunction.status && b.fn && b.fn.testFunction.status) { sortAPrefix = statusSortPrefix[a.fn.testFunction.status] ? statusSortPrefix[a.fn.testFunction.status] : sortAPrefix; sortBPrefix = statusSortPrefix[b.fn.testFunction.status] ? statusSortPrefix[b.fn.testFunction.status] : sortBPrefix; } @@ -178,10 +187,13 @@ function buildItemsForTestFiles(rootDirectory: string, testFiles: TestFile[]): T }; }); fileItems.sort((a, b) => { - if (a.detail < b.detail) { + if (!a.detail && !b.detail) { + return 0; + } + if (!a.detail || a.detail < b.detail!) { return -1; } - if (a.detail > b.detail) { + if (!b.detail || a.detail! > b.detail) { return 1; } return 0; @@ -194,7 +206,7 @@ function onItemSelected(cmdSource: CommandSource, wkspace: Uri, selection: TestI } let cmd = ''; // tslint:disable-next-line:no-any - const args: any[] = [cmdSource, wkspace]; + const args: any[] = [undefined, cmdSource, wkspace]; switch (selection.type) { case Type.Null: { return; @@ -221,14 +233,14 @@ function onItemSelected(cmdSource: CommandSource, wkspace: Uri, selection: TestI } case Type.RunMethod: { cmd = constants.Commands.Tests_Run; - // tslint:disable-next-line:prefer-type-cast - args.push({ testFunction: [selection.fn.testFunction] } as TestsToRun); + // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion + 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); + // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion + args.push({ testFunction: [selection.fn!.testFunction] } as TestsToRun); args.push(true); break; } @@ -237,5 +249,5 @@ function onItemSelected(cmdSource: CommandSource, wkspace: Uri, selection: TestI } } - vscode.commands.executeCommand(cmd, ...args); + commands.executeCommand(cmd, ...args); } diff --git a/src/client/unittests/main.ts b/src/client/unittests/main.ts index 2e386edea0b0..c803425ec1fa 100644 --- a/src/client/unittests/main.ts +++ b/src/client/unittests/main.ts @@ -1,306 +1,330 @@ 'use strict'; + +// tslint:disable:no-duplicate-imports no-unnecessary-callback-wrapper + +import { inject, injectable } from 'inversify'; +import { ConfigurationChangeEvent, Disposable, OutputChannel, TextDocument, Uri } from 'vscode'; import * as vscode from 'vscode'; -import { Disposable, Uri, window, workspace } from 'vscode'; -import { PythonSettings } from '../common/configSettings'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; import * as constants from '../common/constants'; -import { IInstaller } from '../common/types'; +import { IConfigurationService, IDisposableRegistry, ILogger, IOutputChannel } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { PythonSymbolProvider } from '../providers/symbolProvider'; import { UNITTEST_STOP, UNITTEST_VIEW_OUTPUT } from '../telemetry/constants'; import { sendTelemetryEvent } from '../telemetry/index'; import { activateCodeLenses } from './codeLenses/main'; -import { CANCELLATION_REASON, CommandSource } from './common/constants'; +import { CANCELLATION_REASON, CommandSource, TEST_OUTPUT_CHANNEL } from './common/constants'; import { selectTestWorkspace } from './common/testUtils'; import { ITestCollectionStorageService, ITestManager, IWorkspaceTestManagerService, TestFile, TestFunction, TestStatus, TestsToRun } from './common/types'; -import { displayTestFrameworkError } from './configuration'; -import { TestResultDisplay } from './display/main'; -import { TestDisplay } from './display/picker'; - -let workspaceTestManagerService: IWorkspaceTestManagerService; -let testResultDisplay: TestResultDisplay; -let testDisplay: TestDisplay; -let outChannel: vscode.OutputChannel; -const onDidChange: vscode.EventEmitter = new vscode.EventEmitter(); -let testCollectionStorage: ITestCollectionStorageService; -let _serviceContaner: IServiceContainer; +import { ITestDisplay, ITestResultDisplay, IUnitTestConfigurationService, IUnitTestManagementService } from './types'; -export function activate(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, symboldProvider: PythonSymbolProvider, serviceContainer: IServiceContainer) { - _serviceContaner = serviceContainer; +@injectable() +export class UnitTestManagementService implements IUnitTestManagementService, Disposable { + private readonly outputChannel: vscode.OutputChannel; + private readonly disposableRegistry: Disposable[]; + private workspaceTestManagerService?: IWorkspaceTestManagerService; + private documentManager: IDocumentManager; + private workspaceService: IWorkspaceService; + private testResultDisplay?: ITestResultDisplay; + private autoDiscoverTimer?: NodeJS.Timer; + private configChangedTimer?: NodeJS.Timer; + private readonly onDidChange: vscode.EventEmitter = new vscode.EventEmitter(); - context.subscriptions.push({ dispose: dispose }); - outChannel = outputChannel; - const disposables = registerCommands(); - context.subscriptions.push(...disposables); + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.disposableRegistry = serviceContainer.get(IDisposableRegistry); + this.outputChannel = serviceContainer.get(IOutputChannel, TEST_OUTPUT_CHANNEL); + this.workspaceService = serviceContainer.get(IWorkspaceService); + this.documentManager = serviceContainer.get(IDocumentManager); - testCollectionStorage = serviceContainer.get(ITestCollectionStorageService); - workspaceTestManagerService = serviceContainer.get(IWorkspaceTestManagerService); - - context.subscriptions.push(autoResetTests()); - context.subscriptions.push(activateCodeLenses(onDidChange, symboldProvider, testCollectionStorage)); - context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(onDocumentSaved)); - - 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, _serviceContaner.get(IInstaller)); - } -} -let timeoutId: NodeJS.Timer; -async function onDocumentSaved(doc: vscode.TextDocument): Promise { - const testManager = await getTestManager(false, doc.uri); - if (!testManager) { - return; - } - const tests = await testManager.discoverTests(CommandSource.auto, false, true); - if (!tests || !Array.isArray(tests.testFiles) || tests.testFiles.length === 0) { - return; + this.disposableRegistry.push(this); } - if (tests.testFiles.findIndex((f: TestFile) => f.fullPath === doc.uri.fsPath) === -1) { - return; + public dispose() { + if (this.workspaceTestManagerService) { + this.workspaceTestManagerService.dispose(); + } } + public async activate(): Promise { + this.workspaceTestManagerService = this.serviceContainer.get(IWorkspaceTestManagerService); - if (timeoutId) { - clearTimeout(timeoutId); + this.registerHandlers(); + this.registerCommands(); + this.autoDiscoverTests() + .catch(ex => this.serviceContainer.get(ILogger).logError('Failed to auto discover tests upon activation', ex)); + } + public async activateCodeLenses(symboldProvider: PythonSymbolProvider): Promise { + const testCollectionStorage = this.serviceContainer.get(ITestCollectionStorageService); + this.disposableRegistry.push(activateCodeLenses(this.onDidChange, symboldProvider, testCollectionStorage)); + } + public async getTestManager(displayTestNotConfiguredMessage: boolean, resource?: Uri): Promise { + let wkspace: Uri | undefined; + if (resource) { + const wkspaceFolder = this.workspaceService.getWorkspaceFolder(resource); + wkspace = wkspaceFolder ? wkspaceFolder.uri : undefined; + } else { + wkspace = await selectTestWorkspace(); + } + if (!wkspace) { + return; + } + const testManager = this.workspaceTestManagerService!.getTestManager(wkspace); + if (testManager) { + return testManager; + } + if (displayTestNotConfiguredMessage) { + const configurationService = this.serviceContainer.get(IUnitTestConfigurationService); + await configurationService.displayTestFrameworkError(wkspace); + } } - timeoutId = setTimeout(() => discoverTests(CommandSource.auto, doc.uri, true), 1000); -} + public async configurationChangeHandler(e: ConfigurationChangeEvent) { + // If there's one workspace, then stop the tests and restart, + // else let the user do this manually. + if (!this.workspaceService.hasWorkspaceFolders || this.workspaceService.workspaceFolders!.length > 1) { + return; + } -function dispose() { - workspaceTestManagerService.dispose(); - testCollectionStorage.dispose(); -} -function registerCommands(): vscode.Disposable[] { - const disposables: Disposable[] = []; - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Discover, (cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri) => { - // Ignore the exceptions returned. - // This command will be invoked else where in the extension. - // tslint:disable-next-line:no-empty - discoverTests(cmdSource, resource, true, true).catch(() => { }); - })); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Failed, (cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => runTestsImpl(cmdSource, resource, undefined, true))); - // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run, (cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testToRun?: TestsToRun) => runTestsImpl(cmdSource, file, testToRun))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Debug, (cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testToRun: TestsToRun) => runTestsImpl(cmdSource, file, testToRun, false, true))); - // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_View_UI, () => displayUI(CommandSource.commandPalette))); - // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI, (cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testFunctions: TestFunction[]) => displayPickerUI(cmdSource, file, testFunctions))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI_Debug, (cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testFunctions: TestFunction[]) => displayPickerUI(cmdSource, file, testFunctions, true))); - // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Stop, (resource: Uri) => stopTests(resource))); - // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_ViewOutput, (cmdSource: CommandSource = CommandSource.commandPalette) => viewOutput(cmdSource))); - 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'))); - // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_Method, (cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => selectAndRunTestMethod(cmdSource, resource))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Debug_Method, (cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => selectAndRunTestMethod(cmdSource, resource, true))); - // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_File, (cmdSource: CommandSource = CommandSource.commandPalette) => selectAndRunTestFile(cmdSource))); - // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Current_File, (cmdSource: CommandSource = CommandSource.commandPalette) => runCurrentTestFile(cmdSource))); + const workspaceUri = this.workspaceService.workspaceFolders![0].uri; + if (!e.affectsConfiguration('python.unitTest', workspaceUri)) { + return; + } + const settings = this.serviceContainer.get(IConfigurationService).getSettings(workspaceUri); + if (!settings.unitTest.nosetestsEnabled && !settings.unitTest.pyTestEnabled && !settings.unitTest.unittestEnabled) { + if (this.testResultDisplay) { + this.testResultDisplay.enabled = false; + } + // tslint:disable-next-line:no-suspicious-comment + // TODO: Why are we disposing, what happens when tests are enabled. + if (this.workspaceTestManagerService) { + this.workspaceTestManagerService.dispose(); + } + return; + } + if (this.testResultDisplay) { + this.testResultDisplay.enabled = true; + } + this.autoDiscoverTests() + .catch(ex => this.serviceContainer.get(ILogger).logError('Failed to auto discover tests upon activation', ex)); + } - return disposables; -} + public async discoverTestsForDocument(doc: TextDocument): Promise { + const testManager = await this.getTestManager(false, doc.uri); + if (!testManager) { + return; + } + const tests = await testManager.discoverTests(CommandSource.auto, false, true); + if (!tests || !Array.isArray(tests.testFiles) || tests.testFiles.length === 0) { + return; + } + if (tests.testFiles.findIndex((f: TestFile) => f.fullPath === doc.uri.fsPath) === -1) { + return; + } -function viewOutput(cmdSource: CommandSource) { - sendTelemetryEvent(UNITTEST_VIEW_OUTPUT); - outChannel.show(); -} -async function displayUI(cmdSource: CommandSource) { - const testManager = await getTestManager(true); - if (!testManager) { - return; + if (this.autoDiscoverTimer) { + clearTimeout(this.autoDiscoverTimer); + } + this.autoDiscoverTimer = setTimeout(() => this.discoverTests(CommandSource.auto, doc.uri, true, false, true), 1000); } + public async autoDiscoverTests() { + if (!this.workspaceService.hasWorkspaceFolders) { + return; + } + const configurationService = this.serviceContainer.get(IConfigurationService); + const settings = configurationService.getSettings(); + if (!settings.unitTest.nosetestsEnabled && !settings.unitTest.pyTestEnabled && !settings.unitTest.unittestEnabled) { + return; + } - testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); - testDisplay.displayTestUI(cmdSource, testManager.workspaceFolder); -} -async function displayPickerUI(cmdSource: CommandSource, file: Uri, testFunctions: TestFunction[], debug?: boolean) { - const testManager = await getTestManager(true, file); - if (!testManager) { - return; + // No need to display errors. + // tslint:disable-next-line:no-empty + this.discoverTests(CommandSource.auto, this.workspaceService.workspaceFolders![0].uri, true).catch(() => { }); } + public async discoverTests(cmdSource: CommandSource, resource?: Uri, ignoreCache?: boolean, userInitiated?: boolean, quietMode?: boolean) { + const testManager = await this.getTestManager(true, resource); + if (!testManager) { + return; + } - testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); - testDisplay.displayFunctionTestPickerUI(cmdSource, testManager.workspaceFolder, testManager.workingDirectory, file, testFunctions, debug); -} -async function selectAndRunTestMethod(cmdSource: CommandSource, resource: Uri, debug?: boolean) { - const testManager = await getTestManager(true, resource); - if (!testManager) { - return; - } - try { - await testManager.discoverTests(cmdSource, true, true, true); - } catch (ex) { - return; - } + if (testManager.status === TestStatus.Discovering || testManager.status === TestStatus.Running) { + return; + } - const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; - testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); - const selectedTestFn = await testDisplay.selectTestFunction(testManager.workspaceFolder.fsPath, tests); - if (!selectedTestFn) { - return; - } - // tslint:disable-next-line:prefer-type-cast - await runTestsImpl(cmdSource, testManager.workspaceFolder, { testFunction: [selectedTestFn.testFunction] } as TestsToRun, false, debug); -} -async function selectAndRunTestFile(cmdSource: CommandSource) { - const testManager = await getTestManager(true); - if (!testManager) { - return; + if (!this.testResultDisplay) { + this.testResultDisplay = this.serviceContainer.get(ITestResultDisplay); + this.testResultDisplay.onDidChange(() => this.onDidChange.fire()); + } + const discoveryPromise = testManager.discoverTests(cmdSource, ignoreCache, quietMode, userInitiated); + this.testResultDisplay.displayDiscoverStatus(discoveryPromise, quietMode) + .catch(ex => console.error('Python Extension: displayDiscoverStatus', ex)); + await discoveryPromise; } - try { - await testManager.discoverTests(cmdSource, true, true, true); - } catch (ex) { - return; + public async stopTests(resource: Uri) { + sendTelemetryEvent(UNITTEST_STOP); + const testManager = await this.getTestManager(true, resource); + if (testManager) { + testManager.stop(); + } } + public async displayStopUI(message: string): Promise { + const testManager = await this.getTestManager(true); + if (!testManager) { + return; + } - const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; - testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); - const selectedFile = await testDisplay.selectTestFile(testManager.workspaceFolder.fsPath, tests); - if (!selectedFile) { - return; - } - // tslint:disable-next-line:prefer-type-cast - await runTestsImpl(cmdSource, testManager.workspaceFolder, { testFile: [selectedFile] } as TestsToRun); -} -async function runCurrentTestFile(cmdSource: CommandSource) { - if (!window.activeTextEditor) { - return; - } - const testManager = await getTestManager(true, window.activeTextEditor.document.uri); - if (!testManager) { - return; + const testDisplay = this.serviceContainer.get(ITestDisplay); + testDisplay.displayStopTestUI(testManager.workspaceFolder, message); } - try { - await testManager.discoverTests(cmdSource, true, true, true); - } catch (ex) { - return; - } - const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; - 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(cmdSource, testManager.workspaceFolder, { testFile: [testFiles[0]] } as TestsToRun); -} -async function displayStopUI(message: string) { - const testManager = await getTestManager(true); - if (!testManager) { - return; - } - - testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); - testDisplay.displayStopTestUI(testManager.workspaceFolder, message); -} + public async displayUI(cmdSource: CommandSource) { + const testManager = await this.getTestManager(true); + if (!testManager) { + return; + } -let uniTestSettingsString: string; -function autoResetTests() { - if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length > 1) { - // tslint:disable-next-line:no-empty - return { dispose: () => { } }; + const testDisplay = this.serviceContainer.get(ITestDisplay); + testDisplay.displayTestUI(cmdSource, testManager.workspaceFolder); } + public async displayPickerUI(cmdSource: CommandSource, file: Uri, testFunctions: TestFunction[], debug?: boolean) { + const testManager = await this.getTestManager(true, file); + if (!testManager) { + return; + } - const settings = PythonSettings.getInstance(); - uniTestSettingsString = JSON.stringify(settings.unitTest); - return workspace.onDidChangeConfiguration(() => setTimeout(onConfigChanged, 1000)); -} -function onConfigChanged() { - // 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 testDisplay = this.serviceContainer.get(ITestDisplay); + testDisplay.displayFunctionTestPickerUI(cmdSource, testManager.workspaceFolder, testManager.workingDirectory, file, testFunctions, debug); } - 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; + public viewOutput(cmdSource: CommandSource) { + sendTelemetryEvent(UNITTEST_VIEW_OUTPUT); + this.outputChannel.show(); } + public async selectAndRunTestMethod(cmdSource: CommandSource, resource: Uri, debug?: boolean) { + const testManager = await this.getTestManager(true, resource); + if (!testManager) { + return; + } + try { + await testManager.discoverTests(cmdSource, true, true, true); + } catch (ex) { + return; + } - uniTestSettingsString = newSettings; - if (!settings.unitTest.nosetestsEnabled && !settings.unitTest.pyTestEnabled && !settings.unitTest.unittestEnabled) { - if (testResultDisplay) { - testResultDisplay.enabled = false; + const testCollectionStorage = this.serviceContainer.get(ITestCollectionStorageService); + const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; + const testDisplay = this.serviceContainer.get(ITestDisplay); + const selectedTestFn = await testDisplay.selectTestFunction(testManager.workspaceFolder.fsPath, tests); + if (!selectedTestFn) { + return; } - workspaceTestManagerService.dispose(); - return; - } - if (testResultDisplay) { - testResultDisplay.enabled = true; + // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion + await this.runTestsImpl(cmdSource, testManager.workspaceFolder, { testFunction: [selectedTestFn.testFunction] } as TestsToRun, false, debug); } - autoDiscoverTests(); -} -function autoDiscoverTests() { - if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length > 1) { - return; + public async selectAndRunTestFile(cmdSource: CommandSource) { + const testManager = await this.getTestManager(true); + if (!testManager) { + return; + } + try { + await testManager.discoverTests(cmdSource, true, true, true); + } catch (ex) { + return; + } + + const testCollectionStorage = this.serviceContainer.get(ITestCollectionStorageService); + const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; + const testDisplay = this.serviceContainer.get(ITestDisplay); + const selectedFile = await testDisplay.selectTestFile(testManager.workspaceFolder.fsPath, tests); + if (!selectedFile) { + return; + } + await this.runTestsImpl(cmdSource, testManager.workspaceFolder, { testFile: [selectedFile] }); } - const settings = PythonSettings.getInstance(); - if (!settings.unitTest.nosetestsEnabled && !settings.unitTest.pyTestEnabled && !settings.unitTest.unittestEnabled) { - return; + public async runCurrentTestFile(cmdSource: CommandSource) { + if (!this.documentManager.activeTextEditor) { + return; + } + const testManager = await this.getTestManager(true, this.documentManager.activeTextEditor.document.uri); + if (!testManager) { + return; + } + try { + await testManager.discoverTests(cmdSource, true, true, true); + } catch (ex) { + return; + } + const testCollectionStorage = this.serviceContainer.get(ITestCollectionStorageService); + const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; + const testFiles = tests.testFiles.filter(testFile => { + return testFile.fullPath === this.documentManager.activeTextEditor!.document.uri.fsPath; + }); + if (testFiles.length < 1) { + return; + } + await this.runTestsImpl(cmdSource, testManager.workspaceFolder, { testFile: [testFiles[0]] }); } - // No need to display errors. - // tslint:disable-next-line:no-empty - discoverTests(CommandSource.auto, workspace.workspaceFolders[0].uri, true).catch(() => { }); -} -async function stopTests(resource: Uri) { - sendTelemetryEvent(UNITTEST_STOP); - const testManager = await getTestManager(true, resource); - if (testManager) { - testManager.stop(); - } -} -async function discoverTests(cmdSource: CommandSource, resource?: Uri, ignoreCache?: boolean, userInitiated?: boolean) { - const testManager = await getTestManager(true, resource); - if (!testManager) { - return; + public async runTestsImpl(cmdSource: CommandSource, resource?: Uri, testsToRun?: TestsToRun, runFailedTests?: boolean, debug: boolean = false) { + const testManager = await this.getTestManager(true, resource); + if (!testManager) { + return; + } + + if (!this.testResultDisplay) { + this.testResultDisplay = this.serviceContainer.get(ITestResultDisplay); + this.testResultDisplay.onDidChange(() => this.onDidChange.fire()); + } + + const promise = testManager.runTest(cmdSource, testsToRun, runFailedTests, debug) + .catch(reason => { + if (reason !== CANCELLATION_REASON) { + this.outputChannel.appendLine(`Error: ${reason}`); + } + return Promise.reject(reason); + }); + + this.testResultDisplay.displayProgressStatus(promise, debug); + await promise; } + private registerCommands(): void { + const disposablesRegistry = this.serviceContainer.get(IDisposableRegistry); + const commandManager = this.serviceContainer.get(ICommandManager); - if (testManager && (testManager.status !== TestStatus.Discovering && testManager.status !== TestStatus.Running)) { - testResultDisplay = testResultDisplay ? testResultDisplay : new TestResultDisplay(outChannel, onDidChange); - const discoveryPromise = testManager.discoverTests(cmdSource, ignoreCache, false, userInitiated); - testResultDisplay.displayDiscoverStatus(discoveryPromise) - .catch(ex => console.error('Python Extension: displayDiscoverStatus', ex)); - await discoveryPromise; + const disposables = [ + commandManager.registerCommand(constants.Commands.Tests_Discover, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri) => { + // Ignore the exceptions returned. + // This command will be invoked from other places of the extension. + this.discoverTests(cmdSource, resource, true, true).ignoreErrors(); + }), + commandManager.registerCommand(constants.Commands.Tests_Run_Failed, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => this.runTestsImpl(cmdSource, resource, undefined, true)), + commandManager.registerCommand(constants.Commands.Tests_Run, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testToRun?: TestsToRun) => this.runTestsImpl(cmdSource, file, testToRun)), + commandManager.registerCommand(constants.Commands.Tests_Debug, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testToRun: TestsToRun) => this.runTestsImpl(cmdSource, file, testToRun, false, true)), + commandManager.registerCommand(constants.Commands.Tests_View_UI, () => this.displayUI(CommandSource.commandPalette)), + commandManager.registerCommand(constants.Commands.Tests_Picker_UI, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testFunctions: TestFunction[]) => this.displayPickerUI(cmdSource, file, testFunctions)), + commandManager.registerCommand(constants.Commands.Tests_Picker_UI_Debug, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testFunctions: TestFunction[]) => this.displayPickerUI(cmdSource, file, testFunctions, true)), + commandManager.registerCommand(constants.Commands.Tests_Stop, (_, resource: Uri) => this.stopTests(resource)), + commandManager.registerCommand(constants.Commands.Tests_ViewOutput, (_, cmdSource: CommandSource = CommandSource.commandPalette) => this.viewOutput(cmdSource)), + commandManager.registerCommand(constants.Commands.Tests_Ask_To_Stop_Discovery, () => this.displayStopUI('Stop discovering tests')), + commandManager.registerCommand(constants.Commands.Tests_Ask_To_Stop_Test, () => this.displayStopUI('Stop running tests')), + commandManager.registerCommand(constants.Commands.Tests_Select_And_Run_Method, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => this.selectAndRunTestMethod(cmdSource, resource)), + commandManager.registerCommand(constants.Commands.Tests_Select_And_Debug_Method, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => this.selectAndRunTestMethod(cmdSource, resource, true)), + commandManager.registerCommand(constants.Commands.Tests_Select_And_Run_File, (_, cmdSource: CommandSource = CommandSource.commandPalette) => this.selectAndRunTestFile(cmdSource)), + commandManager.registerCommand(constants.Commands.Tests_Run_Current_File, (_, cmdSource: CommandSource = CommandSource.commandPalette) => this.runCurrentTestFile(cmdSource)) + ]; + + disposablesRegistry.push(...disposables); } -} -async function runTestsImpl(cmdSource: CommandSource, resource?: Uri, testsToRun?: TestsToRun, runFailedTests?: boolean, debug: boolean = false) { - const testManager = await getTestManager(true, resource); - if (!testManager) { - return; + private onDocumentSaved(doc: TextDocument) { + const settings = this.serviceContainer.get(IConfigurationService).getSettings(doc.uri); + if (!settings.unitTest.autoTestDiscoverOnSaveEnabled) { + return; + } + this.discoverTestsForDocument(doc); } + private registerHandlers() { + const documentManager = this.serviceContainer.get(IDocumentManager); - testResultDisplay = testResultDisplay ? testResultDisplay : new TestResultDisplay(outChannel, onDidChange); - const promise = testManager.runTest(cmdSource, testsToRun, runFailedTests, debug) - .catch(reason => { - if (reason !== CANCELLATION_REASON) { - outChannel.appendLine(`Error: ${reason}`); + this.disposableRegistry.push(documentManager.onDidSaveTextDocument(this.onDocumentSaved.bind(this))); + this.disposableRegistry.push(this.workspaceService.onDidChangeConfiguration(e => { + if (this.configChangedTimer) { + clearTimeout(this.configChangedTimer); } - return Promise.reject(reason); - }); - - testResultDisplay.displayProgressStatus(promise, debug); - await promise; + this.configChangedTimer = setTimeout(() => this.configurationChangeHandler(e), 1000); + })); + } } diff --git a/src/client/unittests/nosetest/main.ts b/src/client/unittests/nosetest/main.ts index 5ffae0b80cd3..19aa8b125e9a 100644 --- a/src/client/unittests/nosetest/main.ts +++ b/src/client/unittests/nosetest/main.ts @@ -1,20 +1,26 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; -import { PythonSettings } from '../../common/configSettings'; import { Product } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; +import { NOSETEST_PROVIDER } from '../common/constants'; import { BaseTestManager } from '../common/managers/baseTestManager'; -import { TestDiscoveryOptions, TestRunOptions, Tests, TestsToRun } from '../common/types'; -import { runTest } from './runner'; +import { ITestsHelper, TestDiscoveryOptions, TestRunOptions, Tests, TestsToRun } from '../common/types'; +import { IArgumentsService, ITestManagerRunner, TestFilter } from '../types'; @injectable() export class TestManager extends BaseTestManager { + private readonly argsService: IArgumentsService; + private readonly helper: ITestsHelper; + private readonly runner: ITestManagerRunner; public get enabled() { - return PythonSettings.getInstance(this.workspaceFolder).unitTest.nosetestsEnabled; + return this.settings.unitTest.nosetestsEnabled; } constructor(workspaceFolder: Uri, rootDirectory: string, @inject(IServiceContainer) serviceContainer: IServiceContainer) { - super('nosetest', Product.nosetest, workspaceFolder, rootDirectory, serviceContainer); + super(NOSETEST_PROVIDER, Product.nosetest, workspaceFolder, rootDirectory, serviceContainer); + this.argsService = this.serviceContainer.get(IArgumentsService, this.testProvider); + this.helper = this.serviceContainer.get(ITestsHelper); + this.runner = this.serviceContainer.get(ITestManagerRunner, this.testProvider); } public getDiscoveryOptions(ignoreCache: boolean): TestDiscoveryOptions { const args = this.settings.unitTest.nosetestArgs.slice(0); @@ -25,14 +31,21 @@ export class TestManager extends BaseTestManager { outChannel: this.outputChannel }; } - // 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); + public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { + let args: string[]; + + const runAllTests = this.helper.shouldRunAllTests(testsToRun); + if (debug) { + args = this.argsService.filterArguments(this.settings.unitTest.nosetestArgs, runAllTests ? TestFilter.debugAll : TestFilter.debugSpecific); + } else { + args = this.argsService.filterArguments(this.settings.unitTest.nosetestArgs, runAllTests ? TestFilter.runAll : TestFilter.runSpecific); + } + if (runFailedTests === true && args.indexOf('--failed') === -1) { - args.push('--failed'); + args.splice(0, 0, '--failed'); } if (!runFailedTests && args.indexOf('--with-id') === -1) { - args.push('--with-id'); + args.splice(0, 0, '--with-id'); } const options: TestRunOptions = { workspaceFolder: Uri.file(this.rootDirectory), @@ -42,6 +55,6 @@ export class TestManager extends BaseTestManager { outChannel: this.outputChannel, debug }; - return runTest(this.serviceContainer, this.testResultsService, options); + return this.runner.runTest(this.testResultsService, options, this); } } diff --git a/src/client/unittests/nosetest/runner.ts b/src/client/unittests/nosetest/runner.ts index 3627ea308dbc..a6ba3cc148e0 100644 --- a/src/client/unittests/nosetest/runner.ts +++ b/src/client/unittests/nosetest/runner.ts @@ -1,96 +1,99 @@ 'use strict'; -import * as path from 'path'; -import { createTemporaryFile } from '../../common/helpers'; + +import { inject, injectable } from 'inversify'; +import { noop } from '../../common/core.utils'; +import { IFileSystem, TemporaryFile } from '../../common/platform/types'; import { IServiceContainer } from '../../ioc/types'; -import { Options, run } from '../common/runner'; -import { ITestDebugLauncher, ITestResultsService, TestRunOptions, Tests } from '../common/types'; -import { PassCalculationFormulae, updateResultsFromXmlLogFile } from '../common/xUnitParser'; +import { NOSETEST_PROVIDER } from '../common/constants'; +import { Options } from '../common/runner'; +import { ITestDebugLauncher, ITestManager, ITestResultsService, ITestRunner, IXUnitParser, LaunchOptions, PassCalculationFormulae, TestRunOptions, Tests } from '../common/types'; +import { IArgumentsHelper, IArgumentsService, ITestManagerRunner } from '../types'; const WITH_XUNIT = '--with-xunit'; const XUNIT_FILE = '--xunit-file'; -// tslint:disable-next-line:no-any -export function runTest(serviceContainer: IServiceContainer, testResultsService: ITestResultsService, options: TestRunOptions): Promise { - let testPaths: string[] = []; - if (options.testsToRun && options.testsToRun.testFolder) { - testPaths = testPaths.concat(options.testsToRun.testFolder.map(f => f.nameToRun)); - } - if (options.testsToRun && options.testsToRun.testFile) { - testPaths = testPaths.concat(options.testsToRun.testFile.map(f => f.nameToRun)); - } - if (options.testsToRun && options.testsToRun.testSuite) { - testPaths = testPaths.concat(options.testsToRun.testSuite.map(f => f.nameToRun)); +@injectable() +export class TestManagerRunner implements ITestManagerRunner { + private readonly argsService: IArgumentsService; + private readonly argsHelper: IArgumentsHelper; + private readonly testRunner: ITestRunner; + private readonly xUnitParser: IXUnitParser; + private readonly fs: IFileSystem; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.argsService = serviceContainer.get(IArgumentsService, NOSETEST_PROVIDER); + this.argsHelper = serviceContainer.get(IArgumentsHelper); + this.testRunner = serviceContainer.get(ITestRunner); + this.xUnitParser = this.serviceContainer.get(IXUnitParser); + this.fs = this.serviceContainer.get(IFileSystem); } - if (options.testsToRun && options.testsToRun.testFunction) { - testPaths = testPaths.concat(options.testsToRun.testFunction.map(f => f.nameToRun)); - } - - let xmlLogFile = ''; - // tslint:disable-next-line:no-empty - let xmlLogFileCleanup: Function = () => { }; + public async runTest(testResultsService: ITestResultsService, options: TestRunOptions, _: ITestManager): Promise { + let testPaths: string[] = []; + if (options.testsToRun && options.testsToRun.testFolder) { + testPaths = testPaths.concat(options.testsToRun.testFolder.map(f => f.nameToRun)); + } + if (options.testsToRun && options.testsToRun.testFile) { + testPaths = testPaths.concat(options.testsToRun.testFile.map(f => f.nameToRun)); + } + if (options.testsToRun && options.testsToRun.testSuite) { + testPaths = testPaths.concat(options.testsToRun.testSuite.map(f => f.nameToRun)); + } + if (options.testsToRun && options.testsToRun.testFunction) { + testPaths = testPaths.concat(options.testsToRun.testFunction.map(f => f.nameToRun)); + } - // Check if '--with-xunit' is in args list - const noseTestArgs = options.args.slice(); - if (noseTestArgs.indexOf(WITH_XUNIT) === -1) { - noseTestArgs.push(WITH_XUNIT); - } + let deleteJUnitXmlFile: Function = noop; + const args = options.args; + // Check if '--with-xunit' is in args list + if (args.indexOf(WITH_XUNIT) === -1) { + args.splice(0, 0, WITH_XUNIT); + } - // Check if '--xunit-file' exists, if not generate random xml file - const indexOfXUnitFile = noseTestArgs.findIndex(value => value.indexOf(XUNIT_FILE) === 0); - let promiseToGetXmlLogFile: Promise; - if (indexOfXUnitFile === -1) { - promiseToGetXmlLogFile = createTemporaryFile('.xml').then(xmlLogResult => { - xmlLogFileCleanup = xmlLogResult.cleanupCallback; - xmlLogFile = xmlLogResult.filePath; + try { + const xmlLogResult = await this.getUnitXmlFile(args); + const xmlLogFile = xmlLogResult.filePath; + deleteJUnitXmlFile = xmlLogResult.dispose; + // Remove the '--unixml' if it exists, and add it with our path. + const testArgs = this.argsService.filterArguments(args, [XUNIT_FILE]); + testArgs.splice(0, 0, `${XUNIT_FILE}=${xmlLogFile}`); - noseTestArgs.push(`${XUNIT_FILE}=${xmlLogFile}`); - return xmlLogResult.filePath; - }); - } else { - if (noseTestArgs[indexOfXUnitFile].indexOf('=') === -1) { - xmlLogFile = noseTestArgs[indexOfXUnitFile + 1]; - } else { - xmlLogFile = noseTestArgs[indexOfXUnitFile].substring(noseTestArgs[indexOfXUnitFile].indexOf('=') + 1).trim(); - } + // Positional arguments control the tests to be run. + testArgs.push(...testPaths); - promiseToGetXmlLogFile = Promise.resolve(xmlLogFile); - } + if (options.debug === true) { + const debugLauncher = this.serviceContainer.get(ITestDebugLauncher); + const debuggerArgs = [options.cwd, 'nose'].concat(testArgs); + const launchOptions: LaunchOptions = { cwd: options.cwd, args: debuggerArgs, token: options.token, outChannel: options.outChannel, testProvider: NOSETEST_PROVIDER }; + await debugLauncher.launchDebugger(launchOptions); + } else { + const runOptions: Options = { + args: testArgs.concat(testPaths), + cwd: options.cwd, + outChannel: options.outChannel, + token: options.token, + workspaceFolder: options.workspaceFolder + }; + await this.testRunner.run(NOSETEST_PROVIDER, runOptions); + } - return promiseToGetXmlLogFile.then(() => { - if (options.debug === true) { - const debugLauncher = serviceContainer.get(ITestDebugLauncher); - const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py'); - const nosetestlauncherargs = [options.cwd, 'nose']; - const debuggerArgs = [testLauncherFile].concat(nosetestlauncherargs).concat(noseTestArgs.concat(testPaths)); - const launchOptions = { cwd: options.cwd, args: debuggerArgs, token: options.token, outChannel: options.outChannel }; - // tslint:disable-next-line:prefer-type-cast no-any - return debugLauncher.launchDebugger(launchOptions) as Promise; - } else { - // tslint:disable-next-line:prefer-type-cast no-any - const runOptions: Options = { - args: noseTestArgs.concat(testPaths), - cwd: options.cwd, - outChannel: options.outChannel, - token: options.token, - workspaceFolder: options.workspaceFolder - }; - return run(serviceContainer, 'nosetest', runOptions); + return options.debug ? options.tests : await this.updateResultsFromLogFiles(options.tests, xmlLogFile, testResultsService); + } catch (ex) { + return Promise.reject(ex); + } finally { + deleteJUnitXmlFile(); } - }).then(() => { - return updateResultsFromLogFiles(options.tests, xmlLogFile, testResultsService); - }).then(result => { - xmlLogFileCleanup(); - return result; - }).catch(reason => { - xmlLogFileCleanup(); - return Promise.reject(reason); - }); -} + } -// tslint:disable-next-line:no-any -export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise { - return updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.nosetests).then(() => { + private async updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise { + await this.xUnitParser.updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.nosetests); testResultsService.updateResults(tests); return tests; - }); + } + private async getUnitXmlFile(args: string[]): Promise { + const xmlFile = this.argsHelper.getOptionValues(args, XUNIT_FILE); + if (typeof xmlFile === 'string') { + return { filePath: xmlFile, dispose: noop }; + } + + return this.fs.createTemporaryFile('.xml'); + } } diff --git a/src/client/unittests/nosetest/services/argsService.ts b/src/client/unittests/nosetest/services/argsService.ts new file mode 100644 index 000000000000..8fbe92f41006 --- /dev/null +++ b/src/client/unittests/nosetest/services/argsService.ts @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IServiceContainer } from '../../../ioc/types'; +import { IArgumentsHelper, IArgumentsService, TestFilter } from '../../types'; + +const OptionsWithArguments = ['--attr', '--config', '--cover-html-dir', '--cover-min-percentage', + '--cover-package', '--cover-xml-file', '--debug', '--debug-log', '--doctest-extension', + '--doctest-fixtures', '--doctest-options', '--doctest-result-variable', '--eval-attr', + '--exclude', '--id-file', '--ignore-files', '--include', '--log-config', '--logging-config', + '--logging-datefmt', '--logging-filter', '--logging-format', '--logging-level', '--match', + '--process-timeout', '--processes', '--py3where', '--testmatch', '--tests', '--verbosity', + '--where', '--xunit-file', '--xunit-testsuite-name', + '-A', '-a', '-c', '-e', '-i', '-I', '-l', '-m', '-w', + '--profile-restrict', '--profile-sort', '--profile-stats-file']; + +const OptionsWithoutArguments = ['-h', '--help', '-V', '--version', '-p', '--plugins', + '-v', '--verbose', '--quiet', '-x', '--stop', '-P', '--no-path-adjustment', + '--exe', '--noexe', '--traverse-namespace', '--first-package-wins', '--first-pkg-wins', + '--1st-pkg-wins', '--no-byte-compile', '-s', '--nocapture', '--nologcapture', + '--logging-clear-handlers', '--with-coverage', '--cover-erase', '--cover-tests', + '--cover-inclusive', '--cover-html', '--cover-branches', '--cover-xml', '--pdb', + '--pdb-failures', '--pdb-errors', '--no-deprecated', '--with-doctest', '--doctest-tests', + '--with-isolation', '-d', '--detailed-errors', '--failure-detail', '--no-skip', + '--with-id', '--failed', '--process-restartworker', '--with-xunit', + '--all-modules', '--collect-only', '--with-profile']; + +@injectable() +export class ArgumentsService implements IArgumentsService { + private readonly helper: IArgumentsHelper; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.helper = serviceContainer.get(IArgumentsHelper); + } + public getKnownOptions(): { withArgs: string[]; withoutArgs: string[] } { + return { + withArgs: OptionsWithArguments, + withoutArgs: OptionsWithoutArguments + }; + } + public getOptionValue(args: string[], option: string): string | string[] | undefined { + return this.helper.getOptionValues(args, option); + } + // tslint:disable-next-line:max-func-body-length + public filterArguments(args: string[], argumentToRemoveOrFilter: string[] | TestFilter): string[] { + const optionsWithoutArgsToRemove: string[] = []; + const optionsWithArgsToRemove: string[] = []; + // Positional arguments in nosetest are test directories and files. + // So if we want to run a specific test, then remove positional args. + let removePositionalArgs = false; + if (Array.isArray(argumentToRemoveOrFilter)) { + argumentToRemoveOrFilter.forEach(item => { + if (OptionsWithArguments.indexOf(item) >= 0) { + optionsWithArgsToRemove.push(item); + } + if (OptionsWithoutArguments.indexOf(item) >= 0) { + optionsWithoutArgsToRemove.push(item); + } + }); + } else { + switch (argumentToRemoveOrFilter) { + case TestFilter.removeTests: { + removePositionalArgs = true; + break; + } + case TestFilter.discovery: { + optionsWithoutArgsToRemove.push(...[ + '-v', '--verbose', '-q', '--quiet', + '-x', '--stop', + '--with-coverage', + ...OptionsWithoutArguments.filter(item => item.startsWith('--cover')), + ...OptionsWithoutArguments.filter(item => item.startsWith('--logging')), + ...OptionsWithoutArguments.filter(item => item.startsWith('--pdb')), + ...OptionsWithoutArguments.filter(item => item.indexOf('xunit') >= 0) + ]); + optionsWithArgsToRemove.push(...[ + '--verbosity', '-l', '--debug', '--cover-package', + ...OptionsWithoutArguments.filter(item => item.startsWith('--cover')), + ...OptionsWithArguments.filter(item => item.startsWith('--logging')), + ...OptionsWithoutArguments.filter(item => item.indexOf('xunit') >= 0) + ]); + break; + } + case TestFilter.debugAll: + case TestFilter.runAll: { + break; + } + case TestFilter.debugSpecific: + case TestFilter.runSpecific: { + removePositionalArgs = true; + break; + } + default: { + throw new Error(`Unsupported Filter '${argumentToRemoveOrFilter}'`); + } + } + } + + let filteredArgs = args.slice(); + if (removePositionalArgs) { + const positionalArgs = this.helper.getPositionalArguments(filteredArgs, OptionsWithArguments, OptionsWithoutArguments); + filteredArgs = filteredArgs.filter(item => positionalArgs.indexOf(item) === -1); + } + return this.helper.filterArguments(filteredArgs, optionsWithArgsToRemove, optionsWithoutArgsToRemove); + } + public getTestFolders(args: string[]): string[] { + return this.helper.getPositionalArguments(args, OptionsWithArguments, OptionsWithoutArguments); + } +} diff --git a/src/client/unittests/nosetest/services/discoveryService.ts b/src/client/unittests/nosetest/services/discoveryService.ts index acd651189b6d..157b24d11257 100644 --- a/src/client/unittests/nosetest/services/discoveryService.ts +++ b/src/client/unittests/nosetest/services/discoveryService.ts @@ -5,43 +5,33 @@ import { inject, injectable, named } from 'inversify'; import { CancellationTokenSource } from 'vscode'; import { IServiceContainer } from '../../../ioc/types'; import { NOSETEST_PROVIDER } from '../../common/constants'; -import { Options, run } from '../../common/runner'; -import { ITestDiscoveryService, ITestsParser, TestDiscoveryOptions, Tests } from '../../common/types'; - -const argsToExcludeForDiscovery = ['-v', '--verbose', - '-q', '--quiet', '-x', '--stop', - '--with-coverage', '--cover-erase', '--cover-tests', - '--cover-inclusive', '--cover-html', '--cover-branches', '--cover-xml', - '--pdb', '--pdb-failures', '--pdb-errors', - '--failed', '--process-restartworker', '--with-xunit']; -const settingsInArgsToExcludeForDiscovery = ['--verbosity']; +import { Options } from '../../common/runner'; +import { ITestDiscoveryService, ITestRunner, ITestsParser, TestDiscoveryOptions, Tests } from '../../common/types'; +import { IArgumentsService, TestFilter } from '../../types'; @injectable() export class TestDiscoveryService implements ITestDiscoveryService { - constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(ITestsParser) @named(NOSETEST_PROVIDER) private testParser: ITestsParser) { } + private argsService: IArgumentsService; + private runner: ITestRunner; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(ITestsParser) @named(NOSETEST_PROVIDER) private testParser: ITestsParser) { + this.argsService = this.serviceContainer.get(IArgumentsService, NOSETEST_PROVIDER); + this.runner = this.serviceContainer.get(ITestRunner); + } public async discoverTests(options: TestDiscoveryOptions): Promise { - // Remove unwanted arguments - const args = options.args.filter(arg => { - if (argsToExcludeForDiscovery.indexOf(arg.trim()) !== -1) { - return false; - } - if (settingsInArgsToExcludeForDiscovery.some(setting => setting.indexOf(arg.trim()) === 0)) { - return false; - } - return true; - }); + // Remove unwanted arguments. + const args = this.argsService.filterArguments(options.args, TestFilter.discovery); const token = options.token ? options.token : new CancellationTokenSource().token; const runOptions: Options = { - args: args.concat(['--collect-only', '-vvv']), + args: ['--collect-only', '-vvv'].concat(args), cwd: options.cwd, workspaceFolder: options.workspaceFolder, token, outChannel: options.outChannel }; - const data = await run(this.serviceContainer, NOSETEST_PROVIDER, runOptions); + const data = await this.runner.run(NOSETEST_PROVIDER, runOptions); if (options.token && options.token.isCancellationRequested) { return Promise.reject('cancelled'); } diff --git a/src/client/unittests/nosetest/services/parserService.ts b/src/client/unittests/nosetest/services/parserService.ts index fc706ec67606..634369db83c7 100644 --- a/src/client/unittests/nosetest/services/parserService.ts +++ b/src/client/unittests/nosetest/services/parserService.ts @@ -13,7 +13,7 @@ const NOSE_WANT_FILE_SUFFIX_WITHOUT_EXT = '? True'; @injectable() export class TestsParser implements ITestsParser { - constructor( @inject(ITestsHelper) private testsHelper: ITestsHelper) { } + constructor(@inject(ITestsHelper) private testsHelper: ITestsHelper) { } public parse(content: string, options: ParserOptions): Tests { let testFiles = this.getTestFiles(content, options); // Exclude tests that don't have any functions or test suites. @@ -121,9 +121,10 @@ export class TestsParser implements ITestsParser { 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); + const cls = testFile.suites.find(suite => suite.name === clsName); + if (cls) { + cls.functions.push(fn); + } return; } if (line.startsWith('nose.selector: DEBUG: wantFunction { - const promises = ['.noserc', 'nose.cfg'].map(cfg => { - return new Promise(resolve => { - fs.exists(path.join(rootDir, cfg), exists => { resolve(exists ? cfg : ''); }); - }); - }); - const values = await Promise.all(promises); - return values.filter(exists => exists.length > 0); + public async requiresUserToConfigure(wkspace: Uri): Promise { + const fs = this.serviceContainer.get(IFileSystem); + for (const cfg of ['.noserc', 'nose.cfg']) { + if (await fs.fileExists(path.join(wkspace.fsPath, cfg))) { + return true; + } + } + return false; } - // tslint:disable-next-line:no-any - public async configure(wkspace: Uri): Promise { + public async configure(wkspace: Uri): Promise { const args: string[] = []; const configFileOptionLabel = 'Use existing config file'; - const configFiles = await ConfigurationManager.configFilesExist(wkspace.fsPath); // If a config file exits, there's nothing to be configured. - if (configFiles.length > 0) { + if (await this.requiresUserToConfigure(wkspace)) { return; } - const subDirs = await this.getTestDirs(wkspace.fsPath); const testDir = await this.selectTestDir(wkspace.fsPath, subDirs); if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { diff --git a/src/client/unittests/pytest/main.ts b/src/client/unittests/pytest/main.ts index f513361e217f..9f1f725268b2 100644 --- a/src/client/unittests/pytest/main.ts +++ b/src/client/unittests/pytest/main.ts @@ -1,19 +1,26 @@ 'use strict'; + import { Uri } from 'vscode'; -import { PythonSettings } from '../../common/configSettings'; import { Product } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; +import { PYTEST_PROVIDER } from '../common/constants'; import { BaseTestManager } from '../common/managers/baseTestManager'; -import { TestDiscoveryOptions, TestRunOptions, Tests, TestsToRun } from '../common/types'; -import { runTest } from './runner'; +import { ITestsHelper, TestDiscoveryOptions, TestRunOptions, Tests, TestsToRun } from '../common/types'; +import { IArgumentsService, ITestManagerRunner, TestFilter } from '../types'; export class TestManager extends BaseTestManager { + private readonly argsService: IArgumentsService; + private readonly helper: ITestsHelper; + private readonly runner: ITestManagerRunner; public get enabled() { - return PythonSettings.getInstance(this.workspaceFolder).unitTest.pyTestEnabled; + return this.settings.unitTest.pyTestEnabled; } constructor(workspaceFolder: Uri, rootDirectory: string, serviceContainer: IServiceContainer) { - super('pytest', Product.pytest, workspaceFolder, rootDirectory, serviceContainer); + super(PYTEST_PROVIDER, Product.pytest, workspaceFolder, rootDirectory, serviceContainer); + this.argsService = this.serviceContainer.get(IArgumentsService, this.testProvider); + this.helper = this.serviceContainer.get(ITestsHelper); + this.runner = this.serviceContainer.get(ITestManagerRunner, this.testProvider); } public getDiscoveryOptions(ignoreCache: boolean): TestDiscoveryOptions { const args = this.settings.unitTest.pyTestArgs.slice(0); @@ -24,10 +31,18 @@ export class TestManager extends BaseTestManager { outChannel: this.outputChannel }; } - public async runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<{}> { - const args = this.settings.unitTest.pyTestArgs.slice(0); + public async runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { + let args: string[]; + + const runAllTests = this.helper.shouldRunAllTests(testsToRun); + if (debug) { + args = this.argsService.filterArguments(this.settings.unitTest.pyTestArgs, runAllTests ? TestFilter.debugAll : TestFilter.debugSpecific); + } else { + args = this.argsService.filterArguments(this.settings.unitTest.pyTestArgs, runAllTests ? TestFilter.runAll : TestFilter.runSpecific); + } + if (runFailedTests === true && args.indexOf('--lf') === -1 && args.indexOf('--last-failed') === -1) { - args.push('--last-failed'); + args.splice(0, 0, '--last-failed'); } const options: TestRunOptions = { workspaceFolder: this.workspaceFolder, @@ -36,6 +51,6 @@ export class TestManager extends BaseTestManager { token: this.testRunnerCancellationToken!, outChannel: this.outputChannel }; - return runTest(this.serviceContainer, this.testResultsService, options); + return this.runner.runTest(this.testResultsService, options, this); } } diff --git a/src/client/unittests/pytest/runner.ts b/src/client/unittests/pytest/runner.ts index 4f7a5fd3053e..8bff79d5a9b3 100644 --- a/src/client/unittests/pytest/runner.ts +++ b/src/client/unittests/pytest/runner.ts @@ -1,70 +1,92 @@ 'use strict'; -import * as path from 'path'; -import { createTemporaryFile } from '../../common/helpers'; +import { inject, injectable } from 'inversify'; +import { noop } from '../../common/core.utils'; +import { IFileSystem, TemporaryFile } from '../../common/platform/types'; import { IServiceContainer } from '../../ioc/types'; -import { Options, run } from '../common/runner'; -import { ITestDebugLauncher, ITestResultsService, TestRunOptions, Tests } from '../common/types'; -import { PassCalculationFormulae, updateResultsFromXmlLogFile } from '../common/xUnitParser'; +import { PYTEST_PROVIDER } from '../common/constants'; +import { Options } from '../common/runner'; +import { ITestDebugLauncher, ITestManager, ITestResultsService, ITestRunner, IXUnitParser, LaunchOptions, PassCalculationFormulae, TestRunOptions, Tests } from '../common/types'; +import { IArgumentsHelper, IArgumentsService, ITestManagerRunner } from '../types'; -export function runTest(serviceContainer: IServiceContainer, testResultsService: ITestResultsService, options: TestRunOptions): Promise { - let testPaths: string[] = []; - if (options.testsToRun && options.testsToRun.testFolder) { - testPaths = testPaths.concat(options.testsToRun.testFolder.map(f => f.nameToRun)); - } - if (options.testsToRun && options.testsToRun.testFile) { - testPaths = testPaths.concat(options.testsToRun.testFile.map(f => f.nameToRun)); - } - if (options.testsToRun && options.testsToRun.testSuite) { - testPaths = testPaths.concat(options.testsToRun.testSuite.map(f => f.nameToRun)); - } - if (options.testsToRun && options.testsToRun.testFunction) { - testPaths = testPaths.concat(options.testsToRun.testFunction.map(f => f.nameToRun)); +const JunitXmlArg = '--junitxml'; +@injectable() +export class TestManagerRunner implements ITestManagerRunner { + private readonly argsService: IArgumentsService; + private readonly argsHelper: IArgumentsHelper; + private readonly testRunner: ITestRunner; + private readonly xUnitParser: IXUnitParser; + private readonly fs: IFileSystem; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.argsService = serviceContainer.get(IArgumentsService, PYTEST_PROVIDER); + this.argsHelper = serviceContainer.get(IArgumentsHelper); + this.testRunner = serviceContainer.get(ITestRunner); + this.xUnitParser = this.serviceContainer.get(IXUnitParser); + this.fs = this.serviceContainer.get(IFileSystem); } + public async runTest(testResultsService: ITestResultsService, options: TestRunOptions, _: ITestManager): Promise { + let testPaths: string[] = []; + if (options.testsToRun && options.testsToRun.testFolder) { + testPaths = testPaths.concat(options.testsToRun.testFolder.map(f => f.nameToRun)); + } + if (options.testsToRun && options.testsToRun.testFile) { + testPaths = testPaths.concat(options.testsToRun.testFile.map(f => f.nameToRun)); + } + if (options.testsToRun && options.testsToRun.testSuite) { + testPaths = testPaths.concat(options.testsToRun.testSuite.map(f => f.nameToRun)); + } + if (options.testsToRun && options.testsToRun.testFunction) { + testPaths = testPaths.concat(options.testsToRun.testFunction.map(f => f.nameToRun)); + } - let xmlLogFile = ''; - let xmlLogFileCleanup: Function; - let args = options.args; + let deleteJUnitXmlFile: Function = noop; + const args = options.args; + try { + const xmlLogResult = await this.getJUnitXmlFile(args); + const xmlLogFile = xmlLogResult.filePath; + deleteJUnitXmlFile = xmlLogResult.dispose; + // Remove the '--junixml' if it exists, and add it with our path. + const testArgs = this.argsService.filterArguments(args, [JunitXmlArg]); + testArgs.splice(0, 0, `${JunitXmlArg}=${xmlLogFile}`); - return createTemporaryFile('.xml').then(xmlLogResult => { - xmlLogFile = xmlLogResult.filePath; - xmlLogFileCleanup = xmlLogResult.cleanupCallback; - if (testPaths.length > 0) { - // Ignore the test directories, as we're running a specific test - args = args.filter(arg => arg.trim().startsWith('-')); - } - const testArgs = testPaths.concat(args, [`--junitxml=${xmlLogFile}`]); - if (options.debug) { - const debugLauncher = serviceContainer.get(ITestDebugLauncher); - const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py'); - const pytestlauncherargs = [options.cwd, 'pytest']; - const debuggerArgs = [testLauncherFile].concat(pytestlauncherargs).concat(testArgs); - const launchOptions = { cwd: options.cwd, args: debuggerArgs, token: options.token, outChannel: options.outChannel }; - // tslint:disable-next-line:prefer-type-cast no-any - return debugLauncher.launchDebugger(launchOptions) as Promise; - } else { - const runOptions: Options = { - args: testArgs, - cwd: options.cwd, - outChannel: options.outChannel, - token: options.token, - workspaceFolder: options.workspaceFolder - }; - return run(serviceContainer, 'pytest', runOptions); + // Positional arguments control the tests to be run. + testArgs.push(...testPaths); + + if (options.debug) { + const debugLauncher = this.serviceContainer.get(ITestDebugLauncher); + const debuggerArgs = [options.cwd, 'pytest'].concat(testArgs); + const launchOptions: LaunchOptions = { cwd: options.cwd, args: debuggerArgs, token: options.token, outChannel: options.outChannel, testProvider: PYTEST_PROVIDER }; + await debugLauncher.launchDebugger(launchOptions); + } else { + const runOptions: Options = { + args: testArgs, + cwd: options.cwd, + outChannel: options.outChannel, + token: options.token, + workspaceFolder: options.workspaceFolder + }; + await this.testRunner.run(PYTEST_PROVIDER, runOptions); + } + + return options.debug ? options.tests : await this.updateResultsFromLogFiles(options.tests, xmlLogFile, testResultsService); + } catch (ex) { + return Promise.reject(ex); + } finally { + deleteJUnitXmlFile(); } - }).then(() => { - return updateResultsFromLogFiles(options.tests, xmlLogFile, testResultsService); - }).then(result => { - xmlLogFileCleanup(); - return result; - }).catch(reason => { - xmlLogFileCleanup(); - return Promise.reject(reason); - }); -} + } -export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise { - return updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.pytest).then(() => { + private async updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise { + await this.xUnitParser.updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.pytest); testResultsService.updateResults(tests); return tests; - }); + } + + private async getJUnitXmlFile(args: string[]): Promise { + const xmlFile = this.argsHelper.getOptionValues(args, JunitXmlArg); + if (typeof xmlFile === 'string') { + return { filePath: xmlFile, dispose: noop }; + } + return this.fs.createTemporaryFile('.xml'); + } + } diff --git a/src/client/unittests/pytest/services/argsService.ts b/src/client/unittests/pytest/services/argsService.ts new file mode 100644 index 000000000000..e8942a840b9f --- /dev/null +++ b/src/client/unittests/pytest/services/argsService.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IServiceContainer } from '../../../ioc/types'; +import { IArgumentsHelper, IArgumentsService, TestFilter } from '../../types'; + +const OptionsWithArguments = ['-c', '-k', '-m', '-o', '-p', '-r', '-W', + '--assert', '--basetemp', '--capture', '--color', '--confcutdir', + '--cov', '--cov-config', '--cov-fail-under', '--cov-report', + '--deselect', '--dist', '--doctest-glob', + '--doctest-report', '--durations', '--ignore', '--import-mode', + '--junit-prefix', '--junit-xml', '--last-failed-no-failures', + '--lfnf', '--log-cli-date-format', '--log-cli-format', + '--log-cli-level', '--log-date-format', '--log-file', + '--log-file-date-format', '--log-file-format', '--log-file-level', + '--log-format', '--log-level', '--maxfail', '--override-ini', + '--pastebin', '--pdbcls', '--pythonwarnings', '--result-log', + '--rootdir', '--show-capture', '--tb', '--verbosity', '--max-slave-restart', + '--numprocesses', '--rsyncdir', '--rsyncignore', '--tx']; + +const OptionsWithoutArguments = ['--cache-clear', '--cache-show', '--collect-in-virtualenv', + '--collect-only', '--continue-on-collection-errors', + '--cov-append', '--cov-branch', '--debug', '--disable-pytest-warnings', + '--disable-warnings', '--doctest-continue-on-failure', '--doctest-ignore-import-errors', + '--doctest-modules', '--exitfirst', '--failed-first', '--ff', '--fixtures', + '--fixtures-per-test', '--force-sugar', '--full-trace', '--funcargs', '--help', + '--keep-duplicates', '--last-failed', '--lf', '--markers', '--new-first', '--nf', + '--no-cov', '--no-cov-on-fail', + '--no-print-logs', '--noconftest', '--old-summary', '--pdb', '--pyargs', '-PyTest, Unittest-pyargs', + '--quiet', '--runxfail', '--setup-only', '--setup-plan', '--setup-show', '--showlocals', + '--strict', '--trace-config', '--verbose', '--version', '-h', '-l', '-q', '-s', '-v', '-x', + '--boxed', '--forked', '--looponfail', '--tx', '-d']; + +@injectable() +export class ArgumentsService implements IArgumentsService { + private readonly helper: IArgumentsHelper; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.helper = serviceContainer.get(IArgumentsHelper); + } + public getKnownOptions(): { withArgs: string[]; withoutArgs: string[] } { + return { + withArgs: OptionsWithArguments, + withoutArgs: OptionsWithoutArguments + }; + } + public getOptionValue(args: string[], option: string): string | string[] | undefined { + return this.helper.getOptionValues(args, option); + } + public filterArguments(args: string[], argumentToRemoveOrFilter: string[] | TestFilter): string[] { + const optionsWithoutArgsToRemove: string[] = []; + const optionsWithArgsToRemove: string[] = []; + // Positional arguments in pytest are test directories and files. + // So if we want to run a specific test, then remove positional args. + let removePositionalArgs = false; + if (Array.isArray(argumentToRemoveOrFilter)) { + argumentToRemoveOrFilter.forEach(item => { + if (OptionsWithArguments.indexOf(item) >= 0) { + optionsWithArgsToRemove.push(item); + } + if (OptionsWithoutArguments.indexOf(item) >= 0) { + optionsWithoutArgsToRemove.push(item); + } + }); + } else { + switch (argumentToRemoveOrFilter) { + case TestFilter.removeTests: { + optionsWithoutArgsToRemove.push(...[ + '--lf', '--last-failed', + '--ff', '--failed-first', + '--nf', '--new-first' + ]); + optionsWithArgsToRemove.push(...[ + '-k', '-m', + '--lfnf', '--last-failed-no-failures' + ]); + removePositionalArgs = true; + break; + } + case TestFilter.discovery: { + optionsWithoutArgsToRemove.push(...[ + '-x', '--exitfirst', + '--fixtures', '--funcargs', + '--fixtures-per-test', '--pdb', + '--lf', '--last-failed', + '--ff', '--failed-first', + '--nf', '--new-first', + '--cache-show', + '-v', '--verbose', '-q', '-quiet', + '-l', '--showlocals', + '--no-print-logs', + '--debug', + '--setup-only', '--setup-show', '--setup-plan' + ]); + optionsWithArgsToRemove.push(...[ + '-m', '--maxfail', + '--pdbcls', '--capture', + '--lfnf', '--last-failed-no-failures', + '--verbosity', '-r', + '--tb', + '--rootdir', '--show-capture', + '--durations', + '--junit-xml', '--junit-prefix', '--result-log', + '-W', '--pythonwarnings', + '--log-*' + ]); + removePositionalArgs = true; + break; + } + case TestFilter.debugAll: + case TestFilter.runAll: { + optionsWithoutArgsToRemove.push('--collect-only'); + break; + } + case TestFilter.debugSpecific: + case TestFilter.runSpecific: { + optionsWithoutArgsToRemove.push(...[ + '--collect-only', + '--lf', '--last-failed', + '--ff', '--failed-first', + '--nf', '--new-first' + ]); + optionsWithArgsToRemove.push(...[ + '-k', '-m', + '--lfnf', '--last-failed-no-failures' + ]); + removePositionalArgs = true; + break; + } + default: { + throw new Error(`Unsupported Filter '${argumentToRemoveOrFilter}'`); + } + } + } + + let filteredArgs = args.slice(); + if (removePositionalArgs) { + const positionalArgs = this.helper.getPositionalArguments(filteredArgs, OptionsWithArguments, OptionsWithoutArguments); + filteredArgs = filteredArgs.filter(item => positionalArgs.indexOf(item) === -1); + } + return this.helper.filterArguments(filteredArgs, optionsWithArgsToRemove, optionsWithoutArgsToRemove); + } + public getTestFolders(args: string[]): string[] { + const testDirs = this.helper.getOptionValues(args, '--rootdir'); + if (typeof testDirs === 'string') { + return [testDirs]; + } + if (Array.isArray(testDirs) && testDirs.length > 0) { + return testDirs; + } + const positionalArgs = this.helper.getPositionalArguments(args, OptionsWithArguments, OptionsWithoutArguments); + // Positional args in pytest are files or directories. + // Remove files from the args, and what's left are test directories. + // If users enter test modules/methods, then its not supported. + return positionalArgs.filter(arg => !arg.toUpperCase().endsWith('.PY')); + } +} diff --git a/src/client/unittests/pytest/services/discoveryService.ts b/src/client/unittests/pytest/services/discoveryService.ts index c3fc02e131be..57d9a3902e5d 100644 --- a/src/client/unittests/pytest/services/discoveryService.ts +++ b/src/client/unittests/pytest/services/discoveryService.ts @@ -4,48 +4,67 @@ import { inject, injectable, named } from 'inversify'; import { CancellationTokenSource } from 'vscode'; import { IServiceContainer } from '../../../ioc/types'; -import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../common/constants'; -import { Options, run } from '../../common/runner'; -import { ITestDiscoveryService, ITestsParser, TestDiscoveryOptions, Tests } from '../../common/types'; - -const argsToExcludeForDiscovery = ['-x', '--exitfirst', - '--fixtures-per-test', '--pdb', '--runxfail', - '--lf', '--last-failed', '--ff', '--failed-first', - '--cache-show', '--cache-clear', - '-v', '--verbose', '-q', '-quiet', - '--disable-pytest-warnings', '-l', '--showlocals']; - -type PytestDiscoveryOptions = TestDiscoveryOptions & { - startDirectory: string; - pattern: string; -}; +import { PYTEST_PROVIDER } from '../../common/constants'; +import { ITestDiscoveryService, ITestRunner, ITestsHelper, ITestsParser, Options, TestDiscoveryOptions, Tests } from '../../common/types'; +import { IArgumentsService, TestFilter } from '../../types'; @injectable() export class TestDiscoveryService implements ITestDiscoveryService { - constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(ITestsParser) @named(PYTEST_PROVIDER) private testParser: ITestsParser) { } + private argsService: IArgumentsService; + private helper: ITestsHelper; + private runner: ITestRunner; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(ITestsParser) @named(PYTEST_PROVIDER) private testParser: ITestsParser) { + this.argsService = this.serviceContainer.get(IArgumentsService, PYTEST_PROVIDER); + this.helper = this.serviceContainer.get(ITestsHelper); + this.runner = this.serviceContainer.get(ITestRunner); + } public async discoverTests(options: TestDiscoveryOptions): Promise { - // Remove unwanted arguments - const args = options.args.filter(arg => { - if (argsToExcludeForDiscovery.indexOf(arg.trim()) !== -1) { - return false; - } - return true; - }); - if (options.ignoreCache && args.indexOf('--cache-clear') === -1) { - args.push('--cache-clear'); + const args = this.buildTestCollectionArgs(options); + + // Collect tests for each test directory separately and merge. + const testDirectories = this.argsService.getTestFolders(options.args); + if (testDirectories.length === 0) { + const opts = { + ...options, + args + }; + return this.discoverTestsInTestDirectory(opts); } + const results = await Promise.all(testDirectories.map(testDir => { + // Add test directory as a positional argument. + const opts = { + ...options, + args: [...args, testDir] + }; + return this.discoverTestsInTestDirectory(opts); + })); + return this.helper.mergeTests(results); + } + private buildTestCollectionArgs(options: TestDiscoveryOptions) { + // Remove unwnted arguments (which happen to be test directories & test specific args). + const args = this.argsService.filterArguments(options.args, TestFilter.discovery); + if (options.ignoreCache && args.indexOf('--cache-clear') === -1) { + args.splice(0, 0, '--cache-clear'); + } + if (args.indexOf('-s') === -1) { + args.splice(0, 0, '-s'); + } + args.splice(0, 0, '--collect-only'); + return args; + } + private async discoverTestsInTestDirectory(options: TestDiscoveryOptions): Promise { const token = options.token ? options.token : new CancellationTokenSource().token; const runOptions: Options = { - args: args.concat(['--collect-only']), + args: options.args, cwd: options.cwd, workspaceFolder: options.workspaceFolder, token, outChannel: options.outChannel }; - const data = await run(this.serviceContainer, PYTEST_PROVIDER, runOptions); + const data = await this.runner.run(PYTEST_PROVIDER, runOptions); if (options.token && options.token.isCancellationRequested) { return Promise.reject('cancelled'); } diff --git a/src/client/unittests/pytest/services/parserService.ts b/src/client/unittests/pytest/services/parserService.ts index f0fad3b88b12..822ab930ae04 100644 --- a/src/client/unittests/pytest/services/parserService.ts +++ b/src/client/unittests/pytest/services/parserService.ts @@ -5,13 +5,13 @@ import { inject, injectable } from 'inversify'; import * as os from 'os'; import * as path from 'path'; import { convertFileToPackage, extractBetweenDelimiters } from '../../common/testUtils'; -import { ITestsHelper, ITestsParser, ParserOptions, TestDiscoveryOptions, TestFile, TestFunction, Tests, TestStatus, TestSuite } from '../../common/types'; +import { ITestsHelper, ITestsParser, ParserOptions, TestFile, TestFunction, Tests, TestSuite } from '../../common/types'; const DELIMITER = '\''; @injectable() export class TestsParser implements ITestsParser { - constructor( @inject(ITestsHelper) private testsHelper: ITestsHelper) { } + constructor(@inject(ITestsHelper) private testsHelper: ITestsHelper) { } public parse(content: string, options: ParserOptions): Tests { const testFiles = this.getTestFiles(content, options); return this.testsHelper.flattenTestFiles(testFiles); @@ -20,7 +20,7 @@ export class TestsParser implements ITestsParser { private getTestFiles(content: string, options: ParserOptions) { let logOutputLines: string[] = ['']; const testFiles: TestFile[] = []; - const parentNodes: { indent: number, item: TestFile | TestSuite }[] = []; + const parentNodes: { indent: number; item: TestFile | TestSuite }[] = []; const errorLine = /==*( *)ERRORS( *)=*/; const errorFileLine = /__*( *)ERROR collecting (.*)/; @@ -75,7 +75,7 @@ export class TestsParser implements ITestsParser { } private parsePyTestModuleCollectionError(rootDirectory: string, lines: string[], testFiles: TestFile[], - parentNodes: { indent: number, item: TestFile | TestSuite }[]) { + parentNodes: { indent: number; item: TestFile | TestSuite }[]) { lines = lines.filter(line => line.trim().length > 0); if (lines.length <= 1) { @@ -98,7 +98,7 @@ export class TestsParser implements ITestsParser { return; } - private parsePyTestModuleCollectionResult(rootDirectory: string, lines: string[], testFiles: TestFile[], parentNodes: { indent: number, item: TestFile | TestSuite }[]) { + private parsePyTestModuleCollectionResult(rootDirectory: string, lines: string[], testFiles: TestFile[], parentNodes: { indent: number; item: TestFile | TestSuite }[]) { let currentPackage: string = ''; lines.forEach(line => { @@ -146,7 +146,7 @@ export class TestsParser implements ITestsParser { }); } - private findParentOfCurrentItem(indentOfCurrentItem: number, parentNodes: { indent: number, item: TestFile | TestSuite }[]): { indent: number, item: TestFile | TestSuite } | undefined { + private findParentOfCurrentItem(indentOfCurrentItem: number, parentNodes: { indent: number; item: TestFile | TestSuite }[]): { indent: number; item: TestFile | TestSuite } | undefined { while (parentNodes.length > 0) { const parentNode = parentNodes[parentNodes.length - 1]; if (parentNode.indent < indentOfCurrentItem) { diff --git a/src/client/unittests/pytest/testConfigurationManager.ts b/src/client/unittests/pytest/testConfigurationManager.ts index f61398ae3431..54b68718661c 100644 --- a/src/client/unittests/pytest/testConfigurationManager.ts +++ b/src/client/unittests/pytest/testConfigurationManager.ts @@ -1,31 +1,27 @@ -import * as fs from 'fs'; import * as path from 'path'; -import * as vscode from 'vscode'; -import { Uri } from 'vscode'; -import { IInstaller, Product } from '../../common/types'; +import { QuickPickItem, Uri } from 'vscode'; +import { IFileSystem } from '../../common/platform/types'; +import { Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; import { TestConfigurationManager } from '../common/managers/testConfigurationManager'; -import { ITestConfigSettingsService } from '../common/types'; export class ConfigurationManager extends TestConfigurationManager { - constructor(workspace: Uri, outputChannel: vscode.OutputChannel, - installer: IInstaller, testConfigSettingsService: ITestConfigSettingsService) { - super(workspace, Product.pytest, outputChannel, installer, testConfigSettingsService); + constructor(workspace: Uri, serviceContainer: IServiceContainer) { + super(workspace, Product.pytest, serviceContainer); } - 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 : ''); }); - }); - }); - const values = await Promise.all(promises); - return values.filter(exists => exists.length > 0); + public async requiresUserToConfigure(wkspace: Uri): Promise { + const configFiles = await this.getConfigFiles(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 false; + } + return true; } - // tslint:disable-next-line:no-any public async configure(wkspace: Uri) { const args: string[] = []; const configFileOptionLabel = 'Use existing config file'; - const options: vscode.QuickPickItem[] = []; - const configFiles = await ConfigurationManager.configFilesExist(wkspace.fsPath); + const options: QuickPickItem[] = []; + const configFiles = await this.getConfigFiles(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; @@ -48,4 +44,11 @@ export class ConfigurationManager extends TestConfigurationManager { } await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.pytest, args); } + private async getConfigFiles(rootDir: string): Promise { + const fs = this.serviceContainer.get(IFileSystem); + const promises = ['pytest.ini', 'tox.ini', 'setup.cfg'] + .map(async cfg => await fs.fileExists(path.join(rootDir, cfg)) ? cfg : ''); + const values = await Promise.all(promises); + return values.filter(exists => exists.length > 0); + } } diff --git a/src/client/unittests/serviceRegistry.ts b/src/client/unittests/serviceRegistry.ts index d500c67070dd..10b0a22cf140 100644 --- a/src/client/unittests/serviceRegistry.ts +++ b/src/client/unittests/serviceRegistry.ts @@ -3,8 +3,11 @@ import { Uri } from 'vscode'; import { IServiceContainer, IServiceManager } from '../ioc/types'; +import { ArgumentsHelper } from './common/argumentsHelper'; import { NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from './common/constants'; import { DebugLauncher } from './common/debugLauncher'; +import { TestRunner } from './common/runner'; +import { TestConfigSettingsService } from './common/services/configSettingService'; import { TestCollectionStorageService } from './common/services/storageService'; import { TestManagerService } from './common/services/testManagerService'; import { TestResultsService } from './common/services/testResultsService'; @@ -13,15 +16,31 @@ import { TestsHelper } from './common/testUtils'; import { TestFlatteningVisitor } from './common/testVisitors/flatteningVisitor'; import { TestFolderGenerationVisitor } from './common/testVisitors/folderGenerationVisitor'; import { TestResultResetVisitor } from './common/testVisitors/resultResetVisitor'; -import { ITestCollectionStorageService, ITestDebugLauncher, ITestDiscoveryService, ITestManager, ITestManagerFactory, ITestManagerService, ITestManagerServiceFactory, IUnitTestSocketServer } from './common/types'; -import { ITestResultsService, ITestsHelper, ITestsParser, ITestVisitor, IWorkspaceTestManagerService, TestProvider } from './common/types'; +import { + ITestCollectionStorageService, ITestConfigSettingsService, ITestDebugLauncher, ITestDiscoveryService, ITestManager, ITestManagerFactory, ITestManagerService, ITestManagerServiceFactory, + ITestResultsService, ITestRunner, ITestsHelper, ITestsParser, ITestVisitor, IUnitTestSocketServer, IWorkspaceTestManagerService, IXUnitParser, TestProvider +} from './common/types'; +import { XUnitParser } from './common/xUnitParser'; +import { UnitTestConfigurationService } from './configuration'; +import { TestConfigurationManagerFactory } from './configurationFactory'; +import { TestResultDisplay } from './display/main'; +import { TestDisplay } from './display/picker'; +import { UnitTestManagementService } from './main'; import { TestManager as NoseTestManager } from './nosetest/main'; +import { TestManagerRunner as NoseTestManagerRunner } from './nosetest/runner'; +import { ArgumentsService as NoseTestArgumentsService } from './nosetest/services/argsService'; import { TestDiscoveryService as NoseTestDiscoveryService } from './nosetest/services/discoveryService'; import { TestsParser as NoseTestTestsParser } from './nosetest/services/parserService'; import { TestManager as PyTestTestManager } from './pytest/main'; +import { TestManagerRunner as PytestManagerRunner } from './pytest/runner'; +import { ArgumentsService as PyTestArgumentsService } from './pytest/services/argsService'; import { TestDiscoveryService as PytestTestDiscoveryService } from './pytest/services/discoveryService'; import { TestsParser as PytestTestsParser } from './pytest/services/parserService'; +import { IArgumentsHelper, IArgumentsService, ITestConfigurationManagerFactory, ITestDisplay, ITestManagerRunner, ITestResultDisplay, IUnitTestConfigurationService, IUnitTestHelper, IUnitTestManagementService } from './types'; +import { UnitTestHelper } from './unittest/helper'; import { TestManager as UnitTestTestManager } from './unittest/main'; +import { TestManagerRunner as UnitTestTestManagerRunner } from './unittest/runner'; +import { ArgumentsService as UnitTestArgumentsService } from './unittest/services/argsService'; import { TestDiscoveryService as UnitTestTestDiscoveryService } from './unittest/services/discoveryService'; import { TestsParser as UnitTestTestsParser } from './unittest/services/parserService'; import { UnitTestSocketServer } from './unittest/socketServer'; @@ -48,6 +67,25 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.add(ITestDiscoveryService, PytestTestDiscoveryService, PYTEST_PROVIDER); serviceManager.add(ITestDiscoveryService, NoseTestDiscoveryService, NOSETEST_PROVIDER); + serviceManager.add(IArgumentsHelper, ArgumentsHelper); + serviceManager.add(ITestRunner, TestRunner); + serviceManager.add(IXUnitParser, XUnitParser); + serviceManager.add(IUnitTestHelper, UnitTestHelper); + + serviceManager.add(IArgumentsService, PyTestArgumentsService, PYTEST_PROVIDER); + serviceManager.add(IArgumentsService, NoseTestArgumentsService, NOSETEST_PROVIDER); + serviceManager.add(IArgumentsService, UnitTestArgumentsService, UNITTEST_PROVIDER); + serviceManager.add(ITestManagerRunner, PytestManagerRunner, PYTEST_PROVIDER); + serviceManager.add(ITestManagerRunner, NoseTestManagerRunner, NOSETEST_PROVIDER); + serviceManager.add(ITestManagerRunner, UnitTestTestManagerRunner, UNITTEST_PROVIDER); + + serviceManager.addSingleton(IUnitTestConfigurationService, UnitTestConfigurationService); + serviceManager.addSingleton(IUnitTestManagementService, UnitTestManagementService); + serviceManager.addSingleton(ITestResultDisplay, TestResultDisplay); + serviceManager.addSingleton(ITestDisplay, TestDisplay); + serviceManager.addSingleton(ITestConfigSettingsService, TestConfigSettingsService); + serviceManager.addSingleton(ITestConfigurationManagerFactory, TestConfigurationManagerFactory); + serviceManager.addFactory(ITestManagerFactory, (context) => { return (testProvider: TestProvider, workspaceFolder: Uri, rootDirectory: string) => { const serviceContainer = context.container.get(IServiceContainer); diff --git a/src/client/unittests/types.ts b/src/client/unittests/types.ts new file mode 100644 index 000000000000..b03bda2856eb --- /dev/null +++ b/src/client/unittests/types.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Disposable, Event, TextDocument, Uri } from 'vscode'; +import { Product } from '../common/types'; +import { PythonSymbolProvider } from '../providers/symbolProvider'; +import { CommandSource } from './common/constants'; +import { FlattenedTestFunction, ITestManager, ITestResultsService, TestFile, TestFunction, TestRunOptions, Tests, TestsToRun, UnitTestProduct } from './common/types'; + +export const IUnitTestConfigurationService = Symbol('IUnitTestConfigurationService'); +export interface IUnitTestConfigurationService { + displayTestFrameworkError(wkspace: Uri): Promise; + selectTestRunner(placeHolderMessage: string): Promise; + enableTest(wkspace: Uri, product: UnitTestProduct); +} + +export const ITestResultDisplay = Symbol('ITestResultDisplay'); + +export interface ITestResultDisplay extends Disposable { + enabled: boolean; + readonly onDidChange: Event; + displayProgressStatus(testRunResult: Promise, debug?: boolean): void; + displayDiscoverStatus(testDiscovery: Promise, quietMode?: boolean): Promise; +} + +export const ITestDisplay = Symbol('ITestDisplay'); +export interface ITestDisplay { + displayStopTestUI(workspace: Uri, message: string): void; + displayTestUI(cmdSource: CommandSource, wkspace: Uri): void; + selectTestFunction(rootDirectory: string, tests: Tests): Promise; + selectTestFile(rootDirectory: string, tests: Tests): Promise; + displayFunctionTestPickerUI(cmdSource: CommandSource, wkspace: Uri, rootDirectory: string, file: Uri, testFunctions: TestFunction[], debug?: boolean): void; +} + +export const IUnitTestManagementService = Symbol('IUnitTestManagementService'); +export interface IUnitTestManagementService { + activate(): Promise; + activateCodeLenses(symboldProvider: PythonSymbolProvider): Promise; + getTestManager(displayTestNotConfiguredMessage: boolean, resource?: Uri): Promise; + discoverTestsForDocument(doc: TextDocument): Promise; + autoDiscoverTests(): Promise; + discoverTests(cmdSource: CommandSource, resource?: Uri, ignoreCache?: boolean, userInitiated?: boolean, quietMode?: boolean): Promise; + stopTests(resource: Uri): Promise; + displayStopUI(message: string): Promise; + displayUI(cmdSource: CommandSource): Promise; + displayPickerUI(cmdSource: CommandSource, file: Uri, testFunctions: TestFunction[], debug?: boolean): Promise; + runTestsImpl(cmdSource: CommandSource, resource?: Uri, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise; + runCurrentTestFile(cmdSource: CommandSource): Promise; + + selectAndRunTestFile(cmdSource: CommandSource): Promise; + + selectAndRunTestMethod(cmdSource: CommandSource, resource: Uri, debug?: boolean): Promise; + + viewOutput(cmdSource: CommandSource): void; +} + +export interface ITestConfigurationManager { + requiresUserToConfigure(wkspace: Uri): Promise; + configure(wkspace: Uri): Promise; + enable(): Promise; + disable(): Promise; +} + +export const ITestConfigurationManagerFactory = Symbol('ITestConfigurationManagerFactory'); +export interface ITestConfigurationManagerFactory { + create(wkspace: Uri, product: Product): ITestConfigurationManager; +} + +export enum TestFilter { + removeTests = 'removeTests', + discovery = 'discovery', + runAll = 'runAll', + runSpecific = 'runSpecific', + debugAll = 'debugAll', + debugSpecific = 'debugSpecific' +} +export const IArgumentsService = Symbol('IArgumentsService'); +export interface IArgumentsService { + getKnownOptions(): { withArgs: string[]; withoutArgs: string[] }; + getOptionValue(args: string[], option: string): string | string[] | undefined; + filterArguments(args: string[], argumentToRemove: string[]): string[]; + // tslint:disable-next-line:unified-signatures + filterArguments(args: string[], filter: TestFilter): string[]; + getTestFolders(args: string[]): string[]; +} +export const IArgumentsHelper = Symbol('IArgumentsHelper'); +export interface IArgumentsHelper { + getOptionValues(args: string[], option: string): string | string[] | undefined; + filterArguments(args: string[], optionsWithArguments?: string[], optionsWithoutArguments?: string[]): string[]; + getPositionalArguments(args: string[], optionsWithArguments?: string[], optionsWithoutArguments?: string[]): string[]; +} + +export const ITestManagerRunner = Symbol('ITestManagerRunner'); +export interface ITestManagerRunner { + runTest(testResultsService: ITestResultsService, options: TestRunOptions, testManager: ITestManager): Promise; +} + +export const IUnitTestHelper = Symbol('IUnitTestHelper'); +export interface IUnitTestHelper { + getStartDirectory(args: string[]): string; + getIdsOfTestsToRun(tests: Tests, testsToRun: TestsToRun): string[]; +} diff --git a/src/client/unittests/unittest/helper.ts b/src/client/unittests/unittest/helper.ts new file mode 100644 index 000000000000..a97aed3102cf --- /dev/null +++ b/src/client/unittests/unittest/helper.ts @@ -0,0 +1,52 @@ + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IServiceContainer } from '../../ioc/types'; +import { Tests, TestsToRun } from '../common/types'; +import { IArgumentsHelper, IUnitTestHelper } from '../types'; + +@injectable() +export class UnitTestHelper implements IUnitTestHelper { + private readonly argsHelper: IArgumentsHelper; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.argsHelper = serviceContainer.get(IArgumentsHelper); + } + public getStartDirectory(args: string[]): string { + const shortValue = this.argsHelper.getOptionValues(args, '-s'); + if (typeof shortValue === 'string') { + return shortValue; + } + const longValue = this.argsHelper.getOptionValues(args, '--start-directory'); + if (typeof longValue === 'string') { + return longValue; + } + return '.'; + } + public getIdsOfTestsToRun(tests: Tests, testsToRun: TestsToRun): string[] { + const testIds: string[] = []; + if (testsToRun && testsToRun.testFolder) { + // Get test ids of files in these folders. + testsToRun.testFolder.map(folder => { + tests.testFiles.forEach(f => { + if (f.fullPath.startsWith(folder.name)) { + testIds.push(f.nameToRun); + } + }); + }); + } + if (testsToRun && testsToRun.testFile) { + testIds.push(...testsToRun.testFile.map(f => f.nameToRun)); + } + if (testsToRun && testsToRun.testSuite) { + testIds.push(...testsToRun.testSuite.map(f => f.nameToRun)); + } + if (testsToRun && testsToRun.testFunction) { + testIds.push(...testsToRun.testFunction.map(f => f.nameToRun)); + } + return testIds; + } +} diff --git a/src/client/unittests/unittest/main.ts b/src/client/unittests/unittest/main.ts index ef9a8f134879..b9056579ea0b 100644 --- a/src/client/unittests/unittest/main.ts +++ b/src/client/unittests/unittest/main.ts @@ -1,20 +1,27 @@ import { Uri } from 'vscode'; -import { PythonSettings } from '../../common/configSettings'; +import { noop } from '../../common/core.utils'; import { Product } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; +import { UNITTEST_PROVIDER } from '../common/constants'; import { BaseTestManager } from '../common/managers/baseTestManager'; -import { TestDiscoveryOptions, TestRunOptions, Tests, TestStatus, TestsToRun } from '../common/types'; -import { runTest } from './runner'; +import { ITestsHelper, TestDiscoveryOptions, TestRunOptions, Tests, TestStatus, TestsToRun } from '../common/types'; +import { IArgumentsService, ITestManagerRunner, TestFilter } from '../types'; export class TestManager extends BaseTestManager { + private readonly argsService: IArgumentsService; + private readonly helper: ITestsHelper; + private readonly runner: ITestManagerRunner; public get enabled() { - return PythonSettings.getInstance(this.workspaceFolder).unitTest.unittestEnabled; + return this.settings.unitTest.unittestEnabled; } constructor(workspaceFolder: Uri, rootDirectory: string, serviceContainer: IServiceContainer) { - super('unittest', Product.unittest, workspaceFolder, rootDirectory, serviceContainer); + super(UNITTEST_PROVIDER, Product.unittest, workspaceFolder, rootDirectory, serviceContainer); + this.argsService = this.serviceContainer.get(IArgumentsService, this.testProvider); + this.helper = this.serviceContainer.get(ITestsHelper); + this.runner = this.serviceContainer.get(ITestManagerRunner, this.testProvider); } - // tslint:disable-next-line:no-empty public configure() { + noop(); } public getDiscoveryOptions(ignoreCache: boolean): TestDiscoveryOptions { const args = this.settings.unitTest.unittestArgs.slice(0); @@ -26,7 +33,15 @@ export class TestManager extends BaseTestManager { }; } public async runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<{}> { - const args = this.settings.unitTest.unittestArgs.slice(0); + let args: string[]; + + const runAllTests = this.helper.shouldRunAllTests(testsToRun); + if (debug) { + args = this.argsService.filterArguments(this.settings.unitTest.unittestArgs, runAllTests ? TestFilter.debugAll : TestFilter.debugSpecific); + } else { + args = this.argsService.filterArguments(this.settings.unitTest.unittestArgs, runAllTests ? TestFilter.runAll : TestFilter.runSpecific); + } + if (runFailedTests === true) { testsToRun = { testFile: [], testFolder: [], testSuite: [], testFunction: [] }; testsToRun.testFunction = tests.testFunctions.filter(fn => { @@ -40,6 +55,6 @@ export class TestManager extends BaseTestManager { token: this.testRunnerCancellationToken!, outChannel: this.outputChannel }; - return runTest(this.serviceContainer, this, this.testResultsService, options); + return this.runner.runTest(this.testResultsService, options, this); } } diff --git a/src/client/unittests/unittest/runner.ts b/src/client/unittests/unittest/runner.ts index d538d640229e..214eab94a93a 100644 --- a/src/client/unittests/unittest/runner.ts +++ b/src/client/unittests/unittest/runner.ts @@ -1,9 +1,14 @@ 'use strict'; +import { inject, injectable } from 'inversify'; import * as path from 'path'; +import { EXTENSION_ROOT_DIR } from '../../common/constants'; +import { noop } from '../../common/core.utils'; +import { ILogger } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; -import { BaseTestManager } from '../common/managers/baseTestManager'; -import { Options, run } from '../common/runner'; -import { ITestDebugLauncher, ITestResultsService, IUnitTestSocketServer, TestRunOptions, Tests, TestStatus, TestsToRun } from '../common/types'; +import { UNITTEST_PROVIDER } from '../common/constants'; +import { Options } from '../common/runner'; +import { ITestDebugLauncher, ITestManager, ITestResultsService, ITestRunner, IUnitTestSocketServer, LaunchOptions, TestRunOptions, Tests, TestStatus, TestsToRun } from '../common/types'; +import { IArgumentsHelper, ITestManagerRunner, IUnitTestHelper } from '../types'; type TestStatusMap = { status: TestStatus; @@ -11,13 +16,9 @@ type TestStatusMap = { }; 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 { @@ -27,64 +28,63 @@ interface ITestData { traceback: string; } -// tslint:disable-next-line:max-func-body-length -export async function runTest(serviceContainer: IServiceContainer, testManager: BaseTestManager, testResultsService: ITestResultsService, options: TestRunOptions): Promise { - options.tests.summary.errors = 0; - options.tests.summary.failures = 0; - options.tests.summary.passed = 0; - options.tests.summary.skipped = 0; - let failFast = false; - const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'visualstudio_py_testlauncher.py'); - const server = serviceContainer.get(IUnitTestSocketServer); - 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 no-any - server.on('connect', (data: any) => { - }); - // tslint:disable-next-line:no-empty - server.on('start', (data: { test: string }) => { - }); - server.on('result', (data: ITestData) => { - const test = options.tests.testFunctions.find(t => t.testFunction.nameToRun === data.test); - const statusDetails = outcomeMapping.get(data.outcome)!; - if (test) { - test.testFunction.status = statusDetails.status; - test.testFunction.message = data.message; - test.testFunction.traceback = data.traceback; - options.tests.summary[statusDetails.summaryProperty] += 1; - - if (failFast && (statusDetails.summaryProperty === 'failures' || statusDetails.summaryProperty === 'errors')) { - testManager.stop(); - } - } else { - if (statusDetails) { +@injectable() +export class TestManagerRunner implements ITestManagerRunner { + private readonly argsHelper: IArgumentsHelper; + private readonly helper: IUnitTestHelper; + private readonly testRunner: ITestRunner; + private readonly server: IUnitTestSocketServer; + private readonly logger: ILogger; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.argsHelper = serviceContainer.get(IArgumentsHelper); + this.testRunner = serviceContainer.get(ITestRunner); + this.server = this.serviceContainer.get(IUnitTestSocketServer); + this.logger = this.serviceContainer.get(ILogger); + this.helper = this.serviceContainer.get(IUnitTestHelper); + } + public async runTest(testResultsService: ITestResultsService, options: TestRunOptions, testManager: ITestManager): Promise { + options.tests.summary.errors = 0; + options.tests.summary.failures = 0; + options.tests.summary.passed = 0; + options.tests.summary.skipped = 0; + let failFast = false; + const testLauncherFile = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'PythonTools', 'visualstudio_py_testlauncher.py'); + this.server.on('error', (message: string, ...data: string[]) => this.logger.logError(`${message} ${data.join(' ')}`)); + this.server.on('log', noop); + this.server.on('connect', noop); + this.server.on('start', noop); + this.server.on('socket.disconnected', noop); + this.server.on('result', (data: ITestData) => { + const test = options.tests.testFunctions.find(t => t.testFunction.nameToRun === data.test); + const statusDetails = outcomeMapping.get(data.outcome)!; + if (test) { + test.testFunction.status = statusDetails.status; + test.testFunction.message = data.message; + test.testFunction.traceback = data.traceback; options.tests.summary[statusDetails.summaryProperty] += 1; + + if (failFast && (statusDetails.summaryProperty === 'failures' || statusDetails.summaryProperty === 'errors')) { + testManager.stop(); + } + } else { + if (statusDetails) { + options.tests.summary[statusDetails.summaryProperty] += 1; + } } - } - }); - // tslint:disable-next-line:no-empty no-any - server.on('socket.disconnected', (data: any) => { - }); + }); - return server.start().then(port => { - const testPaths: string[] = getIdsOfTestsToRun(options.tests, options.testsToRun!); + const port = await this.server.start(); + const testPaths: string[] = this.helper.getIdsOfTestsToRun(options.tests, options.testsToRun!); for (let counter = 0; counter < testPaths.length; counter += 1) { testPaths[counter] = `-t${testPaths[counter].trim()}`; } - const startTestDiscoveryDirectory = getStartDirectory(options.args); - function runTestInternal(testFile: string = '', testId: string = '') { - let testArgs = buildTestArgs(options.args); + const runTestInternal = async (testFile: string = '', testId: string = '') => { + let testArgs = this.buildTestArgs(options.args); failFast = testArgs.indexOf('--uf') >= 0; testArgs = testArgs.filter(arg => arg !== '--uf'); testArgs.push(`--result-port=${port}`); - testArgs.push(`--us=${startTestDiscoveryDirectory}`); if (testId.length > 0) { testArgs.push(`-t${testId}`); } @@ -92,13 +92,11 @@ export async function runTest(serviceContainer: IServiceContainer, testManager: testArgs.push(`--testFile=${testFile}`); } if (options.debug === true) { - const debugLauncher = serviceContainer.get(ITestDebugLauncher); - testArgs.push(...['--debug']); - const launchOptions = { cwd: options.cwd, args: [testLauncherFile].concat(testArgs), token: options.token, outChannel: options.outChannel}; - // tslint:disable-next-line:prefer-type-cast no-any + const debugLauncher = this.serviceContainer.get(ITestDebugLauncher); + testArgs.push('--debug'); + const launchOptions: LaunchOptions = { cwd: options.cwd, args: testArgs, token: options.token, outChannel: options.outChannel, testProvider: UNITTEST_PROVIDER }; return debugLauncher.launchDebugger(launchOptions); } else { - // tslint:disable-next-line:prefer-type-cast no-any const runOptions: Options = { args: [testLauncherFile].concat(testArgs), cwd: options.cwd, @@ -106,109 +104,57 @@ export async function runTest(serviceContainer: IServiceContainer, testManager: token: options.token, workspaceFolder: options.workspaceFolder }; - return run(serviceContainer, 'unittest', runOptions); + await this.testRunner.run(UNITTEST_PROVIDER, runOptions); } - } + }; - // Test everything + // Test everything. if (testPaths.length === 0) { - return runTestInternal(); + await runTestInternal(); } - // Ok, the ptvs test runner can only work with one test at a time - let promise = Promise.resolve(''); - if (Array.isArray(options.testsToRun!.testFile)) { - options.testsToRun!.testFile!.forEach(testFile => { - // tslint:disable-next-line:prefer-type-cast no-any - promise = promise.then(() => runTestInternal(testFile.fullPath, testFile.nameToRun) as Promise); - }); - } - if (Array.isArray(options.testsToRun!.testSuite)) { - options.testsToRun!.testSuite!.forEach(testSuite => { - const testFileName = options.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(options.testsToRun!.testFunction)) { - options.testsToRun!.testFunction!.forEach(testFn => { - const testFileName = options.tests.testFunctions.find(t => t.testFunction === testFn)!.parentTestFile.fullPath; - // tslint:disable-next-line:prefer-type-cast no-any - promise = promise.then(() => runTestInternal(testFileName, testFn.nameToRun) as Promise); - }); + // Ok, the test runner can only work with one test at a time. + if (options.testsToRun) { + let promise = Promise.resolve(undefined); + if (Array.isArray(options.testsToRun.testFile)) { + options.testsToRun.testFile.forEach(testFile => { + promise = promise.then(() => runTestInternal(testFile.fullPath, testFile.nameToRun)); + }); + } + if (Array.isArray(options.testsToRun.testSuite)) { + options.testsToRun.testSuite.forEach(testSuite => { + const testFileName = options.tests.testSuites.find(t => t.testSuite === testSuite)!.parentTestFile.fullPath; + promise = promise.then(() => runTestInternal(testFileName, testSuite.nameToRun)); + }); + } + if (Array.isArray(options.testsToRun.testFunction)) { + options.testsToRun.testFunction.forEach(testFn => { + const testFileName = options.tests.testFunctions.find(t => t.testFunction === testFn)!.parentTestFile.fullPath; + promise = promise.then(() => runTestInternal(testFileName, testFn.nameToRun)); + }); + } + await promise; } - // tslint:disable-next-line:prefer-type-cast no-any - return promise as Promise; - }).then(() => { + testResultsService.updateResults(options.tests); return options.tests; - }).catch(reason => { - return Promise.reject(reason); - }); -} -function getStartDirectory(args: string[]): string { - let startDirectory = '.'; - const indexOfStartDir = args.findIndex(arg => arg.indexOf('-s') === 0 || arg.indexOf('--start-directory') === 0); - if (indexOfStartDir >= 0) { - const startDir = args[indexOfStartDir].trim(); - if ((startDir.trim() === '-s' || startDir.trim() === '--start-directory') && args.length >= indexOfStartDir) { - // Assume the next items is the directory - startDirectory = args[indexOfStartDir + 1]; - } else { - const lenToStartFrom = startDir.startsWith('-s') ? '-s'.length : '--start-directory'.length; - startDirectory = startDir.substring(lenToStartFrom).trim(); - if (startDirectory.startsWith('=')) { - startDirectory = startDirectory.substring(1); - } - } } - return startDirectory; -} -function buildTestArgs(args: string[]): string[] { - const startTestDiscoveryDirectory = getStartDirectory(args); - let pattern = 'test*.py'; - const indexOfPattern = args.findIndex(arg => arg.indexOf('-p') === 0 || arg.indexOf('--pattern') === 0); - if (indexOfPattern >= 0) { - const patternValue = args[indexOfPattern].trim(); - if ((patternValue.trim() === '-p' || patternValue.trim() === '--pattern') && args.length >= indexOfPattern) { - // Assume the next items is the directory - pattern = args[indexOfPattern + 1]; - } else { - const lenToStartFrom = patternValue.startsWith('-p') ? '-p'.length : '--pattern'.length; - pattern = patternValue.substring(lenToStartFrom).trim(); - if (pattern.startsWith('=')) { - pattern = pattern.substring(1); - } + private buildTestArgs(args: string[]): string[] { + const startTestDiscoveryDirectory = this.helper.getStartDirectory(args); + let pattern = 'test*.py'; + const shortValue = this.argsHelper.getOptionValues(args, '-p'); + const longValueValue = this.argsHelper.getOptionValues(args, '-pattern'); + if (typeof shortValue === 'string') { + pattern = shortValue; + } else if (typeof longValueValue === 'string') { + pattern = longValueValue; } + const failFast = args.some(arg => arg.trim() === '-f' || arg.trim() === '--failfast'); + const verbosity = args.some(arg => arg.trim().indexOf('-v') === 0) ? 2 : 1; + const testArgs = [`--us=${startTestDiscoveryDirectory}`, `--up=${pattern}`, `--uvInt=${verbosity}`]; + if (failFast) { + testArgs.push('--uf'); + } + return testArgs; } - const failFast = args.some(arg => arg.trim() === '-f' || arg.trim() === '--failfast'); - const verbosity = args.some(arg => arg.trim().indexOf('-v') === 0) ? 2 : 1; - const testArgs = [`--us=${startTestDiscoveryDirectory}`, `--up=${pattern}`, `--uvInt=${verbosity}`]; - if (failFast) { - testArgs.push('--uf'); - } - return testArgs; -} -function getIdsOfTestsToRun(tests: Tests, testsToRun: TestsToRun): string[] { - const testIds: string[] = []; - if (testsToRun && testsToRun.testFolder) { - // Get test ids of files in these folders - testsToRun.testFolder.map(folder => { - tests.testFiles.forEach(f => { - if (f.fullPath.startsWith(folder.name)) { - testIds.push(f.nameToRun); - } - }); - }); - } - if (testsToRun && testsToRun.testFile) { - testIds.push(...testsToRun.testFile.map(f => f.nameToRun)); - } - if (testsToRun && testsToRun.testSuite) { - testIds.push(...testsToRun.testSuite.map(f => f.nameToRun)); - } - if (testsToRun && testsToRun.testFunction) { - testIds.push(...testsToRun.testFunction.map(f => f.nameToRun)); - } - return testIds; } diff --git a/src/client/unittests/unittest/services/argsService.ts b/src/client/unittests/unittest/services/argsService.ts new file mode 100644 index 000000000000..26b530da23d7 --- /dev/null +++ b/src/client/unittests/unittest/services/argsService.ts @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IServiceContainer } from '../../../ioc/types'; +import { IArgumentsHelper, IArgumentsService, TestFilter } from '../../types'; + +const OptionsWithArguments = ['-k', '-p', '-s', '-t', '--pattern', + '--start-directory', '--top-level-directory']; + +const OptionsWithoutArguments = ['-b', '-c', '-f', '-h', '-q', '-v', + '--buffer', '--catch', '--failfast', '--help', '--locals', + '--quiet', '--verbose']; + +@injectable() +export class ArgumentsService implements IArgumentsService { + private readonly helper: IArgumentsHelper; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.helper = serviceContainer.get(IArgumentsHelper); + } + public getKnownOptions(): { withArgs: string[]; withoutArgs: string[] } { + return { + withArgs: OptionsWithArguments, + withoutArgs: OptionsWithoutArguments + }; + } + public getOptionValue(args: string[], option: string): string | string[] | undefined { + return this.helper.getOptionValues(args, option); + } + public filterArguments(args: string[], argumentToRemoveOrFilter: string[] | TestFilter): string[] { + const optionsWithoutArgsToRemove: string[] = []; + const optionsWithArgsToRemove: string[] = []; + // Positional arguments in pytest positional args are test directories and files. + // So if we want to run a specific test, then remove positional args. + let removePositionalArgs = false; + if (Array.isArray(argumentToRemoveOrFilter)) { + argumentToRemoveOrFilter.forEach(item => { + if (OptionsWithArguments.indexOf(item) >= 0) { + optionsWithArgsToRemove.push(item); + } + if (OptionsWithoutArguments.indexOf(item) >= 0) { + optionsWithoutArgsToRemove.push(item); + } + }); + } else { + removePositionalArgs = true; + } + + let filteredArgs = args.slice(); + if (removePositionalArgs) { + const positionalArgs = this.helper.getPositionalArguments(filteredArgs, OptionsWithArguments, OptionsWithoutArguments); + filteredArgs = filteredArgs.filter(item => positionalArgs.indexOf(item) === -1); + } + return this.helper.filterArguments(filteredArgs, optionsWithArgsToRemove, optionsWithoutArgsToRemove); + } + public getTestFolders(args: string[]): string[] { + const shortValue = this.helper.getOptionValues(args, '-s'); + if (typeof shortValue === 'string') { + return [shortValue]; + } + const longValue = this.helper.getOptionValues(args, '--start-directory'); + if (typeof longValue === 'string') { + return [longValue]; + } + return ['.']; + } +} diff --git a/src/client/unittests/unittest/services/discoveryService.ts b/src/client/unittests/unittest/services/discoveryService.ts index cff7561c4420..fdf28cff3c93 100644 --- a/src/client/unittests/unittest/services/discoveryService.ts +++ b/src/client/unittests/unittest/services/discoveryService.ts @@ -4,8 +4,9 @@ import { inject, injectable, named } from 'inversify'; import { IServiceContainer } from '../../../ioc/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; -import { Options, run } from '../../common/runner'; -import { ITestDiscoveryService, ITestsParser, TestDiscoveryOptions, Tests } from '../../common/types'; +import { Options } from '../../common/runner'; +import { ITestDiscoveryService, ITestRunner, ITestsParser, TestDiscoveryOptions, Tests } from '../../common/types'; +import { IArgumentsHelper } from '../../types'; type UnitTestDiscoveryOptions = TestDiscoveryOptions & { startDirectory: string; @@ -14,8 +15,13 @@ type UnitTestDiscoveryOptions = TestDiscoveryOptions & { @injectable() export class TestDiscoveryService implements ITestDiscoveryService { - constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(ITestsParser) @named(UNITTEST_PROVIDER) private testParser: ITestsParser) { } + private readonly argsHelper: IArgumentsHelper; + private readonly runner: ITestRunner; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(ITestsParser) @named(UNITTEST_PROVIDER) private testParser: ITestsParser) { + this.argsHelper = serviceContainer.get(IArgumentsHelper); + this.runner = serviceContainer.get(ITestRunner); + } public async discoverTests(options: TestDiscoveryOptions): Promise { const pythonScript = this.getDiscoveryScript(options); const unitTestOptions = this.translateOptions(options); @@ -27,7 +33,7 @@ export class TestDiscoveryService implements ITestDiscoveryService { outChannel: options.outChannel }; - const data = await run(this.serviceContainer, UNITTEST_PROVIDER, runOptions); + const data = await this.runner.run(UNITTEST_PROVIDER, runOptions); if (options.token && options.token.isCancellationRequested) { return Promise.reject('cancelled'); @@ -51,43 +57,32 @@ for suite in suites._tests: pass`; } public translateOptions(options: TestDiscoveryOptions): UnitTestDiscoveryOptions { - const unitTestOptions = { ...options } as UnitTestDiscoveryOptions; - unitTestOptions.startDirectory = this.getStartDirectory(options); - unitTestOptions.pattern = this.getTestPattern(options); - return unitTestOptions; + return { + ...options, + startDirectory: this.getStartDirectory(options), + pattern: this.getTestPattern(options) + }; } private getStartDirectory(options: TestDiscoveryOptions) { - let startDirectory = '.'; - const indexOfStartDir = options.args.findIndex(arg => arg.indexOf('-s') === 0); - if (indexOfStartDir >= 0) { - const startDir = options.args[indexOfStartDir].trim(); - if (startDir.trim() === '-s' && options.args.length >= indexOfStartDir) { - // Assume the next items is the directory - startDirectory = options.args[indexOfStartDir + 1]; - } else { - startDirectory = startDir.substring(2).trim(); - if (startDirectory.startsWith('=') || startDirectory.startsWith(' ')) { - startDirectory = startDirectory.substring(1); - } - } + const shortValue = this.argsHelper.getOptionValues(options.args, '-s'); + if (typeof shortValue === 'string') { + return shortValue; } - return startDirectory; + const longValue = this.argsHelper.getOptionValues(options.args, '--start-directory'); + if (typeof longValue === 'string') { + return longValue; + } + return '.'; } private getTestPattern(options: TestDiscoveryOptions) { - let pattern = 'test*.py'; - const indexOfPattern = options.args.findIndex(arg => arg.indexOf('-p') === 0); - if (indexOfPattern >= 0) { - const patternValue = options.args[indexOfPattern].trim(); - if (patternValue.trim() === '-p' && options.args.length >= indexOfPattern) { - // Assume the next items is the directory - pattern = options.args[indexOfPattern + 1]; - } else { - pattern = patternValue.substring(2).trim(); - if (pattern.startsWith('=')) { - pattern = pattern.substring(1); - } - } + const shortValue = this.argsHelper.getOptionValues(options.args, '-p'); + if (typeof shortValue === 'string') { + return shortValue; + } + const longValue = this.argsHelper.getOptionValues(options.args, '--pattern'); + if (typeof longValue === 'string') { + return longValue; } - return pattern; + return 'test*.py'; } } diff --git a/src/client/unittests/unittest/services/parserService.ts b/src/client/unittests/unittest/services/parserService.ts index 8c26d2ea05d0..6e99f1044d3a 100644 --- a/src/client/unittests/unittest/services/parserService.ts +++ b/src/client/unittests/unittest/services/parserService.ts @@ -3,13 +3,13 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { ITestsHelper, ITestsParser, TestDiscoveryOptions, TestFile, TestFunction, Tests, TestStatus, TestSuite } from '../../common/types'; - -type UnitTestParserOptions = TestDiscoveryOptions & { startDirectory: string }; +import { ITestsHelper, ITestsParser, TestFile, + TestFunction, Tests, TestStatus, + UnitTestParserOptions } from '../../common/types'; @injectable() export class TestsParser implements ITestsParser { - constructor( @inject(ITestsHelper) private testsHelper: ITestsHelper) { } + constructor(@inject(ITestsHelper) private testsHelper: ITestsHelper) { } public parse(content: string, options: UnitTestParserOptions): Tests { const testIds = this.getTestIds(content); let testsDirectory = options.cwd; @@ -21,7 +21,7 @@ export class TestsParser implements ITestsParser { private getTestIds(content: string): string[] { let startedCollecting = false; return content.split(/\r?\n/g) - .map((line, index) => { + .map(line => { if (!startedCollecting) { if (line === 'start') { startedCollecting = true; @@ -34,13 +34,22 @@ export class TestsParser implements ITestsParser { } private parseTestIds(rootDirectory: string, testIds: string[]): Tests { const testFiles: TestFile[] = []; - testIds.forEach(testId => { - this.addTestId(rootDirectory, testId, testFiles); - }); + testIds.forEach(testId => this.addTestId(rootDirectory, testId, testFiles)); return this.testsHelper.flattenTestFiles(testFiles); } + /** + * Add the test Ids into the array provided. + * TestIds are fully qualified including the method names. + * E.g. tone_test.Failing2Tests.test_failure + * Where tone_test = folder, Failing2Tests = class/suite, test_failure = method. + * @private + * @param {string} rootDirectory + * @param {string[]} testIds + * @returns {Tests} + * @memberof TestsParser + */ private addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) { const testIdParts = testId.split('.'); // We must have a file, class and function name @@ -51,6 +60,7 @@ export class TestsParser implements ITestsParser { const paths = testIdParts.slice(0, testIdParts.length - 2); const filePath = `${path.join(rootDirectory, ...paths)}.py`; const functionName = testIdParts.pop()!; + const suiteToRun = testIdParts.join('.'); const className = testIdParts.pop()!; // Check if we already have this test file @@ -59,11 +69,9 @@ export class TestsParser implements ITestsParser { testFile = { name: path.basename(filePath), fullPath: filePath, - // tslint:disable-next-line:prefer-type-cast - functions: [] as TestFunction[], - // tslint:disable-next-line:prefer-type-cast - suites: [] as TestSuite[], - nameToRun: `${className}.${functionName}`, + functions: [], + suites: [], + nameToRun: `${suiteToRun}.${functionName}`, xmlName: '', status: TestStatus.Idle, time: 0 @@ -71,19 +79,17 @@ export class TestsParser implements ITestsParser { testFiles.push(testFile); } - // Check if we already have this test file - const classNameToRun = className; - let testSuite = testFile.suites.find(cls => cls.nameToRun === classNameToRun); + // Check if we already have this suite + // nameToRun = testId - method name + let testSuite = testFile.suites.find(cls => cls.nameToRun === suiteToRun); if (!testSuite) { testSuite = { name: className, - // tslint:disable-next-line:prefer-type-cast - functions: [] as TestFunction[], - // tslint:disable-next-line:prefer-type-cast - suites: [] as TestSuite[], + functions: [], + suites: [], isUnitTest: true, isInstance: false, - nameToRun: classNameToRun, + nameToRun: suiteToRun, xmlName: '', status: TestStatus.Idle, time: 0 diff --git a/src/client/unittests/unittest/socketServer.ts b/src/client/unittests/unittest/socketServer.ts index da83a350e61a..c75e33c46c4a 100644 --- a/src/client/unittests/unittest/socketServer.ts +++ b/src/client/unittests/unittest/socketServer.ts @@ -29,7 +29,7 @@ export class UnitTestSocketServer extends EventEmitter implements IUnitTestSocke this.server = undefined; } } - public start(options: { port?: number, host?: string } = { port: 0, host: 'localhost' }): Promise { + public start(options: { port?: number; host?: string } = { port: 0, host: 'localhost' }): Promise { this.startedDef = createDeferred(); this.server = net.createServer(this.connectionListener.bind(this)); this.server!.maxConnections = MaxConnections; diff --git a/src/client/unittests/unittest/testConfigurationManager.ts b/src/client/unittests/unittest/testConfigurationManager.ts index 6993b0e0f980..54416559ca30 100644 --- a/src/client/unittests/unittest/testConfigurationManager.ts +++ b/src/client/unittests/unittest/testConfigurationManager.ts @@ -1,14 +1,15 @@ -import { OutputChannel, Uri } from 'vscode'; -import { IInstaller, Product } from '../../common/types'; +import { Uri } from 'vscode'; +import { Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; import { TestConfigurationManager } from '../common/managers/testConfigurationManager'; -import { ITestConfigSettingsService } from '../common/types'; export class ConfigurationManager extends TestConfigurationManager { - constructor(workspace: Uri, outputChannel: OutputChannel, - installer: IInstaller, testConfigSettingsService: ITestConfigSettingsService) { - super(workspace, Product.unittest, outputChannel, installer, testConfigSettingsService); + constructor(workspace: Uri, serviceContainer: IServiceContainer) { + super(workspace, Product.unittest, serviceContainer); + } + public async requiresUserToConfigure(_wkspace: Uri): Promise { + return true; } - // tslint:disable-next-line:no-any public async configure(wkspace: Uri) { const args = ['-v']; const subDirs = await this.getTestDirs(wkspace.fsPath); diff --git a/src/client/workspaceSymbols/generator.ts b/src/client/workspaceSymbols/generator.ts index 441605860357..86ea33be0eef 100644 --- a/src/client/workspaceSymbols/generator.ts +++ b/src/client/workspaceSymbols/generator.ts @@ -1,15 +1,15 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as vscode from 'vscode'; +import { Disposable, OutputChannel, Uri, window } from 'vscode'; import { PythonSettings } from '../common/configSettings'; -import { IProcessService } from '../common/process/types'; +import { IProcessServiceFactory } from '../common/process/types'; import { IPythonSettings } from '../common/types'; import { captureTelemetry } from '../telemetry'; import { WORKSPACE_SYMBOLS_BUILD } from '../telemetry/constants'; -export class Generator implements vscode.Disposable { +export class Generator implements Disposable { private optionsFile: string; - private disposables: vscode.Disposable[]; + private disposables: Disposable[]; private pythonSettings: IPythonSettings; public get tagFilePath(): string { return this.pythonSettings.workspaceSymbols.tagFilePath; @@ -17,8 +17,8 @@ export class Generator implements vscode.Disposable { public get enabled(): boolean { return this.pythonSettings.workspaceSymbols.enabled; } - constructor(public readonly workspaceFolder: vscode.Uri, private output: vscode.OutputChannel, - private processService: IProcessService) { + constructor(public readonly workspaceFolder: Uri, private readonly output: OutputChannel, + private readonly processServiceFactory: IProcessServiceFactory) { this.disposables = []; this.optionsFile = path.join(__dirname, '..', '..', '..', 'resources', 'ctagOptions'); this.pythonSettings = PythonSettings.getInstance(workspaceFolder); @@ -31,7 +31,7 @@ export class Generator implements vscode.Disposable { if (!this.pythonSettings.workspaceSymbols.enabled) { return; } - return await this.generateTags({ directory: this.workspaceFolder.fsPath }); + return this.generateTags({ directory: this.workspaceFolder.fsPath }); } private buildCmdArgs(): string[] { const exclusions = this.pythonSettings.workspaceSymbols.exclusionPatterns; @@ -40,7 +40,7 @@ export class Generator implements vscode.Disposable { return [`--options=${this.optionsFile}`, '--languages=Python'].concat(excludes); } @captureTelemetry(WORKSPACE_SYMBOLS_BUILD) - private generateTags(source: { directory?: string, file?: string }): Promise { + 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(); @@ -60,8 +60,9 @@ export class Generator implements vscode.Disposable { 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 result = this.processService.execObservable(cmd, args, { cwd: source.directory }); + const promise = new Promise(async (resolve, reject) => { + const processService = await this.processServiceFactory.create(); + const result = processService.execObservable(cmd, args, { cwd: source.directory }); let errorMsg = ''; result.out.subscribe(output => { if (output.source === 'stderr') { @@ -79,7 +80,7 @@ export class Generator implements vscode.Disposable { }); }); - vscode.window.setStatusBarMessage('Generating Tags', promise); + window.setStatusBarMessage('Generating Tags', promise); return promise; } diff --git a/src/client/workspaceSymbols/main.ts b/src/client/workspaceSymbols/main.ts index 9077e33a09d1..59919555ba11 100644 --- a/src/client/workspaceSymbols/main.ts +++ b/src/client/workspaceSymbols/main.ts @@ -1,8 +1,7 @@ -import * as vscode from 'vscode'; -import { OutputChannel, workspace } from 'vscode'; +import { CancellationToken, commands, Disposable, languages, OutputChannel, workspace } from 'vscode'; import { Commands, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; import { isNotInstalledError } from '../common/helpers'; -import { IProcessService } from '../common/process/types'; +import { IProcessServiceFactory } from '../common/process/types'; import { IInstaller, InstallerResponse, IOutputChannel, Product } from '../common/types'; import { fsExistsAsync } from '../common/utils'; import { IServiceContainer } from '../ioc/types'; @@ -11,20 +10,18 @@ import { WorkspaceSymbolProvider } from './provider'; const MAX_NUMBER_OF_ATTEMPTS_TO_INSTALL_AND_BUILD = 2; -export class WorkspaceSymbols implements vscode.Disposable { - private disposables: vscode.Disposable[]; +export class WorkspaceSymbols implements Disposable { + private disposables: Disposable[]; private generators: Generator[] = []; private readonly outputChannel: OutputChannel; - // tslint:disable-next-line:no-any - private timeout: any; constructor(private serviceContainer: IServiceContainer) { this.outputChannel = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); this.disposables = []; this.disposables.push(this.outputChannel); this.registerCommands(); this.initializeGenerators(); - vscode.languages.registerWorkspaceSymbolProvider(new WorkspaceSymbolProvider(this.generators, this.outputChannel)); - this.disposables.push(vscode.workspace.onDidChangeWorkspaceFolders(() => this.initializeGenerators())); + languages.registerWorkspaceSymbolProvider(new WorkspaceSymbolProvider(this.generators, this.outputChannel)); + this.disposables.push(workspace.onDidChangeWorkspaceFolders(() => this.initializeGenerators())); } public dispose() { this.disposables.forEach(d => d.dispose()); @@ -35,21 +32,21 @@ export class WorkspaceSymbols implements vscode.Disposable { generator.dispose(); } - if (Array.isArray(vscode.workspace.workspaceFolders)) { - vscode.workspace.workspaceFolders.forEach(wkSpc => { - const processService = this.serviceContainer.get(IProcessService); - this.generators.push(new Generator(wkSpc.uri, this.outputChannel, processService)); + if (Array.isArray(workspace.workspaceFolders)) { + workspace.workspaceFolders.forEach(wkSpc => { + const processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + this.generators.push(new Generator(wkSpc.uri, this.outputChannel, processServiceFactory)); }); } } private registerCommands() { - this.disposables.push(vscode.commands.registerCommand(Commands.Build_Workspace_Symbols, async (rebuild: boolean = true, token?: vscode.CancellationToken) => { + this.disposables.push(commands.registerCommand(Commands.Build_Workspace_Symbols, async (rebuild: boolean = true, token?: CancellationToken) => { const promises = this.buildWorkspaceSymbols(rebuild, token); return Promise.all(promises); })); } // tslint:disable-next-line:no-any - private buildWorkspaceSymbols(rebuild: boolean = true, token?: vscode.CancellationToken): Promise[] { + private buildWorkspaceSymbols(rebuild: boolean = true, token?: CancellationToken): Promise[] { if (token && token.isCancellationRequested) { return []; } diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json index d0a948e74069..ba9c239e801d 100644 --- a/src/test/.vscode/settings.json +++ b/src/test/.vscode/settings.json @@ -5,16 +5,11 @@ "python.unitTest.nosetestArgs": [], "python.unitTest.pyTestArgs": [], "python.unitTest.unittestArgs": [ - "-v", - "-s", - ".", - "-p", - "*test*.py" + "-s=./tests", + "-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, @@ -22,5 +17,6 @@ "python.linting.pylamaEnabled": false, "python.linting.mypyEnabled": false, "python.formatting.provider": "yapf", - "python.linting.pylintUseMinimalCheckers": false -} \ No newline at end of file + "python.linting.pylintUseMinimalCheckers": false, + "python.pythonPath": "python" +} diff --git a/src/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts new file mode 100644 index 000000000000..e365e7642369 --- /dev/null +++ b/src/test/activation/activationService.unit.test.ts @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import * as TypeMoq from 'typemoq'; +import { ConfigurationChangeEvent, Disposable } from 'vscode'; +import { ExtensionActivationService } from '../../client/activation/activationService'; +import { ExtensionActivators, IExtensionActivationService, IExtensionActivator } from '../../client/activation/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; +import { isLanguageServerTest } from '../../client/common/constants'; +import { IConfigurationService, IDisposableRegistry, IOutputChannel, IPythonSettings } from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; + +suite('Activation - ActivationService', () => { + [true, false].forEach(jediIsEnabled => { + suite(`Jedi is ${jediIsEnabled ? 'enabled' : 'disabled'}`, () => { + let serviceContainer: TypeMoq.IMock; + let pythonSettings: TypeMoq.IMock; + let appShell: TypeMoq.IMock; + let cmdManager: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + setup(function () { + if (isLanguageServerTest()) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + serviceContainer = TypeMoq.Mock.ofType(); + appShell = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + cmdManager = TypeMoq.Mock.ofType(); + const configService = TypeMoq.Mock.ofType(); + pythonSettings = TypeMoq.Mock.ofType(); + + workspaceService.setup(w => w.hasWorkspaceFolders).returns(() => false); + workspaceService.setup(w => w.workspaceFolders).returns(() => []); + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + + const output = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isAny())).returns(() => output.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => []); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICommandManager))).returns(() => cmdManager.object); + }); + + async function testActivation(activationService: IExtensionActivationService, activator: TypeMoq.IMock) { + activator + .setup(a => a.activate()).returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + const activatorName = jediIsEnabled ? ExtensionActivators.Jedi : ExtensionActivators.DotNet; + serviceContainer + .setup(c => c.get(TypeMoq.It.isValue(IExtensionActivator), TypeMoq.It.isValue(activatorName))) + .returns(() => activator.object) + .verifiable(TypeMoq.Times.once()); + + await activationService.activate(); + + activator.verifyAll(); + serviceContainer.verifyAll(); + } + test('Activatory must be activated', async () => { + pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); + const activator = TypeMoq.Mock.ofType(); + const activationService = new ExtensionActivationService(serviceContainer.object); + + await testActivation(activationService, activator); + }); + test('Activatory must be deactivated', async () => { + pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); + const activator = TypeMoq.Mock.ofType(); + const activationService = new ExtensionActivationService(serviceContainer.object); + + await testActivation(activationService, activator); + + activator + .setup(a => a.deactivate()).returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + activationService.dispose(); + activator.verifyAll(); + }); + test('Prompt user to reload VS Code and reload, when setting is toggled', async () => { + let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; + let jediIsEnabledValueInSetting = jediIsEnabled; + workspaceService + .setup(w => w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback(cb => callbackHandler = cb) + .returns(() => TypeMoq.Mock.ofType().object) + .verifiable(TypeMoq.Times.once()); + + pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabledValueInSetting); + const activator = TypeMoq.Mock.ofType(); + const activationService = new ExtensionActivationService(serviceContainer.object); + + workspaceService.verifyAll(); + await testActivation(activationService, activator); + + const event = TypeMoq.Mock.ofType(); + event.setup(e => e.affectsConfiguration(TypeMoq.It.isValue('python.jediEnabled'), TypeMoq.It.isAny())) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) + .returns(() => Promise.resolve('Reload')) + .verifiable(TypeMoq.Times.once()); + cmdManager.setup(c => c.executeCommand(TypeMoq.It.isValue('workbench.action.reloadWindow'))) + .verifiable(TypeMoq.Times.once()); + + // Toggle the value in the setting and invoke the callback. + jediIsEnabledValueInSetting = !jediIsEnabledValueInSetting; + await callbackHandler(event.object); + + event.verifyAll(); + appShell.verifyAll(); + cmdManager.verifyAll(); + }); + test('Prompt user to reload VS Code and do not reload, when setting is toggled', async () => { + let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; + let jediIsEnabledValueInSetting = jediIsEnabled; + workspaceService + .setup(w => w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback(cb => callbackHandler = cb) + .returns(() => TypeMoq.Mock.ofType().object) + .verifiable(TypeMoq.Times.once()); + + pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabledValueInSetting); + const activator = TypeMoq.Mock.ofType(); + const activationService = new ExtensionActivationService(serviceContainer.object); + + workspaceService.verifyAll(); + await testActivation(activationService, activator); + + const event = TypeMoq.Mock.ofType(); + event.setup(e => e.affectsConfiguration(TypeMoq.It.isValue('python.jediEnabled'), TypeMoq.It.isAny())) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + cmdManager.setup(c => c.executeCommand(TypeMoq.It.isValue('workbench.action.reloadWindow'))) + .verifiable(TypeMoq.Times.never()); + + // Toggle the value in the setting and invoke the callback. + jediIsEnabledValueInSetting = !jediIsEnabledValueInSetting; + await callbackHandler(event.object); + + event.verifyAll(); + appShell.verifyAll(); + cmdManager.verifyAll(); + }); + test('Do not prompt user to reload VS Code when setting is not toggled', async () => { + let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; + workspaceService + .setup(w => w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback(cb => callbackHandler = cb) + .returns(() => TypeMoq.Mock.ofType().object) + .verifiable(TypeMoq.Times.once()); + + pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); + const activator = TypeMoq.Mock.ofType(); + const activationService = new ExtensionActivationService(serviceContainer.object); + + workspaceService.verifyAll(); + await testActivation(activationService, activator); + + const event = TypeMoq.Mock.ofType(); + event.setup(e => e.affectsConfiguration(TypeMoq.It.isValue('python.jediEnabled'), TypeMoq.It.isAny())) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + cmdManager.setup(c => c.executeCommand(TypeMoq.It.isValue('workbench.action.reloadWindow'))) + .verifiable(TypeMoq.Times.never()); + + // Invoke the config changed callback. + await callbackHandler(event.object); + + event.verifyAll(); + appShell.verifyAll(); + cmdManager.verifyAll(); + }); + test('Do not prompt user to reload VS Code when setting is not changed', async () => { + let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; + workspaceService + .setup(w => w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback(cb => callbackHandler = cb) + .returns(() => TypeMoq.Mock.ofType().object) + .verifiable(TypeMoq.Times.once()); + + pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); + const activator = TypeMoq.Mock.ofType(); + const activationService = new ExtensionActivationService(serviceContainer.object); + + workspaceService.verifyAll(); + await testActivation(activationService, activator); + + const event = TypeMoq.Mock.ofType(); + event.setup(e => e.affectsConfiguration(TypeMoq.It.isValue('python.jediEnabled'), TypeMoq.It.isAny())) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + cmdManager.setup(c => c.executeCommand(TypeMoq.It.isValue('workbench.action.reloadWindow'))) + .verifiable(TypeMoq.Times.never()); + + // Invoke the config changed callback. + await callbackHandler(event.object); + + event.verifyAll(); + appShell.verifyAll(); + cmdManager.verifyAll(); + }); + }); + }); +}); diff --git a/src/test/activation/downloader.unit.test.ts b/src/test/activation/downloader.unit.test.ts new file mode 100644 index 000000000000..edee2e828f3c --- /dev/null +++ b/src/test/activation/downloader.unit.test.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unused-variable + +import * as assert from 'assert'; +import * as TypeMoq from 'typemoq'; +import { DownloadLinks, LanguageServerDownloader } from '../../client/activation/downloader'; +import { PlatformName } from '../../client/activation/platformData'; +import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; +import { IOutputChannel } from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; + +suite('Activation - Downloader', () => { + let languageServerDownloader: LanguageServerDownloader; + let serviceContainer: TypeMoq.IMock; + let platformService: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + platformService = TypeMoq.Mock.ofType(); + const fs = TypeMoq.Mock.ofType(); + const output = TypeMoq.Mock.ofType(); + + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isAny())).returns(() => output.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fs.object); + + languageServerDownloader = new LanguageServerDownloader(serviceContainer.object, ''); + }); + type PlatformIdentifier = { + windows?: boolean; + mac?: boolean; + linux?: boolean; + is64Bit?: boolean; + }; + function setupPlatform(platform: PlatformIdentifier) { + platformService.setup(x => x.isWindows).returns(() => platform.windows === true); + platformService.setup(x => x.isMac).returns(() => platform.mac === true); + platformService.setup(x => x.isLinux).returns(() => platform.linux === true); + platformService.setup(x => x.is64bit).returns(() => platform.is64Bit === true); + } + test('Windows 32Bit', async () => { + setupPlatform({ windows: true }); + const link = await languageServerDownloader.getDownloadUri(); + assert.equal(link, DownloadLinks[PlatformName.Windows32Bit]); + }); + test('Windows 64Bit', async () => { + setupPlatform({ windows: true, is64Bit: true }); + const link = await languageServerDownloader.getDownloadUri(); + assert.equal(link, DownloadLinks[PlatformName.Windows64Bit]); + }); + test('Mac 64Bit', async () => { + setupPlatform({ mac: true, is64Bit: true }); + const link = await languageServerDownloader.getDownloadUri(); + assert.equal(link, DownloadLinks[PlatformName.Mac64Bit]); + }); + test('Linux 64Bit', async () => { + setupPlatform({ linux: true, is64Bit: true }); + const link = await languageServerDownloader.getDownloadUri(); + assert.equal(link, DownloadLinks[PlatformName.Linux64Bit]); + }); +}); diff --git a/src/test/activation/excludeFiles.ls.test.ts b/src/test/activation/excludeFiles.ls.test.ts new file mode 100644 index 000000000000..f7e753493e3d --- /dev/null +++ b/src/test/activation/excludeFiles.ls.test.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { Container } from 'inversify'; +import * as path from 'path'; +import { commands, ConfigurationTarget, languages, Position, TextDocument, window, workspace } from 'vscode'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import '../../client/common/extensions'; +import { IConfigurationService } from '../../client/common/types'; +import { activated } from '../../client/extension'; +import { ServiceContainer } from '../../client/ioc/container'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { IServiceContainer, IServiceManager } from '../../client/ioc/types'; +import { IsLanguageServerTest } from '../constants'; +import { closeActiveWindows } from '../initialize'; + +const wksPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'exclusions'); +const fileOne = path.join(wksPath, 'one.py'); + +// tslint:disable-next-line:max-func-body-length +suite('Exclude files (Language Server)', () => { + let textDocument: TextDocument; + let serviceManager: IServiceManager; + let serviceContainer: IServiceContainer; + let configService: IConfigurationService; + + suiteSetup(async function () { + if (!IsLanguageServerTest()) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + }); + setup(async () => { + const cont = new Container(); + serviceContainer = new ServiceContainer(cont); + serviceManager = new ServiceManager(cont); + + serviceManager.addSingleton(IConfigurationService, ConfigurationService); + configService = serviceManager.get(IConfigurationService); + }); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + async function openFile(file: string): Promise { + textDocument = await workspace.openTextDocument(file); + await activated; + await window.showTextDocument(textDocument); + // Make sure LS completes file loading and analysis. + // In test mode it awaits for the completion before trying + // to fetch data for completion, hover.etc. + await commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, new Position(0, 0)); + } + + async function setSetting(name: string, value: {} | undefined): Promise { + await configService.updateSettingAsync(name, value, undefined, ConfigurationTarget.Global); + } + + test('Default exclusions', async () => { + await openFile(fileOne); + const diag = languages.getDiagnostics(); + + const main = diag.filter(d => d[0].fsPath.indexOf('one.py') >= 0); + assert.equal(main.length > 0, true); + + const subdir = diag.filter(d => d[0].fsPath.indexOf('three.py') >= 0); + assert.equal(subdir.length > 0, true); + + const node_modules = diag.filter(d => d[0].fsPath.indexOf('node.py') >= 0); + assert.equal(node_modules.length, 0); + + const lib = diag.filter(d => d[0].fsPath.indexOf('fileLib.py') >= 0); + assert.equal(lib.length, 0); + + const sitePackages = diag.filter(d => d[0].fsPath.indexOf('sitePackages.py') >= 0); + assert.equal(sitePackages.length, 0); + }); + test('Exclude subfolder', async () => { + await setSetting('linting.ignorePatterns', ['**/dir1/**']); + + await openFile(fileOne); + const diag = languages.getDiagnostics(); + + const main = diag.filter(d => d[0].fsPath.indexOf('one.py') >= 0); + assert.equal(main.length > 0, true); + + const subdir1 = diag.filter(d => d[0].fsPath.indexOf('dir1file.py') >= 0); + assert.equal(subdir1.length, 0); + + const subdir2 = diag.filter(d => d[0].fsPath.indexOf('dir2file.py') >= 0); + assert.equal(subdir2.length, 0); + + await setSetting('linting.ignorePatterns', undefined); + }); +}); diff --git a/src/test/activation/platformData.unit.test.ts b/src/test/activation/platformData.unit.test.ts new file mode 100644 index 000000000000..0044c27b0182 --- /dev/null +++ b/src/test/activation/platformData.unit.test.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-unused-variable +import * as assert from 'assert'; +import * as TypeMoq from 'typemoq'; +import { PlatformData, PlatformLSExecutables } from '../../client/activation/platformData'; +import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; + +const testDataWinMac = [ + { isWindows: true, is64Bit: true, expectedName: 'win-x64' }, + { isWindows: true, is64Bit: false, expectedName: 'win-x86' }, + { isWindows: false, is64Bit: true, expectedName: 'osx-x64' } +]; + +const testDataLinux = [ + { name: 'centos', expectedName: 'linux-x64' }, + { name: 'debian', expectedName: 'linux-x64' }, + { name: 'fedora', expectedName: 'linux-x64' }, + { name: 'ol', expectedName: 'linux-x64' }, + { name: 'opensuse', expectedName: 'linux-x64' }, + { name: 'rhel', expectedName: 'linux-x64' }, + { name: 'ubuntu', expectedName: 'linux-x64' } +]; + +const testDataModuleName = [ + { isWindows: true, isMac: false, isLinux: false, expectedName: PlatformLSExecutables.Windows }, + { isWindows: false, isMac: true, isLinux: false, expectedName: PlatformLSExecutables.MacOS }, + { isWindows: false, isMac: false, isLinux: true, expectedName: PlatformLSExecutables.Linux } +]; + +// tslint:disable-next-line:max-func-body-length +suite('Activation - platform data', () => { + test('Name and hash (Windows/Mac)', async () => { + for (const t of testDataWinMac) { + const platformService = TypeMoq.Mock.ofType(); + platformService.setup(x => x.isWindows).returns(() => t.isWindows); + platformService.setup(x => x.isMac).returns(() => !t.isWindows); + platformService.setup(x => x.is64bit).returns(() => t.is64Bit); + + const fs = TypeMoq.Mock.ofType(); + const pd = new PlatformData(platformService.object, fs.object); + + const actual = await pd.getPlatformName(); + assert.equal(actual, t.expectedName, `${actual} does not match ${t.expectedName}`); + + const actualHash = await pd.getExpectedHash(); + assert.equal(actualHash, t.expectedName, `${actual} hash not match ${t.expectedName}`); + } + }); + test('Name and hash (Linux)', async () => { + for (const t of testDataLinux) { + const platformService = TypeMoq.Mock.ofType(); + platformService.setup(x => x.isWindows).returns(() => false); + platformService.setup(x => x.isMac).returns(() => false); + platformService.setup(x => x.isLinux).returns(() => true); + platformService.setup(x => x.is64bit).returns(() => true); + + const fs = TypeMoq.Mock.ofType(); + fs.setup(x => x.readFile(TypeMoq.It.isAnyString())).returns(() => Promise.resolve(`NAME="name"\nID=${t.name}\nID_LIKE=debian`)); + const pd = new PlatformData(platformService.object, fs.object); + + const actual = await pd.getPlatformName(); + assert.equal(actual, t.expectedName, `${actual} does not match ${t.expectedName}`); + + const actualHash = await pd.getExpectedHash(); + assert.equal(actual, t.expectedName, `${actual} hash not match ${t.expectedName}`); + } + }); + test('Module name', async () => { + for (const t of testDataModuleName) { + const platformService = TypeMoq.Mock.ofType(); + platformService.setup(x => x.isWindows).returns(() => t.isWindows); + platformService.setup(x => x.isLinux).returns(() => t.isLinux); + platformService.setup(x => x.isMac).returns(() => t.isMac); + + const fs = TypeMoq.Mock.ofType(); + const pd = new PlatformData(platformService.object, fs.object); + + const actual = pd.getEngineExecutableName(); + assert.equal(actual, t.expectedName, `${actual} does not match ${t.expectedName}`); + } + }); +}); diff --git a/src/test/analysisEngineTest.ts b/src/test/analysisEngineTest.ts new file mode 100644 index 000000000000..359da2bc0cde --- /dev/null +++ b/src/test/analysisEngineTest.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-console no-require-imports no-var-requires +import * as path from 'path'; + +process.env.CODE_TESTS_WORKSPACE = path.join(__dirname, '..', '..', 'src', 'test'); +process.env.IS_CI_SERVER_TEST_DEBUGGER = ''; +process.env.VSC_PYTHON_LANGUAGE_SERVER = '1'; + +function start() { + console.log('*'.repeat(100)); + console.log('Start Language Server tests'); + require('../../node_modules/vscode/bin/test'); +} +start(); diff --git a/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts b/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts new file mode 100644 index 000000000000..1fad39ac5453 --- /dev/null +++ b/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:insecure-random + +import * as typemoq from 'typemoq'; +import { DiagnosticSeverity } from 'vscode'; +import { ApplicationDiagnostics } from '../../../client/application/diagnostics/applicationDiagnostics'; +import { EnvironmentPathVariableDiagnosticsServiceId } from '../../../client/application/diagnostics/checks/envPathVariable'; +import { DiagnosticScope, IDiagnostic, IDiagnosticsService } from '../../../client/application/diagnostics/types'; +import { IApplicationDiagnostics } from '../../../client/application/types'; +import { STANDARD_OUTPUT_CHANNEL } from '../../../client/common/constants'; +import { ILogger, IOutputChannel } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('Application Diagnostics - ApplicationDiagnostics', () => { + let serviceContainer: typemoq.IMock; + let envHealthCheck: typemoq.IMock; + let outputChannel: typemoq.IMock; + let logger: typemoq.IMock; + let appDiagnostics: IApplicationDiagnostics; + + setup(() => { + serviceContainer = typemoq.Mock.ofType(); + envHealthCheck = typemoq.Mock.ofType(); + outputChannel = typemoq.Mock.ofType(); + logger = typemoq.Mock.ofType(); + + serviceContainer.setup(d => d.get(typemoq.It.isValue(IDiagnosticsService), typemoq.It.isValue(EnvironmentPathVariableDiagnosticsServiceId))) + .returns(() => envHealthCheck.object); + serviceContainer.setup(d => d.get(typemoq.It.isValue(IOutputChannel), typemoq.It.isValue(STANDARD_OUTPUT_CHANNEL))) + .returns(() => outputChannel.object); + serviceContainer.setup(d => d.get(typemoq.It.isValue(ILogger))) + .returns(() => logger.object); + + appDiagnostics = new ApplicationDiagnostics(serviceContainer.object); + }); + + test('Performing Pre Startup Health Check must check Path environment variable', async () => { + envHealthCheck.setup(e => e.diagnose()) + .returns(() => Promise.resolve([])) + .verifiable(typemoq.Times.once()); + + await appDiagnostics.performPreStartupHealthCheck(); + + envHealthCheck.verifyAll(); + }); + + test('Diagnostics Returned by Per Startup Health Checks must be logged', async () => { + const diagnostics: IDiagnostic[] = []; + for (let i = 0; i <= (Math.random() * 10); i += 1) { + const diagnostic: IDiagnostic = { + code: `Error${i}`, + message: `Error${i}`, + scope: i % 2 === 0 ? DiagnosticScope.Global : DiagnosticScope.WorkspaceFolder, + severity: DiagnosticSeverity.Error + }; + diagnostics.push(diagnostic); + } + for (let i = 0; i <= (Math.random() * 10); i += 1) { + const diagnostic: IDiagnostic = { + code: `Warning${i}`, + message: `Warning${i}`, + scope: i % 2 === 0 ? DiagnosticScope.Global : DiagnosticScope.WorkspaceFolder, + severity: DiagnosticSeverity.Warning + }; + diagnostics.push(diagnostic); + } + for (let i = 0; i <= (Math.random() * 10); i += 1) { + const diagnostic: IDiagnostic = { + code: `Info${i}`, + message: `Info${i}`, + scope: i % 2 === 0 ? DiagnosticScope.Global : DiagnosticScope.WorkspaceFolder, + severity: DiagnosticSeverity.Information + }; + diagnostics.push(diagnostic); + } + + for (const diagnostic of diagnostics) { + const message = `Diagnostic Code: ${diagnostic.code}, Message: ${diagnostic.message}`; + switch (diagnostic.severity) { + case DiagnosticSeverity.Error: { + logger.setup(l => l.logError(message)) + .verifiable(typemoq.Times.once()); + outputChannel.setup(o => o.appendLine(message)) + .verifiable(typemoq.Times.once()); + break; + } + case DiagnosticSeverity.Warning: { + logger.setup(l => l.logWarning(message)) + .verifiable(typemoq.Times.once()); + outputChannel.setup(o => o.appendLine(message)) + .verifiable(typemoq.Times.once()); + break; + } + default: { + logger.setup(l => l.logInformation(message)) + .verifiable(typemoq.Times.once()); + break; + } + } + } + + envHealthCheck.setup(e => e.diagnose()) + .returns(() => Promise.resolve(diagnostics)) + .verifiable(typemoq.Times.once()); + + await appDiagnostics.performPreStartupHealthCheck(); + + envHealthCheck.verifyAll(); + outputChannel.verifyAll(); + logger.verifyAll(); + }); +}); diff --git a/src/test/application/diagnostics/checks/envPathVariable.unit.test.ts b/src/test/application/diagnostics/checks/envPathVariable.unit.test.ts new file mode 100644 index 000000000000..a2f678f1f142 --- /dev/null +++ b/src/test/application/diagnostics/checks/envPathVariable.unit.test.ts @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as path from 'path'; +import * as typemoq from 'typemoq'; +import { DiagnosticSeverity } from 'vscode'; +import { EnvironmentPathVariableDiagnosticsService } from '../../../../client/application/diagnostics/checks/envPathVariable'; +import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; +import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; +import { DiagnosticScope, IDiagnostic, IDiagnosticCommand, IDiagnosticFilterService, IDiagnosticHandlerService, IDiagnosticsService } from '../../../../client/application/diagnostics/types'; +import { IApplicationEnvironment } from '../../../../client/common/application/types'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { ICurrentProcess, IPathUtils } from '../../../../client/common/types'; +import { EnvironmentVariables } from '../../../../client/common/variables/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Application Diagnostics - Checks Env Path Variable', () => { + let diagnosticService: IDiagnosticsService; + let platformService: typemoq.IMock; + let messageHandler: typemoq.IMock>; + let filterService: typemoq.IMock; + let procEnv: typemoq.IMock; + let appEnv: typemoq.IMock; + let commandFactory: typemoq.IMock; + const pathVariableName = 'Path'; + const pathDelimiter = ';'; + const extensionName = 'Some Extension Name'; + setup(() => { + const serviceContainer = typemoq.Mock.ofType(); + platformService = typemoq.Mock.ofType(); + platformService.setup(p => p.pathVariableName).returns(() => pathVariableName); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + + messageHandler = typemoq.Mock.ofType>(); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IDiagnosticHandlerService), typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId))) + .returns(() => messageHandler.object); + + appEnv = typemoq.Mock.ofType(); + appEnv.setup(a => a.extensionName).returns(() => extensionName); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IApplicationEnvironment))) + .returns(() => appEnv.object); + + filterService = typemoq.Mock.ofType(); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .returns(() => filterService.object); + + commandFactory = typemoq.Mock.ofType(); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) + .returns(() => commandFactory.object); + + const currentProc = typemoq.Mock.ofType(); + procEnv = typemoq.Mock.ofType(); + currentProc.setup(p => p.env).returns(() => procEnv.object); + serviceContainer.setup(s => s.get(typemoq.It.isValue(ICurrentProcess))) + .returns(() => currentProc.object); + + const pathUtils = typemoq.Mock.ofType(); + pathUtils.setup(p => p.delimiter).returns(() => pathDelimiter); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IPathUtils))) + .returns(() => pathUtils.object); + + diagnosticService = new EnvironmentPathVariableDiagnosticsService(serviceContainer.object); + }); + + test('Can handle EnvPathVariable diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType(); + diagnostic.setup(d => d.code) + .returns(() => DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(true, 'Invalid value'); + diagnostic.verifyAll(); + }); + test('Can not handle non-EnvPathVariable diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType(); + diagnostic.setup(d => d.code) + .returns(() => 'Something Else') + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(false, 'Invalid value'); + diagnostic.verifyAll(); + }); + test('Should return empty diagnostics for Mac', async () => { + platformService.setup(p => p.isMac).returns(() => true); + platformService.setup(p => p.isLinux).returns(() => false); + platformService.setup(p => p.isWindows).returns(() => false); + const diagnostics = await diagnosticService.diagnose(); + expect(diagnostics).to.be.deep.equal([]); + }); + test('Should return empty diagnostics for Linux', async () => { + platformService.setup(p => p.isMac).returns(() => false); + platformService.setup(p => p.isLinux).returns(() => true); + platformService.setup(p => p.isWindows).returns(() => false); + const diagnostics = await diagnosticService.diagnose(); + expect(diagnostics).to.be.deep.equal([]); + }); + test('Should return empty diagnostics for Windows if path variable is valid', async () => { + platformService.setup(p => p.isWindows).returns(() => true); + const paths = [ + path.join('one', 'two', 'three'), + path.join('one', 'two', 'four') + ].join(pathDelimiter); + procEnv.setup(env => env[pathVariableName]).returns(() => paths); + + const diagnostics = await diagnosticService.diagnose(); + + expect(diagnostics).to.be.deep.equal([]); + }); + // Note: On windows, when a path contains a `;` then Windows encloses the path within `"`. + test('Should return single diagnostics for Windows if path contains \'"\'', async () => { + platformService.setup(p => p.isWindows).returns(() => true); + const paths = [ + path.join('one', 'two', 'three"'), + path.join('one', 'two', 'four') + ].join(pathDelimiter); + procEnv.setup(env => env[pathVariableName]).returns(() => paths); + + const diagnostics = await diagnosticService.diagnose(); + + expect(diagnostics).to.be.lengthOf(1); + expect(diagnostics[0].code).to.be.equal(DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic); + expect(diagnostics[0].message).to.contain(extensionName); + expect(diagnostics[0].message).to.contain(pathVariableName); + expect(diagnostics[0].severity).to.be.equal(DiagnosticSeverity.Warning); + expect(diagnostics[0].scope).to.be.equal(DiagnosticScope.Global); + }); + test('Should not return diagnostics for Windows if path ends with delimiter', async () => { + const paths = [ + path.join('one', 'two', 'three'), + path.join('one', 'two', 'four') + ].join(pathDelimiter) + pathDelimiter; + platformService.setup(p => p.isWindows).returns(() => true); + procEnv.setup(env => env[pathVariableName]).returns(() => paths); + + const diagnostics = await diagnosticService.diagnose(); + + expect(diagnostics).to.be.lengthOf(0); + }); + test('Should display three options in message displayed with 2 commands', async () => { + platformService.setup(p => p.isWindows).returns(() => true); + const diagnostic = typemoq.Mock.ofType(); + diagnostic.setup(d => d.code) + .returns(() => DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + const alwaysIgnoreCommand = typemoq.Mock.ofType(); + commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), + typemoq.It.isObjectWith>({ type: 'ignore', options: DiagnosticScope.Global }))) + .returns(() => alwaysIgnoreCommand.object) + .verifiable(typemoq.Times.once()); + const launchBrowserCommand = typemoq.Mock.ofType(); + commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), + typemoq.It.isObjectWith>({ type: 'launch' }))) + .returns(() => launchBrowserCommand.object) + .verifiable(typemoq.Times.once()); + messageHandler.setup(m => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic.object]); + + diagnostic.verifyAll(); + commandFactory.verifyAll(); + messageHandler.verifyAll(); + }); + test('Should not display a message if the diagnostic code has been ignored', async () => { + platformService.setup(p => p.isWindows).returns(() => true); + const diagnostic = typemoq.Mock.ofType(); + + filterService.setup(f => f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic))) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + diagnostic.setup(d => d.code) + .returns(() => DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + messageHandler.setup(m => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await diagnosticService.handle([diagnostic.object]); + + filterService.verifyAll(); + diagnostic.verifyAll(); + commandFactory.verifyAll(); + messageHandler.verifyAll(); + }); +}); diff --git a/src/test/application/diagnostics/commands/factory.unit.test.ts b/src/test/application/diagnostics/commands/factory.unit.test.ts new file mode 100644 index 000000000000..187499f6d652 --- /dev/null +++ b/src/test/application/diagnostics/commands/factory.unit.test.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { DiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/factory'; +import { IgnoreDiagnosticCommand } from '../../../../client/application/diagnostics/commands/ignore'; +import { LaunchBrowserCommand } from '../../../../client/application/diagnostics/commands/launchBrowser'; +import { IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { DiagnosticScope, IDiagnostic } from '../../../../client/application/diagnostics/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - Commands Factory', () => { + let commandFactory: IDiagnosticsCommandFactory; + setup(() => { + const serviceContainer = typemoq.Mock.ofType(); + commandFactory = new DiagnosticsCommandFactory(serviceContainer.object); + }); + + test('Test creation of Ignore Command', async () => { + const diagnostic = typemoq.Mock.ofType(); + + const command = commandFactory.createCommand(diagnostic.object, { type: 'ignore', options: DiagnosticScope.Global }); + expect(command).to.be.instanceOf(IgnoreDiagnosticCommand); + }); + + test('Test creation of Launch Browser Command', async () => { + const diagnostic = typemoq.Mock.ofType(); + + const command = commandFactory.createCommand(diagnostic.object, { type: 'launch', options: 'x' }); + expect(command).to.be.instanceOf(LaunchBrowserCommand); + }); +}); diff --git a/src/test/application/diagnostics/commands/ignore.unit.test.ts b/src/test/application/diagnostics/commands/ignore.unit.test.ts new file mode 100644 index 000000000000..586071b9abe0 --- /dev/null +++ b/src/test/application/diagnostics/commands/ignore.unit.test.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as typemoq from 'typemoq'; +import { IgnoreDiagnosticCommand } from '../../../../client/application/diagnostics/commands/ignore'; +import { DiagnosticScope, IDiagnostic, IDiagnosticCommand, IDiagnosticFilterService } from '../../../../client/application/diagnostics/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - Commands Ignore', () => { + let ignoreCommand: IDiagnosticCommand; + let serviceContainer: typemoq.IMock; + let diagnostic: typemoq.IMock; + setup(() => { + serviceContainer = typemoq.Mock.ofType(); + + diagnostic = typemoq.Mock.ofType(); + ignoreCommand = new IgnoreDiagnosticCommand(diagnostic.object, serviceContainer.object, DiagnosticScope.Global); + }); + + test('Invoking Command should invoke the filter Service', async () => { + const filterService = typemoq.Mock.ofType(); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .returns(() => filterService.object) + .verifiable(typemoq.Times.once()); + diagnostic.setup(d => d.code).returns(() => 'xyz') + .verifiable(typemoq.Times.once()); + filterService.setup(s => s.ignoreDiagnostic(typemoq.It.isValue('xyz'), typemoq.It.isValue(DiagnosticScope.Global))) + .verifiable(typemoq.Times.once()); + + await ignoreCommand.invoke(); + serviceContainer.verifyAll(); + }); +}); diff --git a/src/test/application/diagnostics/commands/launchBrowser.unit.test.ts b/src/test/application/diagnostics/commands/launchBrowser.unit.test.ts new file mode 100644 index 000000000000..665f7937934a --- /dev/null +++ b/src/test/application/diagnostics/commands/launchBrowser.unit.test.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as typemoq from 'typemoq'; +import { LaunchBrowserCommand } from '../../../../client/application/diagnostics/commands/launchBrowser'; +import { IDiagnostic, IDiagnosticCommand } from '../../../../client/application/diagnostics/types'; +import { IBrowserService } from '../../../../client/common/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - Commands Launch Browser', () => { + let cmd: IDiagnosticCommand; + let serviceContainer: typemoq.IMock; + let diagnostic: typemoq.IMock; + const url = 'xyz://abc'; + setup(() => { + serviceContainer = typemoq.Mock.ofType(); + diagnostic = typemoq.Mock.ofType(); + cmd = new LaunchBrowserCommand(diagnostic.object, serviceContainer.object, url); + }); + + test('Invoking Command should launch the browser', async () => { + const browser = typemoq.Mock.ofType(); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IBrowserService))) + .returns(() => browser.object) + .verifiable(typemoq.Times.once()); + browser.setup(s => s.launch(typemoq.It.isValue(url))) + .verifiable(typemoq.Times.once()); + + await cmd.invoke(); + serviceContainer.verifyAll(); + }); +}); diff --git a/src/test/application/diagnostics/filter.unit.test.ts b/src/test/application/diagnostics/filter.unit.test.ts new file mode 100644 index 000000000000..b42136fc6d8d --- /dev/null +++ b/src/test/application/diagnostics/filter.unit.test.ts @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { DiagnosticFilterService, FilterKeys } from '../../../client/application/diagnostics/filter'; +import { DiagnosticScope, IDiagnosticFilterService } from '../../../client/application/diagnostics/types'; +import { IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('Application Diagnostics - Filter', () => { + let globalState: typemoq.IMock>; + let workspaceState: typemoq.IMock>; + + [ + { name: 'Global', scope: DiagnosticScope.Global, state: () => globalState, otherState: () => workspaceState }, + { name: 'Workspace', scope: DiagnosticScope.WorkspaceFolder, state: () => workspaceState, otherState: () => globalState } + ] + .forEach(item => { + let serviceContainer: typemoq.IMock; + let filterService: IDiagnosticFilterService; + + setup(() => { + globalState = typemoq.Mock.ofType>(); + workspaceState = typemoq.Mock.ofType>(); + + serviceContainer = typemoq.Mock.ofType(); + const stateFactory = typemoq.Mock.ofType(); + + stateFactory.setup(f => f.createGlobalPersistentState(typemoq.It.isValue(FilterKeys.GlobalDiagnosticFilter), typemoq.It.isValue([]))) + .returns(() => globalState.object); + stateFactory.setup(f => f.createWorkspacePersistentState(typemoq.It.isValue(FilterKeys.WorkspaceDiagnosticFilter), typemoq.It.isValue([]))) + .returns(() => workspaceState.object); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IPersistentStateFactory))) + .returns(() => stateFactory.object); + + filterService = new DiagnosticFilterService(serviceContainer.object); + }); + + test(`ignoreDiagnostic must save codes in ${item.name} Persistent State`, async () => { + const code = 'xyz'; + item.state().setup(g => g.value).returns(() => []) + .verifiable(typemoq.Times.once()); + item.state().setup(g => g.updateValue(typemoq.It.isValue([code]))) + .verifiable(typemoq.Times.once()); + + item.otherState().setup(g => g.value) + .verifiable(typemoq.Times.never()); + item.otherState().setup(g => g.updateValue(typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await filterService.ignoreDiagnostic(code, item.scope); + + item.state().verifyAll(); + }); + test('shouldIgnoreDiagnostic should return \'false\' when code does not exist in any State', async () => { + const code = 'xyz'; + item.state().setup(g => g.value).returns(() => []) + .verifiable(typemoq.Times.once()); + item.otherState().setup(g => g.value).returns(() => []) + .verifiable(typemoq.Times.once()); + + const ignore = await filterService.shouldIgnoreDiagnostic(code); + + expect(ignore).to.be.equal(false, 'Incorrect value'); + item.state().verifyAll(); + }); + test(`shouldIgnoreDiagnostic should return \'true\' when code exist in ${item.name} State`, async () => { + const code = 'xyz'; + item.state().setup(g => g.value).returns(() => ['a', 'b', 'c', code]) + .verifiable(typemoq.Times.once()); + item.otherState().setup(g => g.value).returns(() => []) + .verifiable(typemoq.Times.once()); + + const ignore = await filterService.shouldIgnoreDiagnostic(code); + + expect(ignore).to.be.equal(true, 'Incorrect value'); + item.state().verifyAll(); + }); + + test('shouldIgnoreDiagnostic should return \'true\' when code exist in any State', async () => { + const code = 'xyz'; + item.state().setup(g => g.value).returns(() => []) + .verifiable(typemoq.Times.atLeast(0)); + item.otherState().setup(g => g.value).returns(() => ['a', 'b', 'c', code]) + .verifiable(typemoq.Times.atLeast(0)); + + const ignore = await filterService.shouldIgnoreDiagnostic(code); + + expect(ignore).to.be.equal(true, 'Incorrect value'); + item.state().verifyAll(); + }); + + test(`ignoreDiagnostic must append codes in ${item.name} Persistent State`, async () => { + const code = 'xyz'; + const currentState = ['a', 'b', 'c']; + item.state().setup(g => g.value).returns(() => currentState) + .verifiable(typemoq.Times.atLeastOnce()); + item.state().setup(g => g.updateValue(typemoq.It.isAny())) + .callback(value => { + expect(value).to.deep.equal(currentState.concat([code])); + }) + .verifiable(typemoq.Times.atLeastOnce()); + + item.otherState().setup(g => g.value) + .verifiable(typemoq.Times.never()); + item.otherState().setup(g => g.updateValue(typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await filterService.ignoreDiagnostic(code, item.scope); + + item.state().verifyAll(); + }); + }); +}); diff --git a/src/test/application/diagnostics/promptHandler.unit.test.ts b/src/test/application/diagnostics/promptHandler.unit.test.ts new file mode 100644 index 000000000000..ce07d41d91cb --- /dev/null +++ b/src/test/application/diagnostics/promptHandler.unit.test.ts @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:insecure-random max-func-body-length + +import * as typemoq from 'typemoq'; +import { DiagnosticSeverity } from 'vscode'; +import { DiagnosticCommandPromptHandlerService, MessageCommandPrompt } from '../../../client/application/diagnostics/promptHandler'; +import { DiagnosticScope, IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerService } from '../../../client/application/diagnostics/types'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { EnumEx } from '../../../client/common/enumUtils'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('Application Diagnostics - PromptHandler', () => { + let serviceContainer: typemoq.IMock; + let appShell: typemoq.IMock; + let promptHandler: IDiagnosticHandlerService; + + setup(() => { + serviceContainer = typemoq.Mock.ofType(); + appShell = typemoq.Mock.ofType(); + + serviceContainer.setup(s => s.get(typemoq.It.isValue(IApplicationShell))) + .returns(() => appShell.object); + + promptHandler = new DiagnosticCommandPromptHandlerService(serviceContainer.object); + }); + + EnumEx.getNamesAndValues(DiagnosticSeverity).forEach(severity => { + test(`Handling a diagnositic of severity '${severity.name}' should display a message without any buttons`, async () => { + const diagnostic: IDiagnostic = { code: '1', message: 'one', scope: DiagnosticScope.Global, severity: severity.value }; + switch (severity.value) { + case DiagnosticSeverity.Error: { + appShell.setup(a => a.showErrorMessage(typemoq.It.isValue(diagnostic.message))) + .verifiable(typemoq.Times.once()); + break; + } + case DiagnosticSeverity.Warning: { + appShell.setup(a => a.showWarningMessage(typemoq.It.isValue(diagnostic.message))) + .verifiable(typemoq.Times.once()); + break; + } + default: { + appShell.setup(a => a.showInformationMessage(typemoq.It.isValue(diagnostic.message))) + .verifiable(typemoq.Times.once()); + break; + } + } + + await promptHandler.handle(diagnostic); + appShell.verifyAll(); + }); + test(`Handling a diagnositic of severity '${severity.name}' should display a custom message with buttons`, async () => { + const diagnostic: IDiagnostic = { code: '1', message: 'one', scope: DiagnosticScope.Global, severity: severity.value }; + const options: MessageCommandPrompt = { + commandPrompts: [ + { prompt: 'Yes' }, + { prompt: 'No' } + ], + message: 'Custom Message' + }; + + switch (severity.value) { + case DiagnosticSeverity.Error: { + appShell.setup(a => a.showErrorMessage(typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), typemoq.It.isValue('No'))) + .verifiable(typemoq.Times.once()); + break; + } + case DiagnosticSeverity.Warning: { + appShell.setup(a => a.showWarningMessage(typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), typemoq.It.isValue('No'))) + .verifiable(typemoq.Times.once()); + break; + } + default: { + appShell.setup(a => a.showInformationMessage(typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), typemoq.It.isValue('No'))) + .verifiable(typemoq.Times.once()); + break; + } + } + + await promptHandler.handle(diagnostic, options); + appShell.verifyAll(); + }); + test(`Handling a diagnositic of severity '${severity.name}' should display a custom message with buttons and invoke selected command`, async () => { + const diagnostic: IDiagnostic = { code: '1', message: 'one', scope: DiagnosticScope.Global, severity: severity.value }; + const command = typemoq.Mock.ofType(); + const options: MessageCommandPrompt = { + commandPrompts: [ + { prompt: 'Yes', command: command.object }, + { prompt: 'No', command: command.object } + ], + message: 'Custom Message' + }; + command.setup(c => c.invoke()) + .verifiable(typemoq.Times.once()); + + switch (severity.value) { + case DiagnosticSeverity.Error: { + appShell.setup(a => a.showErrorMessage(typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), typemoq.It.isValue('No'))) + .returns(() => Promise.resolve('Yes')) + .verifiable(typemoq.Times.once()); + break; + } + case DiagnosticSeverity.Warning: { + appShell.setup(a => a.showWarningMessage(typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), typemoq.It.isValue('No'))) + .returns(() => Promise.resolve('Yes')) + .verifiable(typemoq.Times.once()); + break; + } + default: { + appShell.setup(a => a.showInformationMessage(typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), typemoq.It.isValue('No'))) + .returns(() => Promise.resolve('Yes')) + .verifiable(typemoq.Times.once()); + break; + } + } + + await promptHandler.handle(diagnostic, options); + appShell.verifyAll(); + command.verifyAll(); + }); + }); +}); diff --git a/src/test/autocomplete/base.test.ts b/src/test/autocomplete/base.test.ts index bf07ac4fd783..1e658e2821c5 100644 --- a/src/test/autocomplete/base.test.ts +++ b/src/test/autocomplete/base.test.ts @@ -1,221 +1,254 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// tslint:disable:no-unused-variable -import * as assert from 'assert'; -import { EOL } from 'os'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { rootWorkspaceUri } from '../common'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; -import { UnitTestIocContainer } from '../unittests/serviceRegistry'; - -const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); -const fileOne = path.join(autoCompPath, 'one.py'); -const fileImport = path.join(autoCompPath, 'imp.py'); -const fileDoc = path.join(autoCompPath, 'doc.py'); -const fileLambda = path.join(autoCompPath, 'lamb.py'); -const fileDecorator = path.join(autoCompPath, 'deco.py'); -const fileEncoding = path.join(autoCompPath, 'four.py'); -const fileEncodingUsed = path.join(autoCompPath, 'five.py'); -const fileSuppress = path.join(autoCompPath, 'suppress.py'); - -// tslint:disable-next-line:max-func-body-length -suite('Autocomplete', () => { - let isPython2: boolean; - let ioc: UnitTestIocContainer; - suiteSetup(async () => { - await initialize(); - initializeDI(); - isPython2 = await ioc.getPythonMajorVersion(rootWorkspaceUri) === 2; - }); - setup(initializeTest); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await closeActiveWindows(); - ioc.dispose(); - }); - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerVariableTypes(); - ioc.registerProcessTypes(); - } - - test('For "sys."', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileOne).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(3, 10); - return vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); - }).then(list => { - assert.equal(list!.items.filter(item => item.label === 'api_version').length, 1, 'api_version not found'); - }).then(done, done); - }); - - // https://github.com/DonJayamanne/pythonVSCode/issues/975 - test('For "import *"', async () => { - const textDocument = await vscode.workspace.openTextDocument(fileImport); - await vscode.window.showTextDocument(textDocument); - const position = new vscode.Position(1, 4); - const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); - assert.equal(list!.items.filter(item => item.label === 'fstat').length, 1, 'fstat not found'); - }); - - // https://github.com/DonJayamanne/pythonVSCode/issues/898 - test('For "f.readlines()"', async () => { - const textDocument = await vscode.workspace.openTextDocument(fileDoc); - await vscode.window.showTextDocument(textDocument); - const position = new vscode.Position(5, 27); - const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); - // These are not known to work, jedi issue - // assert.equal(list.items.filter(item => item.label === 'capitalize').length, 1, 'capitalize not found (known not to work, Jedi issue)'); - // assert.notEqual(list.items.filter(item => item.label === 'upper').length, 1, 'upper not found'); - // assert.notEqual(list.items.filter(item => item.label === 'lower').length, 1, 'lower not found'); - }); - - // https://github.com/DonJayamanne/pythonVSCode/issues/265 - test('For "lambda"', async function () { - if (isPython2) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - return; - } - const textDocument = await vscode.workspace.openTextDocument(fileLambda); - await vscode.window.showTextDocument(textDocument); - const position = new vscode.Position(1, 19); - const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); - assert.notEqual(list!.items.filter(item => item.label === 'append').length, 0, 'append not found'); - assert.notEqual(list!.items.filter(item => item.label === 'clear').length, 0, 'clear not found'); - assert.notEqual(list!.items.filter(item => item.label === 'count').length, 0, 'cound not found'); - }); - - // https://github.com/DonJayamanne/pythonVSCode/issues/630 - test('For "abc.decorators"', async () => { - const textDocument = await vscode.workspace.openTextDocument(fileDecorator); - await vscode.window.showTextDocument(textDocument); - let position = new vscode.Position(3, 9); - let list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); - assert.notEqual(list!.items.filter(item => item.label === 'ABCMeta').length, 0, 'ABCMeta not found'); - assert.notEqual(list!.items.filter(item => item.label === 'abstractmethod').length, 0, 'abstractmethod not found'); - - position = new vscode.Position(4, 9); - list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); - assert.notEqual(list!.items.filter(item => item.label === 'ABCMeta').length, 0, 'ABCMeta not found'); - assert.notEqual(list!.items.filter(item => item.label === 'abstractmethod').length, 0, 'abstractmethod not found'); - - position = new vscode.Position(2, 30); - list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); - assert.notEqual(list!.items.filter(item => item.label === 'ABCMeta').length, 0, 'ABCMeta not found'); - assert.notEqual(list!.items.filter(item => item.label === 'abstractmethod').length, 0, 'abstractmethod not found'); - }); - - // https://github.com/DonJayamanne/pythonVSCode/issues/727 - // https://github.com/DonJayamanne/pythonVSCode/issues/746 - // https://github.com/davidhalter/jedi/issues/859 - test('For "time.slee"', async () => { - const textDocument = await vscode.workspace.openTextDocument(fileDoc); - await vscode.window.showTextDocument(textDocument); - const position = new vscode.Position(10, 9); - const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); - - const items = list!.items.filter(item => item.label === 'sleep'); - assert.notEqual(items.length, 0, 'sleep not found'); - - checkDocumentation(items[0], 'Delay execution for a given number of seconds. The argument may be'); - }); - - test('For custom class', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileOne).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(30, 4); - return vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); - }).then(list => { - assert.notEqual(list!.items.filter(item => item.label === 'method1').length, 0, 'method1 not found'); - assert.notEqual(list!.items.filter(item => item.label === 'method2').length, 0, 'method2 not found'); - }).then(done, done); - }); - - test('With Unicode Characters', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileEncoding).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(25, 4); - return vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); - }).then(list => { - const items = list!.items.filter(item => item.label === 'bar'); - assert.equal(items.length, 1, 'bar not found'); - - const expected = `说明 - keep this line, it works${EOL}delete following line, it works${EOL}如果存在需要等待审批或正在执行的任务,将不刷新页面`; - checkDocumentation(items[0], expected); - }).then(done, done); - }); - - test('Across files With Unicode Characters', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileEncodingUsed).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(1, 5); - return vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); - }).then(list => { - let items = list!.items.filter(item => item.label === 'Foo'); - assert.equal(items.length, 1, 'Foo not found'); - checkDocumentation(items[0], '说明'); - - items = list!.items.filter(item => item.label === 'showMessage'); - assert.equal(items.length, 1, 'showMessage not found'); - - const expected = `Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи. ${EOL}Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку.`; - checkDocumentation(items[0], expected); - }).then(done, done); - }); - - // https://github.com/Microsoft/vscode-python/issues/110 - test('Suppress in strings/comments', async () => { - const positions = [ - new vscode.Position(0, 1), // false - new vscode.Position(0, 9), // true - new vscode.Position(0, 12), // false - new vscode.Position(1, 1), // false - new vscode.Position(1, 3), // false - new vscode.Position(2, 7), // false - new vscode.Position(3, 0), // false - new vscode.Position(4, 2), // false - new vscode.Position(4, 8), // false - new vscode.Position(5, 4) // false - ]; - const expected = [ - false, true, false, false, false, false, false, false, false, false - ]; - const textDocument = await vscode.workspace.openTextDocument(fileSuppress); - await vscode.window.showTextDocument(textDocument); - for (let i = 0; i < positions.length; i += 1) { - const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, positions[i]); - const result = list!.items.filter(item => item.label === 'abs').length; - assert.equal(result > 0, expected[i], - `Expected ${expected[i]} at position ${positions[i].line}:${positions[i].character} but got ${result}`); - } - }); -}); - -// tslint:disable-next-line:no-any -function checkDocumentation(item: vscode.CompletionItem, expectedContains: string): void { - const documentation = item.documentation as vscode.MarkdownString; - assert.notEqual(documentation, null, 'Documentation is not MarkdownString'); - - const inDoc = documentation.value.indexOf(expectedContains) >= 0; - assert.equal(inDoc, true, 'Documentation incorrect'); -} +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-unused-variable +import * as assert from 'assert'; +import { EOL } from 'os'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { IConfigurationService } from '../../client/common/types'; +import { rootWorkspaceUri } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IsLanguageServerTest } from '../initialize'; +import { UnitTestIocContainer } from '../unittests/serviceRegistry'; + +const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); +const fileOne = path.join(autoCompPath, 'one.py'); +const fileImport = path.join(autoCompPath, 'imp.py'); +const fileDoc = path.join(autoCompPath, 'doc.py'); +const fileLambda = path.join(autoCompPath, 'lamb.py'); +const fileDecorator = path.join(autoCompPath, 'deco.py'); +const fileEncoding = path.join(autoCompPath, 'four.py'); +const fileEncodingUsed = path.join(autoCompPath, 'five.py'); +const fileSuppress = path.join(autoCompPath, 'suppress.py'); + +// tslint:disable-next-line:max-func-body-length +suite('Autocomplete', function () { + // Attempt to fix #1301 + // tslint:disable-next-line:no-invalid-this + this.timeout(60000); + let isPython2: boolean; + let ioc: UnitTestIocContainer; + + suiteSetup(async function () { + // Attempt to fix #1301 + // tslint:disable-next-line:no-invalid-this + this.timeout(60000); + await initialize(); + initializeDI(); + isPython2 = await ioc.getPythonMajorVersion(rootWorkspaceUri) === 2; + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + ioc.dispose(); + }); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + } + + test('For "sys."', done => { + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileOne).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(3, 10); + return vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + }).then(list => { + assert.equal(list!.items.filter(item => item.label === 'api_version').length, 1, 'api_version not found'); + }).then(done, done); + }); + + // https://github.com/DonJayamanne/pythonVSCode/issues/975 + test('For "import *"', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileImport); + await vscode.window.showTextDocument(textDocument); + const position = new vscode.Position(1, 4); + const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.equal(list!.items.filter(item => item.label === 'fstat').length, 1, 'fstat not found'); + }); + + // https://github.com/DonJayamanne/pythonVSCode/issues/898 + test('For "f.readlines()"', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileDoc); + await vscode.window.showTextDocument(textDocument); + const position = new vscode.Position(5, 27); + const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + // These are not known to work, jedi issue + // assert.equal(list.items.filter(item => item.label === 'capitalize').length, 1, 'capitalize not found (known not to work, Jedi issue)'); + // assert.notEqual(list.items.filter(item => item.label === 'upper').length, 1, 'upper not found'); + // assert.notEqual(list.items.filter(item => item.label === 'lower').length, 1, 'lower not found'); + }); + + // https://github.com/DonJayamanne/pythonVSCode/issues/265 + test('For "lambda"', async function () { + if (isPython2) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + return; + } + const textDocument = await vscode.workspace.openTextDocument(fileLambda); + await vscode.window.showTextDocument(textDocument); + const position = new vscode.Position(1, 19); + const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.notEqual(list!.items.filter(item => item.label === 'append').length, 0, 'append not found'); + assert.notEqual(list!.items.filter(item => item.label === 'clear').length, 0, 'clear not found'); + assert.notEqual(list!.items.filter(item => item.label === 'count').length, 0, 'cound not found'); + }); + + // https://github.com/DonJayamanne/pythonVSCode/issues/630 + test('For "abc.decorators"', async () => { + // Disabled for the Language Server, see https://github.com/Microsoft/PTVS/issues/3857 + if (IsLanguageServerTest()) { + return; + } + const textDocument = await vscode.workspace.openTextDocument(fileDecorator); + await vscode.window.showTextDocument(textDocument); + let position = new vscode.Position(3, 9); + let list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.notEqual(list!.items.filter(item => item.label === 'ABCMeta').length, 0, 'ABCMeta not found'); + assert.notEqual(list!.items.filter(item => item.label === 'abstractmethod').length, 0, 'abstractmethod not found'); + + position = new vscode.Position(4, 9); + list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.notEqual(list!.items.filter(item => item.label === 'ABCMeta').length, 0, 'ABCMeta not found'); + assert.notEqual(list!.items.filter(item => item.label === 'abstractmethod').length, 0, 'abstractmethod not found'); + + position = new vscode.Position(2, 30); + list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.notEqual(list!.items.filter(item => item.label === 'ABCMeta').length, 0, 'ABCMeta not found'); + assert.notEqual(list!.items.filter(item => item.label === 'abstractmethod').length, 0, 'abstractmethod not found'); + }); + + // https://github.com/DonJayamanne/pythonVSCode/issues/727 + // https://github.com/DonJayamanne/pythonVSCode/issues/746 + // https://github.com/davidhalter/jedi/issues/859 + test('For "time.slee"', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileDoc); + await vscode.window.showTextDocument(textDocument); + const position = new vscode.Position(10, 9); + const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + + const items = list!.items.filter(item => item.label === 'sleep'); + assert.notEqual(items.length, 0, 'sleep not found'); + + checkDocumentation(items[0], 'Delay execution for a given number of seconds. The argument may be'); + }); + + test('For custom class', done => { + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileOne).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(30, 4); + return vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + }).then(list => { + assert.notEqual(list!.items.filter(item => item.label === 'method1').length, 0, 'method1 not found'); + assert.notEqual(list!.items.filter(item => item.label === 'method2').length, 0, 'method2 not found'); + }).then(done, done); + }); + + test('With Unicode Characters', done => { + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileEncoding).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(25, 4); + return vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + }).then(list => { + const items = list!.items.filter(item => item.label === 'bar'); + assert.equal(items.length, 1, 'bar not found'); + + const expected1 = '说明 - keep this line, it works'; + checkDocumentation(items[0], expected1); + + const expected2 = '如果存在需要等待审批或正在执行的任务,将不刷新页面'; + checkDocumentation(items[0], expected2); + }).then(done, done); + }); + + test('Across files With Unicode Characters', done => { + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileEncodingUsed).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(1, 5); + return vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + }).then(list => { + let items = list!.items.filter(item => item.label === 'Foo'); + assert.equal(items.length, 1, 'Foo not found'); + checkDocumentation(items[0], '说明'); + + items = list!.items.filter(item => item.label === 'showMessage'); + assert.equal(items.length, 1, 'showMessage not found'); + + const expected1 = 'Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи.'; + checkDocumentation(items[0], expected1); + + const expected2 = 'Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку.'; + checkDocumentation(items[0], expected2); + }).then(done, done); + }); + + // https://github.com/Microsoft/vscode-python/issues/110 + test('Suppress in strings/comments', async () => { + // Excluded from the Language Server b/c skipping of strings and comments + // is not yet there. See https://github.com/Microsoft/PTVS/issues/3798 + if (IsLanguageServerTest()) { + return; + } + const positions = [ + new vscode.Position(0, 1), // false + new vscode.Position(0, 9), // true + new vscode.Position(0, 12), // false + new vscode.Position(1, 1), // false + new vscode.Position(1, 3), // false + new vscode.Position(2, 7), // false + new vscode.Position(3, 0), // false + new vscode.Position(4, 2), // false + new vscode.Position(4, 8), // false + new vscode.Position(5, 4), // false + new vscode.Position(5, 10) // false + ]; + const expected = [ + false, true, false, false, false, false, false, false, false, false, false + ]; + const textDocument = await vscode.workspace.openTextDocument(fileSuppress); + await vscode.window.showTextDocument(textDocument); + for (let i = 0; i < positions.length; i += 1) { + const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, positions[i]); + const result = list!.items.filter(item => item.label === 'abs').length; + assert.equal(result > 0, expected[i], + `Expected ${expected[i]} at position ${positions[i].line}:${positions[i].character} but got ${result}`); + } + }); +}); + +// tslint:disable-next-line:no-any +function checkDocumentation(item: vscode.CompletionItem, expectedContains: string): void { + let isValidType = false; + let documentation: string; + + if (typeof item.documentation === 'string') { + isValidType = true; + documentation = item.documentation; + } else { + documentation = (item.documentation as vscode.MarkdownString).value; + isValidType = documentation !== undefined && documentation !== null; + } + assert.equal(isValidType, true, 'Documentation is neither string nor vscode.MarkdownString'); + + const inDoc = documentation.indexOf(expectedContains) >= 0; + assert.equal(inDoc, true, 'Documentation incorrect'); +} diff --git a/src/test/autocomplete/pep484.test.ts b/src/test/autocomplete/pep484.test.ts index 1eb13a792f1c..a6d77af91a26 100644 --- a/src/test/autocomplete/pep484.test.ts +++ b/src/test/autocomplete/pep484.test.ts @@ -2,6 +2,7 @@ import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; import { rootWorkspaceUri } from '../common'; +import { IsLanguageServerTest } from '../constants'; import { closeActiveWindows, initialize, initializeTest } from '../initialize'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; @@ -11,10 +12,20 @@ const filePep484 = path.join(autoCompPath, 'pep484.py'); suite('Autocomplete PEP 484', () => { let isPython2: boolean; let ioc: UnitTestIocContainer; - suiteSetup(async () => { + suiteSetup(async function () { + // https://github.com/Microsoft/PTVS/issues/3917 + if (IsLanguageServerTest()) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } await initialize(); initializeDI(); isPython2 = await ioc.getPythonMajorVersion(rootWorkspaceUri) === 2; + if (isPython2) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + return; + } }); setup(initializeTest); suiteTeardown(closeActiveWindows); @@ -29,12 +40,7 @@ suite('Autocomplete PEP 484', () => { ioc.registerProcessTypes(); } - test('argument', async function () { - if (isPython2) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - return; - } + test('argument', async () => { const textDocument = await vscode.workspace.openTextDocument(filePep484); await vscode.window.showTextDocument(textDocument); assert(vscode.window.activeTextEditor, 'No active editor'); @@ -46,9 +52,6 @@ suite('Autocomplete PEP 484', () => { }); test('return value', async () => { - if (isPython2) { - return; - } const textDocument = await vscode.workspace.openTextDocument(filePep484); await vscode.window.showTextDocument(textDocument); assert(vscode.window.activeTextEditor, 'No active editor'); diff --git a/src/test/autocomplete/pep526.test.ts b/src/test/autocomplete/pep526.test.ts index 099df5af10ab..81de7ed79bd1 100644 --- a/src/test/autocomplete/pep526.test.ts +++ b/src/test/autocomplete/pep526.test.ts @@ -2,19 +2,30 @@ import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; import { rootWorkspaceUri } from '../common'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { closeActiveWindows, initialize, initializeTest, IsLanguageServerTest } from '../initialize'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); const filePep526 = path.join(autoCompPath, 'pep526.py'); +// tslint:disable-next-line:max-func-body-length suite('Autocomplete PEP 526', () => { let isPython2: boolean; let ioc: UnitTestIocContainer; - suiteSetup(async () => { + suiteSetup(async function () { + // https://github.com/Microsoft/PTVS/issues/3917 + if (IsLanguageServerTest()) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } await initialize(); initializeDI(); isPython2 = await ioc.getPythonMajorVersion(rootWorkspaceUri) === 2; + if (isPython2) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + return; + } }); setup(initializeTest); suiteTeardown(closeActiveWindows); @@ -29,12 +40,7 @@ suite('Autocomplete PEP 526', () => { ioc.registerProcessTypes(); } - test('variable (abc:str)', async function () { - if (isPython2) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - return; - } + test('variable (abc:str)', async () => { const textDocument = await vscode.workspace.openTextDocument(filePep526); await vscode.window.showTextDocument(textDocument); assert(vscode.window.activeTextEditor, 'No active editor'); @@ -45,11 +51,7 @@ suite('Autocomplete PEP 526', () => { assert.notEqual(list!.items.filter(item => item.label === 'lower').length, 0, 'lower not found'); }); - test('variable (abc: str = "")', async function () { - if (isPython2) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - } + test('variable (abc: str = "")', async () => { const textDocument = await vscode.workspace.openTextDocument(filePep526); await vscode.window.showTextDocument(textDocument); assert(vscode.window.activeTextEditor, 'No active editor'); @@ -60,12 +62,7 @@ suite('Autocomplete PEP 526', () => { assert.notEqual(list!.items.filter(item => item.label === 'lower').length, 0, 'lower not found'); }); - test('variable (abc = UNKNOWN # type: str)', async function () { - if (isPython2) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - return; - } + test('variable (abc = UNKNOWN # type: str)', async () => { const textDocument = await vscode.workspace.openTextDocument(filePep526); await vscode.window.showTextDocument(textDocument); assert(vscode.window.activeTextEditor, 'No active editor'); @@ -76,12 +73,7 @@ suite('Autocomplete PEP 526', () => { assert.notEqual(list!.items.filter(item => item.label === 'lower').length, 0, 'lower not found'); }); - test('class methods', async function () { - if (isPython2) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - return; - } + test('class methods', async () => { const textDocument = await vscode.workspace.openTextDocument(filePep526); await vscode.window.showTextDocument(textDocument); assert(vscode.window.activeTextEditor, 'No active editor'); @@ -94,12 +86,7 @@ suite('Autocomplete PEP 526', () => { assert.notEqual(list!.items.filter(item => item.label === 'b').length, 0, 'method b not found'); }); - test('class method types', async function () { - if (isPython2) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - return; - } + test('class method types', async () => { const textDocument = await vscode.workspace.openTextDocument(filePep526); await vscode.window.showTextDocument(textDocument); assert(vscode.window.activeTextEditor, 'No active editor'); diff --git a/src/test/ciConstants.ts b/src/test/ciConstants.ts new file mode 100644 index 000000000000..e1fc989e5fd8 --- /dev/null +++ b/src/test/ciConstants.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// +// Constants that pertain to CI processes/tests only. No dependencies on vscode! +// + +export const IS_APPVEYOR = process.env.APPVEYOR === 'true'; +export const IS_TRAVIS = process.env.TRAVIS === 'true'; +export const IS_VSTS = process.env.TF_BUILD !== undefined; +export const IS_CI_SERVER = IS_TRAVIS || IS_APPVEYOR || IS_VSTS; + +// Control JUnit-style output logging for reporting purposes. +let reportJunit: boolean = false; +if (IS_CI_SERVER && process.env.MOCHA_REPORTER_JUNIT !== undefined) { + reportJunit = process.env.MOCHA_REPORTER_JUNIT.toLowerCase() === 'true'; +} +export const MOCHA_REPORTER_JUNIT: boolean = reportJunit; +export const MOCHA_CI_REPORTFILE: string = MOCHA_REPORTER_JUNIT && process.env.MOCHA_CI_REPORTFILE !== undefined ? + process.env.MOCHA_CI_REPORTFILE : './junit-out.xml'; +export const MOCHA_CI_PROPERTIES: string = MOCHA_REPORTER_JUNIT && process.env.MOCHA_CI_PROPERTIES !== undefined ? + process.env.MOCHA_CI_PROPERTIES : ''; +export const MOCHA_CI_REPORTER_ID: string = MOCHA_REPORTER_JUNIT && process.env.MOCHA_CI_REPORTER_ID !== undefined ? + process.env.MOCHA_CI_REPORTER_ID : 'mocha-junit-reporter'; +export const IS_CI_SERVER_TEST_DEBUGGER = process.env.IS_CI_SERVER_TEST_DEBUGGER === '1'; diff --git a/src/test/common.ts b/src/test/common.ts index e399012adf2c..d2645fd5e31c 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -2,11 +2,19 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import { ConfigurationTarget, Uri, workspace } from 'vscode'; import { PythonSettings } from '../client/common/configSettings'; +import { EXTENSION_ROOT_DIR } from '../client/common/constants'; +import { sleep } from './core'; import { IS_MULTI_ROOT_TEST } from './initialize'; -const fileInNonRootWorkspace = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); +export * from './core'; + +// tslint:disable:no-non-null-assertion no-unsafe-any await-promise no-any no-use-before-declare no-string-based-set-timeout no-unsafe-any no-any no-invalid-this + +const fileInNonRootWorkspace = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'dummy.py'); export const rootWorkspaceUri = getWorkspaceRoot(); +export const PYTHON_PATH = getPythonPath(); + export type PythonSettingKeys = 'workspaceSymbols.enabled' | 'pythonPath' | 'linting.lintOnSave' | 'linting.enabled' | 'linting.pylintEnabled' | @@ -26,14 +34,14 @@ export async function updateSetting(setting: PythonSettingKeys, value: {} | unde PythonSettings.dispose(); return; } - // tslint:disable-next-line:await-promise await settings.update(setting, value, configTarget); + await sleep(2000); PythonSettings.dispose(); } function getWorkspaceRoot() { if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { - return Uri.file(path.join(__dirname, '..', '..', 'src', 'test')); + return Uri.file(path.join(EXTENSION_ROOT_DIR, 'src', 'test')); } if (workspace.workspaceFolders.length === 1) { return workspace.workspaceFolders[0].uri; @@ -42,25 +50,19 @@ function getWorkspaceRoot() { 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 no-invalid-this wrapped.call(this as Function, ...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); } }); @@ -85,11 +87,8 @@ async function setPythonPathInWorkspace(resource: string | Uri | undefined, conf } } async function restoreGlobalPythonPathSetting(): Promise { - // tslint:disable-next-line:no-any const pythonConfig = workspace.getConfiguration('python', null as any as Uri); - // tslint:disable-next-line:no-non-null-assertion const currentGlobalPythonPathSetting = pythonConfig.inspect('pythonPath')!.globalValue; - // tslint:disable-next-line:no-use-before-declare if (globalPythonPathSetting !== currentGlobalPythonPathSetting) { await pythonConfig.update('pythonPath', undefined, true); } @@ -109,12 +108,15 @@ export async function deleteFile(file: string) { } } -export async function sleep(milliseconds: number) { - return new Promise(resolve => setTimeout(resolve, milliseconds)); -} - -// tslint:disable-next-line:no-non-null-assertion -const globalPythonPathSetting = workspace.getConfiguration('python').inspect('pythonPath')!.globalValue; +// In some tests we will be mocking VS Code API (mocked classes) +const globalPythonPathSetting = workspace.getConfiguration('python') ? workspace.getConfiguration('python').inspect('pythonPath')!.globalValue : 'python'; 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)(); + +function getPythonPath(): string { + if (process.env.CI_PYTHON_PATH && fs.existsSync(process.env.CI_PYTHON_PATH)) { + return process.env.CI_PYTHON_PATH; + } + return 'python'; +} diff --git a/src/test/common/extensions.test.ts b/src/test/common/extensions.unit.test.ts similarity index 53% rename from src/test/common/extensions.test.ts rename to src/test/common/extensions.unit.test.ts index 5724f3291274..7018b8d002a8 100644 --- a/src/test/common/extensions.test.ts +++ b/src/test/common/extensions.unit.test.ts @@ -39,4 +39,36 @@ suite('String Extensions', () => { const fileToTest = 'c:\\users\\user namne\\conda path\\scripts\\python.exe'; expect(fileToTest.fileToCommandArgument()).to.be.equal(`"${fileToTest.replace(/\\/g, '/')}"`); }); + test('Should replace all back slashes with forward slashes (irrespective of OS) and quoted when file has spaces', () => { + const fileToTest = 'c:\\users\\user namne\\conda path\\scripts\\python.exe'; + expect(fileToTest.fileToCommandArgument()).to.be.equal(`"${fileToTest.replace(/\\/g, '/')}"`); + }); + test('Should leave string unchanged', () => { + expect('something {0}'.format()).to.be.equal('something {0}'); + }); + test('String should be formatted to contain first argument', () => { + const formatString = 'something {0}'; + const expectedString = 'something one'; + expect(formatString.format('one')).to.be.equal(expectedString); + }); + test('String should be formatted to contain first argument even with too many args', () => { + const formatString = 'something {0}'; + const expectedString = 'something one'; + expect(formatString.format('one', 'two')).to.be.equal(expectedString); + }); + test('String should be formatted to contain second argument', () => { + const formatString = 'something {1}'; + const expectedString = 'something two'; + expect(formatString.format('one', 'two')).to.be.equal(expectedString); + }); + test('String should be formatted to contain second argument even with too many args', () => { + const formatString = 'something {1}'; + const expectedString = 'something two'; + expect(formatString.format('one', 'two', 'three')).to.be.equal(expectedString); + }); + test('String should be formatted with multiple args', () => { + const formatString = 'something {1}, {0}'; + const expectedString = 'something two, one'; + expect(formatString.format('one', 'two', 'three')).to.be.equal(expectedString); + }); }); diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index d00258561d34..e32ceccf6d73 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -1,21 +1,22 @@ import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, Uri } from 'vscode'; -import { IApplicationShell } from '../../client/common/application/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; import { ConfigurationService } from '../../client/common/configuration/service'; import { EnumEx } from '../../client/common/enumUtils'; import { createDeferred } from '../../client/common/helpers'; import { InstallationChannelManager } from '../../client/common/installer/channelManager'; import { ProductInstaller } from '../../client/common/installer/productInstaller'; -import { IInstallationChannelManager, IModuleInstaller } from '../../client/common/installer/types'; +import { CTagsProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; +import { ProductService } from '../../client/common/installer/productService'; +import { IInstallationChannelManager, IModuleInstaller, IProductPathService, IProductService } from '../../client/common/installer/types'; import { Logger } from '../../client/common/logger'; import { PersistentStateFactory } from '../../client/common/persistentState'; import { PathUtils } from '../../client/common/platform/pathUtils'; import { CurrentProcess } from '../../client/common/process/currentProcess'; -import { IProcessService } from '../../client/common/process/types'; -import { IConfigurationService, ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IsWindows, ModuleNamePurpose, Product } from '../../client/common/types'; -import { rootWorkspaceUri } from '../common'; -import { updateSetting } from '../common'; +import { IProcessServiceFactory } from '../../client/common/process/types'; +import { IConfigurationService, ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IsWindows, ModuleNamePurpose, Product, ProductType } from '../../client/common/types'; +import { rootWorkspaceUri, updateSetting } from '../common'; import { MockModuleInstaller } from '../mocks/moduleInstaller'; import { MockProcessService } from '../mocks/proc'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; @@ -55,12 +56,23 @@ suite('Installer', () => { ioc.serviceManager.addSingleton(IPathUtils, PathUtils); ioc.serviceManager.addSingleton(ICurrentProcess, CurrentProcess); ioc.serviceManager.addSingleton(IInstallationChannelManager, InstallationChannelManager); + ioc.serviceManager.addSingletonInstance(ICommandManager, TypeMoq.Mock.ofType().object); ioc.serviceManager.addSingletonInstance(IApplicationShell, TypeMoq.Mock.ofType().object); ioc.serviceManager.addSingleton(IConfigurationService, ConfigurationService); + const workspaceService = TypeMoq.Mock.ofType(); + workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + ioc.serviceManager.addSingletonInstance(IWorkspaceService, workspaceService.object); + ioc.registerMockProcessTypes(); ioc.serviceManager.addSingletonInstance(IsWindows, false); + ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); + ioc.serviceManager.addSingleton(IProductPathService, CTagsProductPathService, ProductType.WorkspaceSymbols); + ioc.serviceManager.addSingleton(IProductPathService, FormatterProductPathService, ProductType.Formatter); + ioc.serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); + ioc.serviceManager.addSingleton(IProductPathService, TestFrameworkProductPathService, ProductType.TestFramework); + ioc.serviceManager.addSingleton(IProductPathService, RefactoringLibraryProductPathService, ProductType.RefactoringLibrary); } async function resetSettings() { await updateSetting('linting.pylintEnabled', true, rootWorkspaceUri, ConfigurationTarget.Workspace); @@ -68,16 +80,13 @@ suite('Installer', () => { async function testCheckingIfProductIsInstalled(product: Product) { const installer = ioc.serviceContainer.get(IInstaller); - const processService = ioc.serviceContainer.get(IProcessService); + const processService = await ioc.serviceContainer.get(IProcessServiceFactory).create() as MockProcessService; const checkInstalledDef = createDeferred(); processService.onExec((file, args, options, callback) => { const moduleName = installer.translateProductToModuleName(product, ModuleNamePurpose.run); if (args.length > 1 && args[0] === '-c' && args[1] === `import ${moduleName}`) { checkInstalledDef.resolve(true); } - if (product === Product.prospector && args.length > 0 && args[0] === '--version') { - checkInstalledDef.resolve(true); - } callback({ stdout: '' }); }); await installer.isInstalled(product, resource); diff --git a/src/test/common/installer/installer.invalidPath.unit.test.ts b/src/test/common/installer/installer.invalidPath.unit.test.ts new file mode 100644 index 000000000000..27b1d726518a --- /dev/null +++ b/src/test/common/installer/installer.invalidPath.unit.test.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { OutputChannel, Uri } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; +import { EnumEx } from '../../../client/common/enumUtils'; +import '../../../client/common/extensions'; +import { ProductInstaller } from '../../../client/common/installer/productInstaller'; +import { ProductService } from '../../../client/common/installer/productService'; +import { IProductPathService, IProductService } from '../../../client/common/installer/types'; +import { Product } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +use(chaiAsPromised); + +suite('Module Installer - Invalid Paths', () => { + [undefined, Uri.file('resource')].forEach(resource => { + ['moduleName', path.join('users', 'dev', 'tool', 'executable')].forEach(pathToExecutable => { + const isExecutableAModule = path.basename(pathToExecutable) === pathToExecutable; + + EnumEx.getNamesAndValues(Product).forEach(product => { + let installer: ProductInstaller; + let serviceContainer: TypeMoq.IMock; + let app: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + let productPathService: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + const outputChannel = TypeMoq.Mock.ofType(); + + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProductService), TypeMoq.It.isAny())).returns(() => new ProductService()); + app = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())).returns(() => app.object); + workspaceService = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())).returns(() => workspaceService.object); + + productPathService = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProductPathService), TypeMoq.It.isAny())).returns(() => productPathService.object); + + installer = new ProductInstaller(serviceContainer.object, outputChannel.object); + }); + + switch (product.value) { + case Product.isort: + case Product.ctags: + case Product.rope: + case Product.unittest: { + return; + } + default: { + test(`Ensure invalid path message is ${isExecutableAModule ? 'not displayed' : 'displayed'} ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { + // If the path to executable is a module, then we won't display error message indicating path is invalid. + + productPathService + .setup(p => p.getExecutableNameFromSettings(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) + .returns(() => pathToExecutable) + .verifiable(TypeMoq.Times.atLeast(isExecutableAModule ? 0 : 1)); + productPathService + .setup(p => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) + .returns(() => isExecutableAModule) + .verifiable(TypeMoq.Times.atLeastOnce()); + const anyParams = [0, 1, 2, 3, 4, 5].map(() => TypeMoq.It.isAny()); + app.setup(a => a.showErrorMessage(TypeMoq.It.isAny(), ...anyParams)) + .callback(message => { + if (!isExecutableAModule) { + expect(message).contains(pathToExecutable); + } + }) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.exactly(1)); + + await installer.promptToInstall(product.value, resource); + productPathService.verifyAll(); + }); + } + } + }); + }); + }); +}); diff --git a/src/test/common/installer/installer.test.ts b/src/test/common/installer/installer.unit.test.ts similarity index 51% rename from src/test/common/installer/installer.test.ts rename to src/test/common/installer/installer.unit.test.ts index 2d080f672983..09c4de523d1d 100644 --- a/src/test/common/installer/installer.test.ts +++ b/src/test/common/installer/installer.unit.test.ts @@ -1,40 +1,49 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// tslint:disable:max-func-body-length no-invalid-this + import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as TypeMoq from 'typemoq'; -import { Disposable, OutputChannel, Uri } from 'vscode'; +import { Disposable, OutputChannel, Uri, WorkspaceFolder } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; import { EnumEx } from '../../../client/common/enumUtils'; -import { Installer } from '../../../client/common/installer/installer'; +import '../../../client/common/extensions'; +import { createDeferred, Deferred } from '../../../client/common/helpers'; import { ProductInstaller } from '../../../client/common/installer/productInstaller'; -import { IInstallationChannelManager, IModuleInstaller } from '../../../client/common/installer/types'; +import { ProductService } from '../../../client/common/installer/productService'; +import { IInstallationChannelManager, IModuleInstaller, IProductPathService, IProductService } from '../../../client/common/installer/types'; import { IDisposableRegistry, ILogger, InstallerResponse, ModuleNamePurpose, Product } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; use(chaiAsPromised); -// tslint:disable-next-line:max-func-body-length -suite('Module Installerx', () => { +suite('Module Installer', () => { [undefined, Uri.file('resource')].forEach(resource => { EnumEx.getNamesAndValues(Product).forEach(product => { let disposables: Disposable[] = []; - let installer: Installer; + let installer: ProductInstaller; let installationChannel: TypeMoq.IMock; let moduleInstaller: TypeMoq.IMock; let serviceContainer: TypeMoq.IMock; - let productInstallerFactory: ProductInstaller; + let app: TypeMoq.IMock; + let promptDeferred: Deferred; + let workspaceService: TypeMoq.IMock; setup(() => { + promptDeferred = createDeferred(); serviceContainer = TypeMoq.Mock.ofType(); const outputChannel = TypeMoq.Mock.ofType(); - installer = new Installer(serviceContainer.object, outputChannel.object); - disposables = []; serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())).returns(() => disposables); - + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProductService), TypeMoq.It.isAny())).returns(() => new ProductService()); installationChannel = TypeMoq.Mock.ofType(); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInstallationChannelManager), TypeMoq.It.isAny())).returns(() => installationChannel.object); + app = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())).returns(() => app.object); + workspaceService = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())).returns(() => workspaceService.object); moduleInstaller = TypeMoq.Mock.ofType(); // tslint:disable-next-line:no-any @@ -42,9 +51,16 @@ suite('Module Installerx', () => { installationChannel.setup(i => i.getInstallationChannel(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(moduleInstaller.object)); installationChannel.setup(i => i.getInstallationChannel(TypeMoq.It.isAny())).returns(() => Promise.resolve(moduleInstaller.object)); - productInstallerFactory = new ProductInstaller(serviceContainer.object, outputChannel.object); + const productPathService = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProductPathService), TypeMoq.It.isAny())).returns(() => productPathService.object); + productPathService.setup(p => p.getExecutableNameFromSettings(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))).returns(() => 'xyz'); + productPathService.setup(p => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))).returns(() => true); + + installer = new ProductInstaller(serviceContainer.object, outputChannel.object); }); teardown(() => { + // This must be resolved, else all subsequent tests will fail (as this same promise will be used for other tests). + promptDeferred.resolve(); disposables.forEach(disposable => { if (disposable) { disposable.dispose(); @@ -91,11 +107,52 @@ suite('Module Installerx', () => { moduleInstaller.setup(m => m.installModule(TypeMoq.It.isValue(moduleName), TypeMoq.It.isValue(resource))).returns(() => Promise.reject(new Error('UnitTesting'))); try { - await productInstallerFactory.install(product.value, resource); + await installer.install(product.value, resource); } catch (ex) { moduleInstaller.verify(m => m.installModule(TypeMoq.It.isValue(moduleName), TypeMoq.It.isValue(resource)), TypeMoq.Times.once()); } }); + test(`Ensure the prompt is displayed only once, untill the prompt is closed, ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async function () { + if (product.value === Product.unittest) { + return this.skip(); + } + workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) + .returns(() => TypeMoq.Mock.ofType().object) + .verifiable(TypeMoq.Times.exactly(resource ? 5 : 0)); + app.setup(a => a.showErrorMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => promptDeferred.promise) + .verifiable(TypeMoq.Times.once()); + + // Display first prompt. + installer.promptToInstall(product.value, resource).ignoreErrors(); + + // Display a few more prompts. + installer.promptToInstall(product.value, resource).ignoreErrors(); + installer.promptToInstall(product.value, resource).ignoreErrors(); + installer.promptToInstall(product.value, resource).ignoreErrors(); + installer.promptToInstall(product.value, resource).ignoreErrors(); + + app.verifyAll(); + workspaceService.verifyAll(); + }); + test(`Ensure the prompt is displayed again when previous prompt has been closed, ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async function () { + if (product.value === Product.unittest) { + return this.skip(); + } + workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) + .returns(() => TypeMoq.Mock.ofType().object) + .verifiable(TypeMoq.Times.exactly(resource ? 3 : 0)); + app.setup(a => a.showErrorMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.exactly(3)); + + await installer.promptToInstall(product.value, resource); + await installer.promptToInstall(product.value, resource); + await installer.promptToInstall(product.value, resource); + + app.verifyAll(); + workspaceService.verifyAll(); + }); } } }); diff --git a/src/test/common/installer/moduleInstaller.unit.test.ts b/src/test/common/installer/moduleInstaller.unit.test.ts new file mode 100644 index 000000000000..c57c9befe149 --- /dev/null +++ b/src/test/common/installer/moduleInstaller.unit.test.ts @@ -0,0 +1,269 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length no-invalid-this + +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Disposable, OutputChannel, Uri, WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { noop } from '../../../client/common/core.utils'; +import { EnumEx } from '../../../client/common/enumUtils'; +import { CondaInstaller } from '../../../client/common/installer/condaInstaller'; +import { PipEnvInstaller, pipenvName } from '../../../client/common/installer/pipEnvInstaller'; +import { PipInstaller } from '../../../client/common/installer/pipInstaller'; +import { ProductInstaller } from '../../../client/common/installer/productInstaller'; +import { IInstallationChannelManager, IModuleInstaller } from '../../../client/common/installer/types'; +import { PythonVersionInfo } from '../../../client/common/process/types'; +import { ITerminalService, ITerminalServiceFactory } from '../../../client/common/terminal/types'; +import { IConfigurationService, IDisposableRegistry, IPythonSettings, ModuleNamePurpose, Product } from '../../../client/common/types'; +import { ICondaService, IInterpreterService, InterpreterType, PythonInterpreter } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; + +/* Complex test to ensure we cover all combinations: +We could have written separate tests for each installer, but we'd be replicate code. +Both approachs have their benefits. + +Comnbinations of: +1. With and without a workspace. +2. Http Proxy configuration. +3. All products. +4. Different versions of Python. +5. With and without conda. +6. Conda environments with names and without names. +7. All installers. +*/ +suite('Module Installer', () => { + const pythonPath = path.join(__dirname, 'python'); + [CondaInstaller, PipInstaller, PipEnvInstaller].forEach(installerClass => { + // Proxy info is relevant only for PipInstaller. + const proxyServers = installerClass === PipInstaller ? ['', 'proxy:1234'] : ['']; + proxyServers.forEach(proxyServer => { + [undefined, Uri.file('/users/dev/xyz')].forEach(resource => { + // Conda info is relevant only for CondaInstaller. + const condaEnvs = installerClass === CondaInstaller ? [{ name: 'My-Env01', path: '' }, { name: '', path: '/conda/path' }] : []; + [undefined, ...condaEnvs].forEach(condaEnvInfo => { + const testProxySuffix = proxyServer.length === 0 ? 'without proxy info' : 'with proxy info'; + const testCondaEnv = condaEnvInfo ? (condaEnvInfo.name ? 'without conda name' : 'with conda path') : 'without conda'; + const testSuite = [testProxySuffix, testCondaEnv].filter(item => item.length > 0).join(', '); + suite(`${installerClass.name} (${testSuite})`, () => { + let disposables: Disposable[] = []; + let installer: IModuleInstaller; + let installationChannel: TypeMoq.IMock; + let serviceContainer: TypeMoq.IMock; + let terminalService: TypeMoq.IMock; + let pythonSettings: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + const condaExecutable = 'my.exe'; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + + disposables = []; + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())).returns(() => disposables); + + installationChannel = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInstallationChannelManager), TypeMoq.It.isAny())).returns(() => installationChannel.object); + + const condaService = TypeMoq.Mock.ofType(); + condaService.setup(c => c.getCondaFile()).returns(() => Promise.resolve(condaExecutable)); + condaService.setup(c => c.getCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(condaEnvInfo)); + + const configService = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())).returns(() => configService.object); + pythonSettings = TypeMoq.Mock.ofType(); + pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + + terminalService = TypeMoq.Mock.ofType(); + const terminalServiceFactory = TypeMoq.Mock.ofType(); + terminalServiceFactory.setup(f => f.getTerminalService(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => terminalService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory), TypeMoq.It.isAny())).returns(() => terminalServiceFactory.object); + + interpreterService = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())).returns(() => interpreterService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICondaService), TypeMoq.It.isAny())).returns(() => condaService.object); + + const workspaceService = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())).returns(() => workspaceService.object); + const http = TypeMoq.Mock.ofType(); + http.setup(h => h.get(TypeMoq.It.isValue('proxy'), TypeMoq.It.isAny())).returns(() => proxyServer); + workspaceService.setup(w => w.getConfiguration(TypeMoq.It.isValue('http'))).returns(() => http.object); + + installer = new installerClass(serviceContainer.object); + }); + teardown(() => { + disposables.forEach(disposable => { + if (disposable) { + disposable.dispose(); + } + }); + }); + function setActiveInterpreter(activeInterpreter?: PythonInterpreter) { + interpreterService + .setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve(activeInterpreter)) + .verifiable(TypeMoq.Times.atLeastOnce()); + } + getModuleNamesForTesting().forEach(product => { + const moduleName = product.moduleName; + async function installModuleAndVerifyCommand(command: string, expectedArgs: string[]) { + terminalService.setup(t => t.sendCommand(TypeMoq.It.isValue(command), TypeMoq.It.isValue(expectedArgs))) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await installer.installModule(moduleName, resource); + terminalService.verifyAll(); + } + + if (product.value === Product.pylint) { + // tslint:disable-next-line:no-shadowed-variable + generatePythonInterpreterVersions().forEach(interpreterInfo => { + const majorVersion = interpreterInfo.version_info[0]; + if (majorVersion === 2) { + const testTitle = `Ensure install arg is \'pylint<2.0.0\' in ${interpreterInfo.version_info.join('.')}`; + if (installerClass === PipInstaller) { + test(testTitle, async () => { + setActiveInterpreter(interpreterInfo); + const proxyArgs = proxyServer.length === 0 ? [] : ['--proxy', proxyServer]; + const expectedArgs = ['-m', 'pip', ...proxyArgs, 'install', '-U', '"pylint<2.0.0"']; + await installModuleAndVerifyCommand(pythonPath, expectedArgs); + }); + } + if (installerClass === PipEnvInstaller) { + test(testTitle, async () => { + setActiveInterpreter(interpreterInfo); + const expectedArgs = ['install', '"pylint<2.0.0"', '--dev']; + await installModuleAndVerifyCommand(pipenvName, expectedArgs); + }); + } + if (installerClass === CondaInstaller) { + test(testTitle, async () => { + setActiveInterpreter(interpreterInfo); + const expectedArgs = ['install']; + if (condaEnvInfo && condaEnvInfo.name) { + expectedArgs.push('--name'); + expectedArgs.push(condaEnvInfo.name); + } else if (condaEnvInfo && condaEnvInfo.path) { + expectedArgs.push('--prefix'); + expectedArgs.push(condaEnvInfo.path); + } + expectedArgs.push('"pylint<2.0.0"'); + await installModuleAndVerifyCommand(condaExecutable, expectedArgs); + }); + } + } else { + const testTitle = `Ensure install arg is \'pylint\' in ${interpreterInfo.version_info.join('.')}`; + if (installerClass === PipInstaller) { + test(testTitle, async () => { + setActiveInterpreter(interpreterInfo); + const proxyArgs = proxyServer.length === 0 ? [] : ['--proxy', proxyServer]; + const expectedArgs = ['-m', 'pip', ...proxyArgs, 'install', '-U', 'pylint']; + await installModuleAndVerifyCommand(pythonPath, expectedArgs); + }); + } + if (installerClass === PipEnvInstaller) { + test(testTitle, async () => { + setActiveInterpreter(interpreterInfo); + const expectedArgs = ['install', 'pylint', '--dev']; + await installModuleAndVerifyCommand(pipenvName, expectedArgs); + }); + } + if (installerClass === CondaInstaller) { + test(testTitle, async () => { + setActiveInterpreter(interpreterInfo); + const expectedArgs = ['install']; + if (condaEnvInfo && condaEnvInfo.name) { + expectedArgs.push('--name'); + expectedArgs.push(condaEnvInfo.name); + } else if (condaEnvInfo && condaEnvInfo.path) { + expectedArgs.push('--prefix'); + expectedArgs.push(condaEnvInfo.path); + } + expectedArgs.push('pylint'); + await installModuleAndVerifyCommand(condaExecutable, expectedArgs); + }); + } + } + }); + return; + } + + if (installerClass === PipInstaller) { + test(`Ensure getActiveInterperter is used in PipInstaller (${product.name})`, async () => { + setActiveInterpreter(); + try { + await installer.installModule(product.name, resource); + } catch { + noop(); + } + interpreterService.verifyAll(); + }); + } + if (installerClass === PipInstaller) { + test(`Test Args (${product.name})`, async () => { + setActiveInterpreter(); + const proxyArgs = proxyServer.length === 0 ? [] : ['--proxy', proxyServer]; + const expectedArgs = ['-m', 'pip', ...proxyArgs, 'install', '-U', moduleName]; + await installModuleAndVerifyCommand(pythonPath, expectedArgs); + interpreterService.verifyAll(); + }); + } + if (installerClass === PipEnvInstaller) { + test(`Test args (${product.name})`, async () => { + setActiveInterpreter(); + const expectedArgs = ['install', moduleName, '--dev']; + await installModuleAndVerifyCommand(pipenvName, expectedArgs); + }); + } + if (installerClass === CondaInstaller) { + test(`Test args (${product.name})`, async () => { + setActiveInterpreter(); + const expectedArgs = ['install']; + if (condaEnvInfo && condaEnvInfo.name) { + expectedArgs.push('--name'); + expectedArgs.push(condaEnvInfo.name); + } else if (condaEnvInfo && condaEnvInfo.path) { + expectedArgs.push('--prefix'); + expectedArgs.push(condaEnvInfo.path); + } + expectedArgs.push(moduleName); + await installModuleAndVerifyCommand(condaExecutable, expectedArgs); + }); + } + }); + }); + }); + }); + }); + }); +}); + +function generatePythonInterpreterVersions() { + const versions: PythonVersionInfo[] = [[2, 7, 0, 'final'], [3, 4, 0, 'final'], [3, 5, 0, 'final'], [3, 6, 0, 'final'], [3, 7, 0, 'final']]; + return versions.map(version => { + const info = TypeMoq.Mock.ofType(); + info.setup((t: any) => t.then).returns(() => undefined); + info.setup(t => t.type).returns(() => InterpreterType.VirtualEnv); + info.setup(t => t.version_info).returns(() => version); + return info.object; + }); +} + +function getModuleNamesForTesting(): { name: string; value: Product; moduleName: string }[] { + return EnumEx.getNamesAndValues(Product) + .map(product => { + let moduleName = ''; + const mockSvc = TypeMoq.Mock.ofType().object; + const mockOutChnl = TypeMoq.Mock.ofType().object; + try { + const prodInstaller = new ProductInstaller(mockSvc, mockOutChnl); + moduleName = prodInstaller.translateProductToModuleName(product.value, ModuleNamePurpose.install); + return { name: product.name, value: product.value, moduleName }; + } catch { + return; + } + }) + .filter(item => item !== undefined) as { name: string; value: Product; moduleName: string }[]; +} diff --git a/src/test/common/installer/productPath.unit.test.ts b/src/test/common/installer/productPath.unit.test.ts new file mode 100644 index 000000000000..3e34ce86677a --- /dev/null +++ b/src/test/common/installer/productPath.unit.test.ts @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-invalid-this + +import { fail } from 'assert'; +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as TypeMoq from 'typemoq'; +import { OutputChannel, Uri } from 'vscode'; +import { EnumEx } from '../../../client/common/enumUtils'; +import '../../../client/common/extensions'; +import { ProductInstaller } from '../../../client/common/installer/productInstaller'; +import { CTagsProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../../client/common/installer/productPath'; +import { ProductService } from '../../../client/common/installer/productService'; +import { IProductService } from '../../../client/common/installer/types'; +import { IConfigurationService, IFormattingSettings, IInstaller, IPythonSettings, IUnitTestSettings, IWorkspaceSymbolSettings, ModuleNamePurpose, Product, ProductType } from '../../../client/common/types'; +import { IFormatterHelper } from '../../../client/formatters/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { ILinterInfo, ILinterManager } from '../../../client/linters/types'; +import { ITestsHelper } from '../../../client/unittests/common/types'; + +use(chaiAsPromised); + +suite('Product Path', () => { + [undefined, Uri.file('resource')].forEach(resource => { + EnumEx.getNamesAndValues(Product).forEach(product => { + let serviceContainer: TypeMoq.IMock; + let formattingSettings: TypeMoq.IMock; + let unitTestSettings: TypeMoq.IMock; + let workspaceSymnbolSettings: TypeMoq.IMock; + let configService: TypeMoq.IMock; + let productInstaller: ProductInstaller; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + configService = TypeMoq.Mock.ofType(); + formattingSettings = TypeMoq.Mock.ofType(); + unitTestSettings = TypeMoq.Mock.ofType(); + workspaceSymnbolSettings = TypeMoq.Mock.ofType(); + + productInstaller = new ProductInstaller(serviceContainer.object, TypeMoq.Mock.ofType().object); + const pythonSettings = TypeMoq.Mock.ofType(); + pythonSettings.setup(p => p.formatting).returns(() => formattingSettings.object); + pythonSettings.setup(p => p.unitTest).returns(() => unitTestSettings.object); + pythonSettings.setup(p => p.workspaceSymbols).returns(() => workspaceSymnbolSettings.object); + configService.setup(s => s.getSettings(TypeMoq.It.isValue(resource))) + .returns(() => pythonSettings.object); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) + .returns(() => configService.object); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IInstaller), TypeMoq.It.isAny())) + .returns(() => productInstaller); + + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProductService), TypeMoq.It.isAny())).returns(() => new ProductService()); + }); + + if (product.value === Product.isort) { + return; + } + const productType = new ProductService().getProductType(product.value); + switch (productType) { + case ProductType.Formatter: { + test(`Ensure path is returned for ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { + const productPathService = new FormatterProductPathService(serviceContainer.object); + const formatterHelper = TypeMoq.Mock.ofType(); + const expectedPath = 'Some Path'; + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IFormatterHelper), TypeMoq.It.isAny())) + .returns(() => formatterHelper.object); + formattingSettings.setup(f => f.autopep8Path) + .returns(() => expectedPath) + .verifiable(TypeMoq.Times.atLeastOnce()); + formatterHelper.setup(f => f.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) + .returns(() => { + return { + pathName: 'autopep8Path', + argsName: 'autopep8Args' + }; + }) + .verifiable(TypeMoq.Times.once()); + + const value = productPathService.getExecutableNameFromSettings(product.value, resource); + expect(value).to.be.equal(expectedPath); + formattingSettings.verifyAll(); + formatterHelper.verifyAll(); + }); + break; + } + case ProductType.Linter: { + test(`Ensure path is returned for ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { + const productPathService = new LinterProductPathService(serviceContainer.object); + const linterManager = TypeMoq.Mock.ofType(); + const linterInfo = TypeMoq.Mock.ofType(); + const expectedPath = 'Some Path'; + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ILinterManager), TypeMoq.It.isAny())) + .returns(() => linterManager.object); + linterInfo.setup(l => l.pathName(TypeMoq.It.isValue(resource))) + .returns(() => expectedPath) + .verifiable(TypeMoq.Times.once()); + linterManager.setup(l => l.getLinterInfo(TypeMoq.It.isValue(product.value))) + .returns(() => linterInfo.object) + .verifiable(TypeMoq.Times.once()); + + const value = productPathService.getExecutableNameFromSettings(product.value, resource); + expect(value).to.be.equal(expectedPath); + linterInfo.verifyAll(); + linterManager.verifyAll(); + }); + } + case ProductType.RefactoringLibrary: { + test(`Ensure path is returned for ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { + const productPathService = new RefactoringLibraryProductPathService(serviceContainer.object); + + const value = productPathService.getExecutableNameFromSettings(product.value, resource); + const moduleName = productInstaller.translateProductToModuleName(product.value, ModuleNamePurpose.run); + expect(value).to.be.equal(moduleName); + }); + break; + } + case ProductType.WorkspaceSymbols: { + test(`Ensure path is returned for ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { + const productPathService = new CTagsProductPathService(serviceContainer.object); + const expectedPath = 'Some Path'; + workspaceSymnbolSettings.setup(w => w.ctagsPath) + .returns(() => expectedPath) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const value = productPathService.getExecutableNameFromSettings(product.value, resource); + expect(value).to.be.equal(expectedPath); + workspaceSymnbolSettings.verifyAll(); + }); + break; + } + case ProductType.TestFramework: { + test(`Ensure path is returned for ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { + const productPathService = new TestFrameworkProductPathService(serviceContainer.object); + const testHelper = TypeMoq.Mock.ofType(); + const expectedPath = 'Some Path'; + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ITestsHelper), TypeMoq.It.isAny())) + .returns(() => testHelper.object); + testHelper.setup(t => t.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) + .returns(() => { + return { + argsName: 'autoTestDiscoverOnSaveEnabled', + enabledName: 'autoTestDiscoverOnSaveEnabled', + pathName: 'nosetestPath' + }; + }) + .verifiable(TypeMoq.Times.once()); + unitTestSettings.setup(u => u.nosetestPath) + .returns(() => expectedPath) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const value = productPathService.getExecutableNameFromSettings(product.value, resource); + expect(value).to.be.equal(expectedPath); + testHelper.verifyAll(); + unitTestSettings.verifyAll(); + }); + test(`Ensure module name is returned for ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { + const productPathService = new TestFrameworkProductPathService(serviceContainer.object); + const testHelper = TypeMoq.Mock.ofType(); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ITestsHelper), TypeMoq.It.isAny())) + .returns(() => testHelper.object); + testHelper.setup(t => t.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) + .returns(() => { + return { + argsName: 'autoTestDiscoverOnSaveEnabled', + enabledName: 'autoTestDiscoverOnSaveEnabled', + pathName: undefined + }; + }) + .verifiable(TypeMoq.Times.once()); + + const value = productPathService.getExecutableNameFromSettings(product.value, resource); + const moduleName = productInstaller.translateProductToModuleName(product.value, ModuleNamePurpose.run); + expect(value).to.be.equal(moduleName); + testHelper.verifyAll(); + }); + break; + } + default: { + test(`No tests for Product Path of this Product Type ${product.name}`, () => { + fail('No tests for Product Path of this Product Type'); + }); + } + } + }); + }); +}); diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index 91c025d8c49a..0cf8698fc644 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -1,13 +1,16 @@ +// tslint:disable:max-func-body-length + import { expect } from 'chai'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri } from 'vscode'; +import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../client/common/application/types'; import { PythonSettings } from '../../client/common/configSettings'; import { ConfigurationService } from '../../client/common/configuration/service'; import { CondaInstaller } from '../../client/common/installer/condaInstaller'; -import { Installer } from '../../client/common/installer/installer'; import { PipEnvInstaller } from '../../client/common/installer/pipEnvInstaller'; import { PipInstaller } from '../../client/common/installer/pipInstaller'; +import { ProductInstaller } from '../../client/common/installer/productInstaller'; import { IModuleInstaller } from '../../client/common/installer/types'; import { Logger } from '../../client/common/logger'; import { PersistentStateFactory } from '../../client/common/persistentState'; @@ -16,229 +19,288 @@ import { PathUtils } from '../../client/common/platform/pathUtils'; import { PlatformService } from '../../client/common/platform/platformService'; import { Architecture, IFileSystem, IPlatformService } from '../../client/common/platform/types'; import { CurrentProcess } from '../../client/common/process/currentProcess'; -import { IProcessService, IPythonExecutionFactory } from '../../client/common/process/types'; +import { IProcessServiceFactory, IPythonExecutionFactory } from '../../client/common/process/types'; import { ITerminalService, ITerminalServiceFactory } from '../../client/common/terminal/types'; import { IConfigurationService, ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IPythonSettings, IsWindows } from '../../client/common/types'; -import { ICondaService, IInterpreterLocatorService, IInterpreterService, INTERPRETER_LOCATOR_SERVICE, InterpreterType, PIPENV_SERVICE } from '../../client/interpreter/contracts'; +import { ICondaService, IInterpreterLocatorService, IInterpreterService, INTERPRETER_LOCATOR_SERVICE, InterpreterType, PIPENV_SERVICE, PythonInterpreter } from '../../client/interpreter/contracts'; import { IServiceContainer } from '../../client/ioc/types'; -import { rootWorkspaceUri } from '../common'; +import { PYTHON_PATH, rootWorkspaceUri } from '../common'; import { MockModuleInstaller } from '../mocks/moduleInstaller'; import { MockProcessService } from '../mocks/proc'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; import { closeActiveWindows, initializeTest } from './../initialize'; -// tslint:disable-next-line:max-func-body-length +const info: PythonInterpreter = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + type: InterpreterType.Unknown, + version: '', + version_info: [0, 0, 0, 'alpha'], + sysPrefix: '', + sysVersion: '' +}; + suite('Module Installer', () => { - let ioc: UnitTestIocContainer; - let mockTerminalService: TypeMoq.IMock; - let condaService: TypeMoq.IMock; - let interpreterService: TypeMoq.IMock; - - const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); - suiteSetup(initializeTest); - setup(async () => { - initializeDI(); - await initializeTest(); - await resetSettings(); - }); - suiteTeardown(async () => { - await closeActiveWindows(); - await resetSettings(); - }); - teardown(async () => { - ioc.dispose(); - await closeActiveWindows(); - }); + [undefined, Uri.file(__filename)].forEach(resource => { + let ioc: UnitTestIocContainer; + let mockTerminalService: TypeMoq.IMock; + let condaService: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let mockTerminalFactory: TypeMoq.IMock; + + const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); + suiteSetup(initializeTest); + setup(async () => { + initializeDI(); + await initializeTest(); + await resetSettings(); + }); + suiteTeardown(async () => { + await closeActiveWindows(); + await resetSettings(); + }); + teardown(async () => { + ioc.dispose(); + await closeActiveWindows(); + }); - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerUnitTestTypes(); - ioc.registerVariableTypes(); - ioc.registerLinterTypes(); - ioc.registerFormatterTypes(); - - ioc.serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); - ioc.serviceManager.addSingleton(ILogger, Logger); - ioc.serviceManager.addSingleton(IInstaller, Installer); - - mockTerminalService = TypeMoq.Mock.ofType(); - const mockTerminalFactory = TypeMoq.Mock.ofType(); - mockTerminalFactory.setup(t => t.getTerminalService(TypeMoq.It.isAny())).returns(() => mockTerminalService.object); - ioc.serviceManager.addSingletonInstance(ITerminalServiceFactory, mockTerminalFactory.object); - - ioc.serviceManager.addSingleton(IModuleInstaller, PipInstaller); - ioc.serviceManager.addSingleton(IModuleInstaller, CondaInstaller); - ioc.serviceManager.addSingleton(IModuleInstaller, PipEnvInstaller); - condaService = TypeMoq.Mock.ofType(); - ioc.serviceManager.addSingletonInstance(ICondaService, condaService.object); - interpreterService = TypeMoq.Mock.ofType(); - ioc.serviceManager.addSingletonInstance(IInterpreterService, interpreterService.object); - ioc.serviceManager.addSingleton(IPathUtils, PathUtils); - ioc.serviceManager.addSingleton(ICurrentProcess, CurrentProcess); - ioc.serviceManager.addSingleton(IFileSystem, FileSystem); - ioc.serviceManager.addSingleton(IPlatformService, PlatformService); - ioc.serviceManager.addSingleton(IConfigurationService, ConfigurationService); - - ioc.registerMockProcessTypes(); - ioc.serviceManager.addSingletonInstance(IsWindows, false); - } - async function resetSettings(): Promise { - const configService = ioc.serviceManager.get(IConfigurationService); - await configService.updateSettingAsync('linting.pylintEnabled', true, rootWorkspaceUri, ConfigurationTarget.Workspace); - } - async function getCurrentPythonPath(): Promise { - const pythonPath = PythonSettings.getInstance(workspaceUri).pythonPath; - if (path.basename(pythonPath) === pythonPath) { - const pythonProc = await ioc.serviceContainer.get(IPythonExecutionFactory).create(workspaceUri); - return pythonProc.getExecutablePath().catch(() => pythonPath); - } else { - return pythonPath; + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerUnitTestTypes(); + ioc.registerVariableTypes(); + ioc.registerLinterTypes(); + ioc.registerFormatterTypes(); + + ioc.serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); + ioc.serviceManager.addSingleton(ILogger, Logger); + ioc.serviceManager.addSingleton(IInstaller, ProductInstaller); + + mockTerminalService = TypeMoq.Mock.ofType(); + mockTerminalFactory = TypeMoq.Mock.ofType(); + mockTerminalFactory.setup(t => t.getTerminalService(TypeMoq.It.isValue(resource))) + .returns(() => mockTerminalService.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + // If resource is provided, then ensure we do not invoke without the resource. + mockTerminalFactory.setup(t => t.getTerminalService(TypeMoq.It.isAny())) + .callback(passedInResource => expect(passedInResource).to.be.equal(resource)) + .returns(() => mockTerminalService.object); + ioc.serviceManager.addSingletonInstance(ITerminalServiceFactory, mockTerminalFactory.object); + + ioc.serviceManager.addSingleton(IModuleInstaller, PipInstaller); + ioc.serviceManager.addSingleton(IModuleInstaller, CondaInstaller); + ioc.serviceManager.addSingleton(IModuleInstaller, PipEnvInstaller); + condaService = TypeMoq.Mock.ofType(); + ioc.serviceManager.addSingletonInstance(ICondaService, condaService.object); + + interpreterService = TypeMoq.Mock.ofType(); + ioc.serviceManager.addSingletonInstance(IInterpreterService, interpreterService.object); + + ioc.serviceManager.addSingleton(IPathUtils, PathUtils); + ioc.serviceManager.addSingleton(ICurrentProcess, CurrentProcess); + ioc.serviceManager.addSingleton(IFileSystem, FileSystem); + ioc.serviceManager.addSingleton(IPlatformService, PlatformService); + ioc.serviceManager.addSingleton(IConfigurationService, ConfigurationService); + + const workspaceService = TypeMoq.Mock.ofType(); + ioc.serviceManager.addSingletonInstance(IWorkspaceService, workspaceService.object); + const http = TypeMoq.Mock.ofType(); + http.setup(h => h.get(TypeMoq.It.isValue('proxy'), TypeMoq.It.isAny())).returns(() => ''); + workspaceService.setup(w => w.getConfiguration(TypeMoq.It.isValue('http'))).returns(() => http.object); + + ioc.registerMockProcessTypes(); + ioc.serviceManager.addSingletonInstance(IsWindows, false); } - } - test('Ensure pip is supported and conda is not', async () => { - ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('mock', true)); - const mockInterpreterLocator = TypeMoq.Mock.ofType(); - mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); - ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); - ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, TypeMoq.Mock.ofType().object, PIPENV_SERVICE); - - const processService = ioc.serviceContainer.get(IProcessService); - processService.onExec((file, args, options, callback) => { - if (args.length > 1 && args[0] === '-c' && args[1] === 'import pip') { - callback({ stdout: '' }); - } - if (args.length > 0 && args[0] === '--version' && file === 'conda') { - callback({ stdout: '', stderr: 'not available' }); + async function resetSettings(): Promise { + const configService = ioc.serviceManager.get(IConfigurationService); + await configService.updateSettingAsync('linting.pylintEnabled', true, rootWorkspaceUri, ConfigurationTarget.Workspace); + } + async function getCurrentPythonPath(): Promise { + const pythonPath = PythonSettings.getInstance(workspaceUri).pythonPath; + if (path.basename(pythonPath) === pythonPath) { + const pythonProc = await ioc.serviceContainer.get(IPythonExecutionFactory).create({ resource: workspaceUri }); + return pythonProc.getExecutablePath().catch(() => pythonPath); + } else { + return pythonPath; } - }); - const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); - expect(moduleInstallers).length(4, 'Incorrect number of installers'); + } + test('Ensure pip is supported and conda is not', async () => { + ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('mock', true)); + const mockInterpreterLocator = TypeMoq.Mock.ofType(); + mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); + ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, TypeMoq.Mock.ofType().object, PIPENV_SERVICE); + + const processService = await ioc.serviceContainer.get(IProcessServiceFactory).create() as MockProcessService; + processService.onExec((file, args, options, callback) => { + if (args.length > 1 && args[0] === '-c' && args[1] === 'import pip') { + callback({ stdout: '' }); + } + if (args.length > 0 && args[0] === '--version' && file === 'conda') { + callback({ stdout: '', stderr: 'not available' }); + } + }); + const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); + expect(moduleInstallers).length(4, 'Incorrect number of installers'); - const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; - expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); - await expect(pipInstaller.isSupported()).to.eventually.equal(true, 'Pip is not supported'); + const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; + expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); + await expect(pipInstaller.isSupported()).to.eventually.equal(true, 'Pip is not supported'); - const condaInstaller = moduleInstallers.find(item => item.displayName === 'Conda')!; - expect(condaInstaller).not.to.be.an('undefined', 'Conda installer not found'); - await expect(condaInstaller.isSupported()).to.eventually.equal(false, 'Conda is supported'); + const condaInstaller = moduleInstallers.find(item => item.displayName === 'Conda')!; + expect(condaInstaller).not.to.be.an('undefined', 'Conda installer not found'); + await expect(condaInstaller.isSupported()).to.eventually.equal(false, 'Conda is supported'); - const mockInstaller = moduleInstallers.find(item => item.displayName === 'mock')!; - expect(mockInstaller).not.to.be.an('undefined', 'mock installer not found'); - await expect(mockInstaller.isSupported()).to.eventually.equal(true, 'mock is not supported'); - }); + const mockInstaller = moduleInstallers.find(item => item.displayName === 'mock')!; + expect(mockInstaller).not.to.be.an('undefined', 'mock installer not found'); + await expect(mockInstaller.isSupported()).to.eventually.equal(true, 'mock is not supported'); + }); - test('Ensure pip is supported', async () => { - ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('mock', true)); - const pythonPath = await getCurrentPythonPath(); - const mockInterpreterLocator = TypeMoq.Mock.ofType(); - mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ architecture: Architecture.Unknown, companyDisplayName: '', displayName: '', envName: '', path: pythonPath, type: InterpreterType.Conda, version: '' }])); - ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); - ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, TypeMoq.Mock.ofType().object, PIPENV_SERVICE); - - const processService = ioc.serviceContainer.get(IProcessService); - processService.onExec((file, args, options, callback) => { - if (args.length > 1 && args[0] === '-c' && args[1] === 'import pip') { - callback({ stdout: '' }); - } - if (args.length > 0 && args[0] === '--version' && file === 'conda') { - callback({ stdout: '' }); - } + test('Ensure pip is supported', async () => { + ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('mock', true)); + const pythonPath = await getCurrentPythonPath(); + const mockInterpreterLocator = TypeMoq.Mock.ofType(); + mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ ...info, architecture: Architecture.Unknown, companyDisplayName: '', displayName: '', envName: '', path: pythonPath, type: InterpreterType.Conda, version: '' }])); + ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); + ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, TypeMoq.Mock.ofType().object, PIPENV_SERVICE); + + const processService = await ioc.serviceContainer.get(IProcessServiceFactory).create() as MockProcessService; + processService.onExec((file, args, options, callback) => { + if (args.length > 1 && args[0] === '-c' && args[1] === 'import pip') { + callback({ stdout: '' }); + } + if (args.length > 0 && args[0] === '--version' && file === 'conda') { + callback({ stdout: '' }); + } + }); + const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); + expect(moduleInstallers).length(4, 'Incorrect number of installers'); + + const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; + expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); + await expect(pipInstaller.isSupported()).to.eventually.equal(true, 'Pip is not supported'); + }); + test('Ensure conda is supported', async () => { + const serviceContainer = TypeMoq.Mock.ofType(); + + const configService = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configService.object); + const settings = TypeMoq.Mock.ofType(); + const pythonPath = 'pythonABC'; + settings.setup(s => s.pythonPath).returns(() => pythonPath); + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICondaService))).returns(() => condaService.object); + condaService.setup(c => c.isCondaAvailable()).returns(() => Promise.resolve(true)); + condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + + const condaInstaller = new CondaInstaller(serviceContainer.object); + await expect(condaInstaller.isSupported()).to.eventually.equal(true, 'Conda is not supported'); + }); + test('Ensure conda is not supported even if conda is available', async () => { + const serviceContainer = TypeMoq.Mock.ofType(); + + const configService = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configService.object); + const settings = TypeMoq.Mock.ofType(); + const pythonPath = 'pythonABC'; + settings.setup(s => s.pythonPath).returns(() => pythonPath); + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICondaService))).returns(() => condaService.object); + condaService.setup(c => c.isCondaAvailable()).returns(() => Promise.resolve(true)); + condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(false)); + + const condaInstaller = new CondaInstaller(serviceContainer.object); + await expect(condaInstaller.isSupported()).to.eventually.equal(false, 'Conda should not be supported'); }); - const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); - expect(moduleInstallers).length(4, 'Incorrect number of installers'); - const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; - expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); - await expect(pipInstaller.isSupported()).to.eventually.equal(true, 'Pip is not supported'); - }); - test('Ensure conda is supported', async () => { - const serviceContainer = TypeMoq.Mock.ofType(); - - const configService = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configService.object); - const settings = TypeMoq.Mock.ofType(); - const pythonPath = 'pythonABC'; - settings.setup(s => s.pythonPath).returns(() => pythonPath); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICondaService))).returns(() => condaService.object); - condaService.setup(c => c.isCondaAvailable()).returns(() => Promise.resolve(true)); - condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); - - const condaInstaller = new CondaInstaller(serviceContainer.object); - await expect(condaInstaller.isSupported()).to.eventually.equal(true, 'Conda is not supported'); - }); + const resourceTestNameSuffix = resource ? ' with a resource' : ' without a resource'; + test(`Validate pip install arguments ${resourceTestNameSuffix}`, async () => { + const interpreterPath = await getCurrentPythonPath(); + const mockInterpreterLocator = TypeMoq.Mock.ofType(); + mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ ...info, path: interpreterPath, type: InterpreterType.Unknown }])); + ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); + ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, TypeMoq.Mock.ofType().object, PIPENV_SERVICE); - test('Validate pip install arguments', async () => { - const interpreterPath = await getCurrentPythonPath(); - const mockInterpreterLocator = TypeMoq.Mock.ofType(); - mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ path: interpreterPath, type: InterpreterType.Unknown }])); - ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); - ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, TypeMoq.Mock.ofType().object, PIPENV_SERVICE); + const interpreter: PythonInterpreter = { + ...info, + type: InterpreterType.Unknown, + path: PYTHON_PATH + }; + interpreterService.setup(x => x.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => Promise.resolve(interpreter)); - const moduleName = 'xyz'; + const moduleName = 'xyz'; - const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); - const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; + const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); + const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; - expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); + expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); - let argsSent: string[] = []; - mockTerminalService - .setup(async t => await t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) - .returns((cmd: string, args: string[]) => { argsSent = args; return Promise.resolve(void 0); }); - await pipInstaller.installModule(moduleName); + let argsSent: string[] = []; + mockTerminalService + .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) + .returns((cmd: string, args: string[]) => { argsSent = args; return Promise.resolve(void 0); }); + // tslint:disable-next-line:no-any + interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => Promise.resolve({ type: InterpreterType.Unknown } as any)); - expect(argsSent.join(' ')).equal(`-m pip install -U ${moduleName} --user`, 'Invalid command sent to terminal for installation.'); - }); + await pipInstaller.installModule(moduleName, resource); + + mockTerminalFactory.verifyAll(); + expect(argsSent.join(' ')).equal(`-m pip install -U ${moduleName} --user`, 'Invalid command sent to terminal for installation.'); + }); - test('Validate Conda install arguments', async () => { - const interpreterPath = await getCurrentPythonPath(); - const mockInterpreterLocator = TypeMoq.Mock.ofType(); - mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ path: interpreterPath, type: InterpreterType.Conda }])); - ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); - ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, TypeMoq.Mock.ofType().object, PIPENV_SERVICE); + test(`Validate Conda install arguments ${resourceTestNameSuffix}`, async () => { + const interpreterPath = await getCurrentPythonPath(); + const mockInterpreterLocator = TypeMoq.Mock.ofType(); + mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ ...info, path: interpreterPath, type: InterpreterType.Conda }])); + ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); + ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, TypeMoq.Mock.ofType().object, PIPENV_SERVICE); - const moduleName = 'xyz'; + const moduleName = 'xyz'; - const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); - const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; + const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); + const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; - expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); + expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); - let argsSent: string[] = []; - mockTerminalService - .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) - .returns((cmd: string, args: string[]) => { argsSent = args; return Promise.resolve(void 0); }); + let argsSent: string[] = []; + mockTerminalService + .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) + .returns((cmd: string, args: string[]) => { argsSent = args; return Promise.resolve(void 0); }); - await pipInstaller.installModule(moduleName); + await pipInstaller.installModule(moduleName, resource); - expect(argsSent.join(' ')).equal(`-m pip install -U ${moduleName}`, 'Invalid command sent to terminal for installation.'); - }); + mockTerminalFactory.verifyAll(); + expect(argsSent.join(' ')).equal(`-m pip install -U ${moduleName}`, 'Invalid command sent to terminal for installation.'); + }); - test('Validate pipenv install arguments', async () => { - const mockInterpreterLocator = TypeMoq.Mock.ofType(); - mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ path: 'interpreterPath', type: InterpreterType.VirtualEnv }])); - ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, PIPENV_SERVICE); - - const moduleName = 'xyz'; - const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); - const pipInstaller = moduleInstallers.find(item => item.displayName === 'pipenv')!; - - expect(pipInstaller).not.to.be.an('undefined', 'pipenv installer not found'); - - let argsSent: string[] = []; - let command: string | undefined; - mockTerminalService - .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) - .returns((cmd: string, args: string[]) => { - argsSent = args; - command = cmd; - return Promise.resolve(void 0); - }); + test(`Validate pipenv install arguments ${resourceTestNameSuffix}`, async () => { + const mockInterpreterLocator = TypeMoq.Mock.ofType(); + mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ ...info, path: 'interpreterPath', type: InterpreterType.VirtualEnv }])); + ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, PIPENV_SERVICE); - await pipInstaller.installModule(moduleName); + const moduleName = 'xyz'; + const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); + const pipInstaller = moduleInstallers.find(item => item.displayName === 'pipenv')!; - expect(command!).equal('pipenv', 'Invalid command sent to terminal for installation.'); - expect(argsSent.join(' ')).equal(`install ${moduleName}`, 'Invalid command arguments sent to terminal for installation.'); + expect(pipInstaller).not.to.be.an('undefined', 'pipenv installer not found'); + + let argsSent: string[] = []; + let command: string | undefined; + mockTerminalService + .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) + .returns((cmd: string, args: string[]) => { + argsSent = args; + command = cmd; + return Promise.resolve(void 0); + }); + + await pipInstaller.installModule(moduleName, resource); + + mockTerminalFactory.verifyAll(); + expect(command!).equal('pipenv', 'Invalid command sent to terminal for installation.'); + expect(argsSent.join(' ')).equal(`install ${moduleName} --dev`, 'Invalid command arguments sent to terminal for installation.'); + }); }); }); diff --git a/src/test/common/platform/filesystem.test.ts b/src/test/common/platform/filesystem.unit.test.ts similarity index 82% rename from src/test/common/platform/filesystem.test.ts rename to src/test/common/platform/filesystem.unit.test.ts index fa9331ce60fa..eb29e10b22b9 100644 --- a/src/test/common/platform/filesystem.test.ts +++ b/src/test/common/platform/filesystem.unit.test.ts @@ -1,12 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { expect } from 'chai'; +import { expect, use } from 'chai'; import * as fs from 'fs-extra'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { FileSystem } from '../../../client/common/platform/fileSystem'; import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +// tslint:disable-next-line:no-require-imports no-var-requires +const assertArrays = require('chai-arrays'); +use(assertArrays); // tslint:disable-next-line:max-func-body-length suite('FileSystem', () => { @@ -67,6 +70,9 @@ suite('FileSystem', () => { test('Case sensitivity is not ignored when comparing file names on linux', async () => { caseSensitivityFileCheck(false, false, true); }); + test('Check existence of files synchronously', async () => { + expect(fileSystem.fileExistsSync(__filename)).to.be.equal(true, 'file not found'); + }); test('Test appending to file', async () => { const dataToAppend = `Some Data\n${new Date().toString()}\nAnd another line`; @@ -74,4 +80,12 @@ suite('FileSystem', () => { const fileContents = await fileSystem.readFile(fileToAppendTo); expect(fileContents).to.be.equal(dataToAppend); }); + test('Test searching for files', async () => { + const files = await fileSystem.search(path.join(__dirname, '*.js')); + expect(files).to.be.array(); + expect(files.length).to.be.at.least(1); + const expectedFileName = __filename.replace(/\\/g, '/'); + const fileName = files[0].replace(/\\/g, '/'); + expect(fileName).to.equal(expectedFileName); + }); }); diff --git a/src/test/common/process/execFactory.test.ts b/src/test/common/process/execFactory.test.ts index 9a8aa3ce4322..21eea8523d86 100644 --- a/src/test/common/process/execFactory.test.ts +++ b/src/test/common/process/execFactory.test.ts @@ -1,32 +1,37 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// tslint:disable:max-func-body-length no-any + import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; import { Uri } from 'vscode'; import { IFileSystem } from '../../../client/common/platform/types'; -import { IProcessService } from '../../../client/common/process/types'; +import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; import { InterpreterVersionService } from '../../../client/interpreter/interpreterVersion'; import { IServiceContainer } from '../../../client/ioc/types'; -// tslint:disable-next-line:max-func-body-length suite('PythonExecutableService', () => { let serviceContainer: TypeMoq.IMock; let configService: TypeMoq.IMock; let procService: TypeMoq.IMock; + let procServiceFactory: TypeMoq.IMock; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); const envVarsProvider = TypeMoq.Mock.ofType(); + procServiceFactory = TypeMoq.Mock.ofType(); procService = TypeMoq.Mock.ofType(); configService = TypeMoq.Mock.ofType(); const fileSystem = TypeMoq.Mock.ofType(); - fileSystem.setup(f => f.fileExistsAsync(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + fileSystem.setup(f => f.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider))).returns(() => envVarsProvider.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessService))).returns(() => procService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessServiceFactory))).returns(() => procServiceFactory.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configService.object); + procService.setup((x: any) => x.then).returns(() => undefined); + procServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(procService.object)); envVarsProvider.setup(v => v.getEnvironmentVariables(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); }); @@ -38,7 +43,7 @@ suite('PythonExecutableService', () => { configService.setup(c => c.getSettings(TypeMoq.It.isValue(undefined))).returns(() => pythonSettings.object); procService.setup(p => p.exec(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: pythonVersion })); - const versionService = new InterpreterVersionService(procService.object); + const versionService = new InterpreterVersionService(procServiceFactory.object); const version = await versionService.getVersion(pythonPath, ''); expect(version).to.be.equal(pythonVersion); @@ -52,7 +57,7 @@ suite('PythonExecutableService', () => { configService.setup(c => c.getSettings(TypeMoq.It.isValue(resource))).returns(() => pythonSettings.object); procService.setup(p => p.exec(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: pythonVersion })); - const versionService = new InterpreterVersionService(procService.object); + const versionService = new InterpreterVersionService(procServiceFactory.object); const version = await versionService.getVersion(pythonPath, ''); expect(version).to.be.equal(pythonVersion); diff --git a/src/test/common/process/pythonProc.simple.multiroot.test.ts b/src/test/common/process/pythonProc.simple.multiroot.test.ts index 2b9a3b513ad7..b4568e4db817 100644 --- a/src/test/common/process/pythonProc.simple.multiroot.test.ts +++ b/src/test/common/process/pythonProc.simple.multiroot.test.ts @@ -4,6 +4,7 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import { execFile } from 'child_process'; +import * as fs from 'fs-extra'; import { Container } from 'inversify'; import { EOL } from 'os'; import * as path from 'path'; @@ -45,11 +46,9 @@ suite('PythonExecutableService', () => { this.skip(); } await clearPythonPathInWorkspaceFolder(workspace4Path); - - await (new ConfigurationService()).updateSettingAsync('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); await initialize(); }); - setup(() => { + setup(async () => { cont = new Container(); serviceContainer = new ServiceContainer(cont); const serviceManager = new ServiceManager(cont); @@ -69,6 +68,7 @@ suite('PythonExecutableService', () => { configService = serviceManager.get(IConfigurationService); pythonExecFactory = serviceContainer.get(IPythonExecutionFactory); + await configService.updateSettingAsync('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); return initializeTest(); }); suiteTeardown(closeActiveWindows); @@ -84,7 +84,7 @@ suite('PythonExecutableService', () => { test('Importing without a valid PYTHONPATH should fail', async () => { await configService.updateSettingAsync('envFile', 'someInvalidFile.env', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); pythonExecFactory = serviceContainer.get(IPythonExecutionFactory); - const pythonExecService = await pythonExecFactory.create(workspace4PyFile); + const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); const promise = pythonExecService.exec([workspace4PyFile.fsPath], { cwd: path.dirname(workspace4PyFile.fsPath), throwOnStdErr: true }); await expect(promise).to.eventually.be.rejectedWith(StdErrError); @@ -92,14 +92,14 @@ suite('PythonExecutableService', () => { test('Importing with a valid PYTHONPATH from .env file should succeed', async () => { await configService.updateSettingAsync('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); - const pythonExecService = await pythonExecFactory.create(workspace4PyFile); + const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); const promise = pythonExecService.exec([workspace4PyFile.fsPath], { cwd: path.dirname(workspace4PyFile.fsPath), throwOnStdErr: true }); await expect(promise).to.eventually.have.property('stdout', `Hello${EOL}`); }); test('Known modules such as \'os\' and \'sys\' should be deemed \'installed\'', async () => { - const pythonExecService = await pythonExecFactory.create(workspace4PyFile); + const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); const osModuleIsInstalled = pythonExecService.isModuleInstalled('os'); const sysModuleIsInstalled = pythonExecService.isModuleInstalled('sys'); await expect(osModuleIsInstalled).to.eventually.equal(true, 'os module is not installed'); @@ -107,7 +107,7 @@ suite('PythonExecutableService', () => { }); test('Unknown modules such as \'xyzabc123\' be deemed \'not installed\'', async () => { - const pythonExecService = await pythonExecFactory.create(workspace4PyFile); + const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); const randomModuleName = `xyz123${new Date().getSeconds()}`; const randomModuleIsInstalled = pythonExecService.isModuleInstalled(randomModuleName); await expect(randomModuleIsInstalled).to.eventually.equal(false, `Random module '${randomModuleName}' is installed`); @@ -115,12 +115,17 @@ suite('PythonExecutableService', () => { test('Ensure correct path to executable is returned', async () => { const pythonPath = PythonSettings.getInstance(workspace4Path).pythonPath; - const expectedExecutablePath = await new Promise(resolve => { - execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_error, stdout, _stdErr) => { - resolve(stdout.trim()); + let expectedExecutablePath: string; + if (await fs.pathExists(pythonPath)) { + expectedExecutablePath = pythonPath; + } else { + expectedExecutablePath = await new Promise(resolve => { + execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_error, stdout, _stdErr) => { + resolve(stdout.trim()); + }); }); - }); - const pythonExecService = await pythonExecFactory.create(workspace4PyFile); + } + const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); const executablePath = await pythonExecService.getExecutablePath(); expect(executablePath).to.equal(expectedExecutablePath, 'Executable paths are not the same'); }); diff --git a/src/test/common/terminals/activation.bash.test.ts b/src/test/common/terminals/activation.bash.test.ts index c321528140ea..a859f79cbbfc 100644 --- a/src/test/common/terminals/activation.bash.test.ts +++ b/src/test/common/terminals/activation.bash.test.ts @@ -37,6 +37,10 @@ suite('Terminal Environment Activation (bash)', () => { EnumEx.getNamesAndValues(TerminalShellType).forEach(shellType => { let isScriptFileSupported = false; switch (shellType.value) { + case TerminalShellType.zsh: + case TerminalShellType.ksh: + case TerminalShellType.wsl: + case TerminalShellType.gitbash: case TerminalShellType.bash: { isScriptFileSupported = ['activate', 'activate.sh'].indexOf(scriptFileName) >= 0; break; @@ -45,6 +49,7 @@ suite('Terminal Environment Activation (bash)', () => { isScriptFileSupported = ['activate.fish'].indexOf(scriptFileName) >= 0; break; } + case TerminalShellType.tcshell: case TerminalShellType.cshell: { isScriptFileSupported = ['activate.csh'].indexOf(scriptFileName) >= 0; break; @@ -61,7 +66,12 @@ suite('Terminal Environment Activation (bash)', () => { const supported = bash.isShellSupported(shellType.value); switch (shellType.value) { + case TerminalShellType.wsl: + case TerminalShellType.zsh: + case TerminalShellType.ksh: case TerminalShellType.bash: + case TerminalShellType.gitbash: + case TerminalShellType.tcshell: case TerminalShellType.cshell: case TerminalShellType.fish: { expect(supported).to.be.equal(true, `${shellType.name} shell not supported (it should be)`); @@ -75,7 +85,7 @@ suite('Terminal Environment Activation (bash)', () => { } const pathToScriptFile = path.join(path.dirname(pythonPath), scriptFileName); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); const command = await bash.getActivationCommands(undefined, shellType.value); if (isScriptFileSupported) { diff --git a/src/test/common/terminals/activation.commandPrompt.test.ts b/src/test/common/terminals/activation.commandPrompt.test.ts index f6c7509ce00d..8f6f743b6063 100644 --- a/src/test/common/terminals/activation.commandPrompt.test.ts +++ b/src/test/common/terminals/activation.commandPrompt.test.ts @@ -85,7 +85,7 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { const bash = new CommandPromptAndPowerShell(serviceContainer.object); const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); const commands = await bash.getActivationCommands(resource, TerminalShellType.commandPrompt); // Ensure the script file is of the following form: @@ -97,17 +97,17 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { }); test('Ensure batch files are supported by powershell (on windows)', async () => { - const bash = new CommandPromptAndPowerShell(serviceContainer.object); + const batch = new CommandPromptAndPowerShell(serviceContainer.object); platform.setup(p => p.isWindows).returns(() => true); const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); - const command = await bash.getActivationCommands(resource, TerminalShellType.powershell); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); + const command = await batch.getActivationCommands(resource, TerminalShellType.powershell); // Executing batch files from powershell requires going back to cmd, then into powershell const activationCommand = pathToScriptFile.fileToCommandArgument(); - const commands = [`& cmd /k "${activationCommand} & powershell"`]; + const commands = [`& cmd /k "${activationCommand.replace(/"/g, '""')} & powershell"`]; expect(command).to.be.deep.equal(commands, 'Invalid command'); }); @@ -116,13 +116,13 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { platform.setup(p => p.isWindows).returns(() => true); const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); const command = await bash.getActivationCommands(resource, TerminalShellType.powershellCore); // Executing batch files from powershell requires going back to cmd, then into powershell const activationCommand = pathToScriptFile.fileToCommandArgument(); - const commands = [`& cmd /k "${activationCommand} & pwsh"`]; + const commands = [`& cmd /k "${activationCommand.replace(/"/g, '""')} & pwsh"`]; expect(command).to.be.deep.equal(commands, 'Invalid command'); }); @@ -131,7 +131,7 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { platform.setup(p => p.isWindows).returns(() => false); const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); const command = await bash.getActivationCommands(resource, TerminalShellType.powershell); expect(command).to.be.equal(undefined, 'Invalid command'); @@ -142,7 +142,7 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { platform.setup(p => p.isWindows).returns(() => false); const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); const command = await bash.getActivationCommands(resource, TerminalShellType.powershellCore); expect(command).to.be.equal(undefined, 'Invalid command'); @@ -172,7 +172,7 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { platform.setup(p => p.isWindows).returns(() => true); const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.ps1'); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); const command = await bash.getActivationCommands(resource, TerminalShellType.commandPrompt); expect(command).to.be.deep.equal([], 'Invalid command (running powershell files are not supported on command prompt)'); @@ -183,7 +183,7 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { platform.setup(p => p.isWindows).returns(() => true); const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.ps1'); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); const command = await bash.getActivationCommands(resource, TerminalShellType.powershell); expect(command).to.be.deep.equal([`& ${pathToScriptFile.fileToCommandArgument()}`.trim()], 'Invalid command'); @@ -194,7 +194,7 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { platform.setup(p => p.isWindows).returns(() => true); const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.ps1'); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); const command = await bash.getActivationCommands(resource, TerminalShellType.powershellCore); expect(command).to.be.deep.equal([`& ${pathToScriptFile.fileToCommandArgument()}`.trim()], 'Invalid command'); diff --git a/src/test/common/terminals/activation.conda.test.ts b/src/test/common/terminals/activation.conda.test.ts index c9570ef54d94..671f896e3be0 100644 --- a/src/test/common/terminals/activation.conda.test.ts +++ b/src/test/common/terminals/activation.conda.test.ts @@ -1,13 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// tslint:disable:max-func-body-length no-any + import { expect } from 'chai'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { Disposable } from 'vscode'; import { EnumEx } from '../../../client/common/enumUtils'; +import '../../../client/common/extensions'; import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; -import { IProcessService } from '../../../client/common/process/types'; +import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; import { CondaActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; import { TerminalHelper } from '../../../client/common/terminal/helper'; import { ITerminalActivationCommandProvider, TerminalShellType } from '../../../client/common/terminal/types'; @@ -15,7 +18,6 @@ import { IConfigurationService, IDisposableRegistry, IPythonSettings, ITerminalS import { ICondaService } from '../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../client/ioc/types'; -// tslint:disable-next-line:max-func-body-length suite('Terminal Environment Activation conda', () => { let terminalHelper: TerminalHelper; let disposables: Disposable[] = []; @@ -25,6 +27,7 @@ suite('Terminal Environment Activation conda', () => { let pythonSettings: TypeMoq.IMock; let serviceContainer: TypeMoq.IMock; let processService: TypeMoq.IMock; + let procServiceFactory: TypeMoq.IMock; let condaService: TypeMoq.IMock; setup(() => { @@ -36,10 +39,13 @@ suite('Terminal Environment Activation conda', () => { platformService = TypeMoq.Mock.ofType(); processService = TypeMoq.Mock.ofType(); condaService = TypeMoq.Mock.ofType(); + processService.setup((x: any) => x.then).returns(() => undefined); + procServiceFactory = TypeMoq.Mock.ofType(); + procServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())).returns(() => platformService.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())).returns(() => fileSystem.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessService), TypeMoq.It.isAny())).returns(() => processService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())).returns(() => procServiceFactory.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICondaService), TypeMoq.It.isAny())).returns(() => condaService.object); const configService = TypeMoq.Mock.ofType(); @@ -67,50 +73,69 @@ suite('Terminal Environment Activation conda', () => { expect(activationCommands).to.equal(undefined, 'Activation commands should be undefined'); }); - async function expectNoCondaActivationCommandForPowershell(isWindows: boolean, isOsx: boolean, isLinux: boolean, pythonPath: string, shellType: TerminalShellType) { + async function expectNoCondaActivationCommandForPowershell(isWindows: boolean, isOsx: boolean, isLinux: boolean, pythonPath: string, shellType: TerminalShellType, hasSpaceInEnvironmentName = false) { terminalSettings.setup(t => t.activateEnvironment).returns(() => true); platformService.setup(p => p.isLinux).returns(() => isLinux); platformService.setup(p => p.isWindows).returns(() => isWindows); platformService.setup(p => p.isMac).returns(() => isOsx); condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); pythonSettings.setup(s => s.pythonPath).returns(() => pythonPath); - condaService.setup(c => c.getCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve({ name: 'EnvA', path: path.dirname(pythonPath) })); + const envName = hasSpaceInEnvironmentName ? 'EnvA' : 'Env A'; + condaService.setup(c => c.getCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve({ name: envName, path: path.dirname(pythonPath) })); const activationCommands = await new CondaActivationCommandProvider(serviceContainer.object).getActivationCommands(undefined, shellType); let expectedActivationCommamnd: string[] | undefined; switch (shellType) { case TerminalShellType.powershell: case TerminalShellType.powershellCore: { - expectedActivationCommamnd = undefined; + const powershellExe = shellType === TerminalShellType.powershell ? 'powershell' : 'pwsh'; + const envNameForCmd = envName.toCommandArgument().replace(/"/g, '""'); + expectedActivationCommamnd = isWindows ? [`& cmd /k \"activate ${envNameForCmd} & ${powershellExe}\"`] : undefined; break; } case TerminalShellType.fish: { - expectedActivationCommamnd = ['conda activate EnvA']; + expectedActivationCommamnd = [`conda activate ${envName.toCommandArgument()}`]; break; } default: { - expectedActivationCommamnd = isWindows ? ['activate EnvA'] : ['source activate EnvA']; + expectedActivationCommamnd = isWindows ? [`activate ${envName.toCommandArgument()}`] : [`source activate ${envName.toCommandArgument()}`]; break; } } expect(activationCommands).to.deep.equal(expectedActivationCommamnd, 'Incorrect Activation command'); } - EnumEx.getNamesAndValues(TerminalShellType).forEach(shellType => { + EnumEx.getNamesAndValues(TerminalShellType).forEach(shellType => { test(`Conda activation command for shell ${shellType.name} on (windows)`, async () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); await expectNoCondaActivationCommandForPowershell(true, false, false, pythonPath, shellType.value); }); - test(`Conda activation command for shell ${shellType.name} on (windows)`, async () => { + test(`Conda activation command for shell ${shellType.name} on (linux)`, async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); await expectNoCondaActivationCommandForPowershell(false, false, true, pythonPath, shellType.value); }); - test(`Conda activation command for shell ${shellType.name} on (linux)`, async () => { + test(`Conda activation command for shell ${shellType.name} on (mac)`, async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); await expectNoCondaActivationCommandForPowershell(false, true, false, pythonPath, shellType.value); }); }); + EnumEx.getNamesAndValues(TerminalShellType).forEach(shellType => { + test(`Conda activation command for shell ${shellType.name} on (windows), containing spaces in environment name`, async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); + await expectNoCondaActivationCommandForPowershell(true, false, false, pythonPath, shellType.value, true); + }); + + test(`Conda activation command for shell ${shellType.name} on (linux), containing spaces in environment name`, async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); + await expectNoCondaActivationCommandForPowershell(false, false, true, pythonPath, shellType.value, true); + }); + + test(`Conda activation command for shell ${shellType.name} on (mac), containing spaces in environment name`, async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); + await expectNoCondaActivationCommandForPowershell(false, true, false, pythonPath, shellType.value, true); + }); + }); async function expectCondaActivationCommand(isWindows: boolean, isOsx: boolean, isLinux: boolean, pythonPath: string) { terminalSettings.setup(t => t.activateEnvironment).returns(() => true); platformService.setup(p => p.isLinux).returns(() => isLinux); @@ -127,19 +152,19 @@ suite('Terminal Environment Activation conda', () => { test('If environment is a conda environment, ensure conda activation command is sent (windows)', async () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); await expectCondaActivationCommand(true, false, false, pythonPath); }); test('If environment is a conda environment, ensure conda activation command is sent (linux)', async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); await expectCondaActivationCommand(false, false, true, pythonPath); }); test('If environment is a conda environment, ensure conda activation command is sent (osx)', async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); await expectCondaActivationCommand(false, true, false, pythonPath); }); @@ -180,21 +205,21 @@ suite('Terminal Environment Activation conda', () => { test('If environment is a conda environment and environment detection fails, ensure activatino of script is sent (windows)', async () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); await expectActivationCommandIfCondaDetectionFails(true, false, false, pythonPath, condaEnvDir); }); test('If environment is a conda environment and environment detection fails, ensure activatino of script is sent (osx)', async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'python'); const condaEnvDir = path.join('users', 'xyz', '.conda', 'envs'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); await expectActivationCommandIfCondaDetectionFails(false, true, false, pythonPath, condaEnvDir); }); test('If environment is a conda environment and environment detection fails, ensure activatino of script is sent (linux)', async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'python'); const condaEnvDir = path.join('users', 'xyz', '.conda', 'envs'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); await expectActivationCommandIfCondaDetectionFails(false, false, true, pythonPath, condaEnvDir); }); diff --git a/src/test/common/terminals/factory.test.ts b/src/test/common/terminals/factory.test.ts index 96518c7ccfa3..232beb41246b 100644 --- a/src/test/common/terminals/factory.test.ts +++ b/src/test/common/terminals/factory.test.ts @@ -11,14 +11,12 @@ import { ITerminalHelper, ITerminalServiceFactory } from '../../../client/common import { IDisposableRegistry } from '../../../client/common/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../client/ioc/types'; -import { initialize } from '../../initialize'; // tslint:disable-next-line:max-func-body-length suite('Terminal Service Factory', () => { let factory: ITerminalServiceFactory; let disposables: Disposable[] = []; let workspaceService: TypeMoq.IMock; - suiteSetup(initialize); setup(() => { const serviceContainer = TypeMoq.Mock.ofType(); const interpreterService = TypeMoq.Mock.ofType(); diff --git a/src/test/common/terminals/helper.test.ts b/src/test/common/terminals/helper.test.ts index e692f480a6ca..2008ca545a78 100644 --- a/src/test/common/terminals/helper.test.ts +++ b/src/test/common/terminals/helper.test.ts @@ -48,11 +48,11 @@ suite('Terminal Service helpers', () => { shellPathsAndIdentification.set('c:\\windows\\system32\\cmd.exe', TerminalShellType.commandPrompt); shellPathsAndIdentification.set('c:\\windows\\system32\\bash.exe', TerminalShellType.bash); - shellPathsAndIdentification.set('c:\\windows\\system32\\wsl.exe', TerminalShellType.bash); - shellPathsAndIdentification.set('c:\\windows\\system32\\gitbash.exe', TerminalShellType.bash); + shellPathsAndIdentification.set('c:\\windows\\system32\\wsl.exe', TerminalShellType.wsl); + shellPathsAndIdentification.set('c:\\windows\\system32\\gitbash.exe', TerminalShellType.gitbash); shellPathsAndIdentification.set('/usr/bin/bash', TerminalShellType.bash); - shellPathsAndIdentification.set('/usr/bin/zsh', TerminalShellType.bash); - shellPathsAndIdentification.set('/usr/bin/ksh', TerminalShellType.bash); + shellPathsAndIdentification.set('/usr/bin/zsh', TerminalShellType.zsh); + shellPathsAndIdentification.set('/usr/bin/ksh', TerminalShellType.ksh); shellPathsAndIdentification.set('c:\\windows\\system32\\powershell.exe', TerminalShellType.powershell); shellPathsAndIdentification.set('c:\\windows\\system32\\pwsh.exe', TerminalShellType.powershellCore); @@ -65,6 +65,7 @@ suite('Terminal Service helpers', () => { shellPathsAndIdentification.set('/usr/bin/shell', TerminalShellType.other); shellPathsAndIdentification.set('/usr/bin/csh', TerminalShellType.cshell); + shellPathsAndIdentification.set('/usr/bin/tcsh', TerminalShellType.tcshell); shellPathsAndIdentification.forEach((shellType, shellPath) => { expect(helper.identifyTerminalShell(shellPath)).to.equal(shellType, `Incorrect Shell Type for path '${shellPath}'`); diff --git a/src/test/common/terminals/service.test.ts b/src/test/common/terminals/service.test.ts index f18b3738b7af..ea05cd81711e 100644 --- a/src/test/common/terminals/service.test.ts +++ b/src/test/common/terminals/service.test.ts @@ -10,7 +10,6 @@ import { TerminalService } from '../../../client/common/terminal/service'; import { ITerminalHelper, TerminalShellType } from '../../../client/common/terminal/types'; import { IDisposableRegistry } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; -import { initialize } from '../../initialize'; // tslint:disable-next-line:max-func-body-length suite('Terminal Service', () => { @@ -22,7 +21,6 @@ suite('Terminal Service', () => { let workspaceService: TypeMoq.IMock; let disposables: Disposable[] = []; let mockServiceContainer: TypeMoq.IMock; - suiteSetup(initialize); setup(() => { terminal = TypeMoq.Mock.ofType(); terminalManager = TypeMoq.Mock.ofType(); @@ -114,6 +112,18 @@ suite('Terminal Service', () => { terminal.verify(t => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(2)); }); + test('Ensure terminal shown and focus is set to the Terminal', async () => { + terminalHelper.setup(helper => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); + terminalHelper.setup(h => h.getTerminalShellPath()).returns(() => ''); + terminalHelper.setup(h => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.show(false); + + terminal.verify(t => t.show(TypeMoq.It.isValue(false)), TypeMoq.Times.exactly(2)); + }); + test('Ensure terminal is activated once after creation', async () => { service = new TerminalService(mockServiceContainer.object); terminalHelper.setup(h => h.getTerminalShellPath()).returns(() => ''); diff --git a/src/test/configuration/interpreterSelector.test.ts b/src/test/configuration/interpreterSelector.test.ts index b53dd61c79d2..9d222f5e1df6 100644 --- a/src/test/configuration/interpreterSelector.test.ts +++ b/src/test/configuration/interpreterSelector.test.ts @@ -5,17 +5,30 @@ import * as assert from 'assert'; import { Container } from 'inversify'; import * as TypeMoq from 'typemoq'; import { IApplicationShell, ICommandManager, IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; -import { IFileSystem } from '../../client/common/platform/types'; +import { Architecture, IFileSystem } from '../../client/common/platform/types'; import { IInterpreterQuickPickItem, InterpreterSelector } from '../../client/interpreter/configuration/interpreterSelector'; import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceContainer } from '../../client/ioc/types'; +const info: PythonInterpreter = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + type: InterpreterType.Unknown, + version: '', + version_info: [0, 0, 0, 'alpha'], + sysPrefix: '', + sysVersion: '' +}; + class InterpreterQuickPickItem implements IInterpreterQuickPickItem { public path: string; public label: string; - public description: string; + public description!: string; public detail?: string; constructor(l: string, p: string) { this.path = p; @@ -24,7 +37,7 @@ class InterpreterQuickPickItem implements IInterpreterQuickPickItem { } // tslint:disable-next-line:max-func-body-length -suite('Intepreters - selector', () => { +suite('Interpreters - selector', () => { let serviceContainer: IServiceContainer; let workspace: TypeMoq.IMock; let appShell: TypeMoq.IMock; @@ -54,7 +67,7 @@ suite('Intepreters - selector', () => { .setup(x => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) .returns((a: string, b: string) => a === b); fileSystem - .setup(x => x.getRealPathAsync(TypeMoq.It.isAnyString())) + .setup(x => x.getRealPath(TypeMoq.It.isAnyString())) .returns((a: string) => new Promise(resolve => resolve(a))); serviceManager.addSingletonInstance(IFileSystem, fileSystem.object); @@ -65,15 +78,15 @@ suite('Intepreters - selector', () => { test('Suggestions', async () => { const initial: PythonInterpreter[] = [ - { displayName: '1', path: 'path1', type: InterpreterType.Unknown }, - { displayName: '2', path: 'path1', type: InterpreterType.Unknown }, - { displayName: '1', path: 'path1', type: InterpreterType.Unknown }, - { displayName: '2', path: 'path2', type: InterpreterType.Unknown }, - { displayName: '2', path: 'path2', type: InterpreterType.Unknown }, - { displayName: '2 (virtualenv)', path: 'path2', type: InterpreterType.VirtualEnv }, - { displayName: '3', path: 'path2', type: InterpreterType.Unknown }, - { displayName: '4', path: 'path4', type: InterpreterType.Conda } - ]; + { displayName: '1', path: 'c:/path1/path1', type: InterpreterType.Unknown }, + { displayName: '2', path: 'c:/path1/path1', type: InterpreterType.Unknown }, + { displayName: '1', path: 'c:/path1/path1', type: InterpreterType.Unknown }, + { displayName: '2', path: 'c:/path2/path2', type: InterpreterType.Unknown }, + { displayName: '2', path: 'c:/path2/path2', type: InterpreterType.Unknown }, + { displayName: '2 (virtualenv)', path: 'c:/path2/path2', type: InterpreterType.VirtualEnv }, + { displayName: '3', path: 'c:/path2/path2', type: InterpreterType.Unknown }, + { displayName: '4', path: 'c:/path4/path4', type: InterpreterType.Conda } + ].map(item => { return { ...info, ...item }; }); interpreterService .setup(x => x.getInterpreters(TypeMoq.It.isAny())) .returns(() => new Promise((resolve) => resolve(initial))); @@ -82,12 +95,12 @@ suite('Intepreters - selector', () => { const actual = await selector.getSuggestions(); const expected: InterpreterQuickPickItem[] = [ - new InterpreterQuickPickItem('1', 'path1'), - new InterpreterQuickPickItem('2', 'path1'), - new InterpreterQuickPickItem('2', 'path2'), - new InterpreterQuickPickItem('2 (virtualenv)', 'path2'), - new InterpreterQuickPickItem('3', 'path2'), - new InterpreterQuickPickItem('4', 'path4') + new InterpreterQuickPickItem('1', 'c:/path1/path1'), + new InterpreterQuickPickItem('2', 'c:/path1/path1'), + new InterpreterQuickPickItem('2', 'c:/path2/path2'), + new InterpreterQuickPickItem('2 (virtualenv)', 'c:/path2/path2'), + new InterpreterQuickPickItem('3', 'c:/path2/path2'), + new InterpreterQuickPickItem('4', 'c:/path4/path4') ]; assert.equal(actual.length, expected.length, 'Suggestion lengths are different.'); diff --git a/src/test/constants.ts b/src/test/constants.ts index 1b468a7d9d57..268352e7f20a 100644 --- a/src/test/constants.ts +++ b/src/test/constants.ts @@ -1,17 +1,26 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// tslint:disable:no-string-literal -import { workspace } from 'vscode'; - -export const IS_APPVEYOR = process.env['APPVEYOR'] === 'true'; -export const IS_CI_SERVER = process.env['TRAVIS'] === 'true' || IS_APPVEYOR; -export const TEST_TIMEOUT = 25000; -export const IS_MULTI_ROOT_TEST = isMultitrootTest(); -export const IS_CI_SERVER_TEST_DEBUGGER = process.env['IS_CI_SERVER_TEST_DEBUGGER'] === '1'; -// If running on CI server, then run debugger tests ONLY if the corresponding flag is enabled. -export const TEST_DEBUGGER = IS_CI_SERVER ? IS_CI_SERVER_TEST_DEBUGGER : true; - -function isMultitrootTest() { - return Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 1; -} +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { workspace } from 'vscode'; +import { PythonSettings } from '../client/common/configSettings'; +// import { IS_APPVEYOR, IS_CI_SERVER, IS_CI_SERVER_TEST_DEBUGGER, +// IS_TRAVIS, IS_VSTS, MOCHA_CI_PROPERTIES, MOCHA_CI_REPORTFILE, +// MOCHA_REPORTER_JUNIT } from './ciConstants'; +import { IS_CI_SERVER, IS_CI_SERVER_TEST_DEBUGGER, IS_TRAVIS } from './ciConstants'; + +export const TEST_TIMEOUT = 25000; +export const IS_MULTI_ROOT_TEST = isMultitrootTest(); + +// If running on CI server, then run debugger tests ONLY if the corresponding flag is enabled. +export const TEST_DEBUGGER = IS_CI_SERVER ? IS_CI_SERVER_TEST_DEBUGGER : true; + +// export { IS_APPVEYOR, IS_CI_SERVER, IS_CI_SERVER_TEST_DEBUGGER, +// IS_TRAVIS, IS_VSTS, MOCHA_CI_PROPERTIES, MOCHA_CI_REPORTFILE, +// MOCHA_REPORTER_JUNIT }; + +function isMultitrootTest() { + return Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 1; +} + +export const IsLanguageServerTest = () => + !IS_TRAVIS && (process.env.VSC_PYTHON_LANGUAGE_SERVER === '1' || !PythonSettings.getInstance().jediEnabled); diff --git a/src/test/core.ts b/src/test/core.ts new file mode 100644 index 000000000000..3e67d5543829 --- /dev/null +++ b/src/test/core.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// File without any dependencies on VS Code. + +export async function sleep(milliseconds: number) { + return new Promise(resolve => setTimeout(resolve, milliseconds)); +} diff --git a/src/test/debugger/attach.ptvsd.test.ts b/src/test/debugger/attach.ptvsd.test.ts new file mode 100644 index 000000000000..ddce60e21ab0 --- /dev/null +++ b/src/test/debugger/attach.ptvsd.test.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-invalid-this max-func-body-length no-empty no-increment-decrement + +import { ChildProcess, spawn } from 'child_process'; +import * as getFreePort from 'get-port'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { DebugConfiguration, Uri } from 'vscode'; +import { DebugClient } from 'vscode-debugadapter-testsupport'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import '../../client/common/extensions'; +import { IS_WINDOWS } from '../../client/common/platform/constants'; +import { IPlatformService } from '../../client/common/platform/types'; +import { PythonV2DebugConfigurationProvider } from '../../client/debugger'; +import { PTVSD_PATH } from '../../client/debugger/Common/constants'; +import { AttachRequestArguments, DebugOptions } from '../../client/debugger/Common/Contracts'; +import { IServiceContainer } from '../../client/ioc/types'; +import { PYTHON_PATH, sleep } from '../common'; +import { initialize, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; +import { continueDebugging, createDebugAdapter } from './utils'; + +const fileToDebug = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'remoteDebugger-start-with-ptvsd.py'); + +suite('Attach Debugger - Experimental', () => { + let debugClient: DebugClient; + let proc: ChildProcess; + suiteSetup(initialize); + + setup(async function () { + if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { + this.skip(); + } + this.timeout(30000); + const coverageDirectory = path.join(EXTENSION_ROOT_DIR, 'debug_coverage_attach_ptvsd'); + debugClient = await createDebugAdapter(coverageDirectory); + }); + teardown(async () => { + // Wait for a second before starting another test (sometimes, sockets take a while to get closed). + await sleep(1000); + try { + await debugClient.stop().catch(() => { }); + } catch (ex) { } + if (proc) { + try { + proc.kill(); + } catch { } + } + }); + async function testAttachingToRemoteProcess(localRoot: string, remoteRoot: string, isLocalHostWindows: boolean) { + const localHostPathSeparator = isLocalHostWindows ? '\\' : '/'; + const port = await getFreePort({ host: 'localhost', port: 3000 }); + const env = { ...process.env }; + + // Set the path for PTVSD to be picked up. + // tslint:disable-next-line:no-string-literal + env['PYTHONPATH'] = PTVSD_PATH; + const pythonArgs = ['-m', 'ptvsd', '--server', '--port', `${port}`, '--file', fileToDebug.fileToCommandArgument()]; + proc = spawn(PYTHON_PATH, pythonArgs, { env: env, cwd: path.dirname(fileToDebug) }); + await sleep(3000); + + // Send initialize, attach + const initializePromise = debugClient.initializeRequest({ + adapterID: 'pythonExperimental', + linesStartAt1: true, + columnsStartAt1: true, + supportsRunInTerminalRequest: true, + pathFormat: 'path', + supportsVariableType: true, + supportsVariablePaging: true + }); + const options: AttachRequestArguments & DebugConfiguration = { + name: 'attach', + request: 'attach', + localRoot, + remoteRoot, + type: 'pythonExperimental', + port: port, + host: 'localhost', + logToFile: false, + debugOptions: [DebugOptions.RedirectOutput] + }; + const platformService = TypeMoq.Mock.ofType(); + platformService.setup(p => p.isWindows).returns(() => isLocalHostWindows); + const serviceContainer = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(IPlatformService, TypeMoq.It.isAny())).returns(() => platformService.object); + const configProvider = new PythonV2DebugConfigurationProvider(serviceContainer.object); + + await configProvider.resolveDebugConfiguration({ index: 0, name: 'root', uri: Uri.file(localRoot) }, options); + const attachPromise = debugClient.attachRequest(options); + + await Promise.all([ + initializePromise, + attachPromise, + debugClient.waitForEvent('initialized') + ]); + + const stdOutPromise = debugClient.assertOutput('stdout', 'this is stdout'); + const stdErrPromise = debugClient.assertOutput('stderr', 'this is stderr'); + + // Don't use path utils, as we're building the paths manually (mimic windows paths on unix test servers and vice versa). + const localFileName = `${localRoot}${localHostPathSeparator}${path.basename(fileToDebug)}`; + const breakpointLocation = { path: localFileName, column: 1, line: 12 }; + const breakpointPromise = debugClient.setBreakpointsRequest({ + lines: [breakpointLocation.line], + breakpoints: [{ line: breakpointLocation.line, column: breakpointLocation.column }], + source: { path: breakpointLocation.path } + }); + const exceptionBreakpointPromise = debugClient.setExceptionBreakpointsRequest({ filters: [] }); + const breakpointStoppedPromise = debugClient.assertStoppedLocation('breakpoint', breakpointLocation); + await Promise.all([ + breakpointPromise, exceptionBreakpointPromise, + debugClient.configurationDoneRequest(), debugClient.threadsRequest(), + stdOutPromise, stdErrPromise, + breakpointStoppedPromise + ]); + + await Promise.all([ + continueDebugging(debugClient), + debugClient.assertOutput('stdout', 'this is print'), + debugClient.waitForEvent('exited'), + debugClient.waitForEvent('terminated') + ]); + } + test('Confirm we are able to attach to a running program', async () => { + await testAttachingToRemoteProcess(path.dirname(fileToDebug), path.dirname(fileToDebug), IS_WINDOWS); + }); + test('Confirm local and remote paths are translated', async () => { + // If tests are running on windows, then treat debug client as a unix client and remote process as current OS. + const isLocalHostWindows = !IS_WINDOWS; + const localWorkspace = isLocalHostWindows ? 'C:\\Project\\src' : '/home/user/Desktop/project/src'; + await testAttachingToRemoteProcess(localWorkspace, path.dirname(fileToDebug), isLocalHostWindows); + }); +}); diff --git a/src/test/debugger/attach.test.ts b/src/test/debugger/attach.test.ts index ba393f911a71..c03e0cdf3573 100644 --- a/src/test/debugger/attach.test.ts +++ b/src/test/debugger/attach.test.ts @@ -11,9 +11,10 @@ import { DebugClient } from 'vscode-debugadapter-testsupport'; import { createDeferred } from '../../client/common/helpers'; import { BufferDecoder } from '../../client/common/process/decoder'; import { ProcessService } from '../../client/common/process/proc'; -import { AttachRequestArguments } from '../../client/debugger/Common/Contracts'; -import { sleep } from '../common'; +import { AttachRequestArgumentsV1 } from '../../client/debugger/Common/Contracts'; +import { PYTHON_PATH, sleep } from '../common'; import { initialize, IS_APPVEYOR, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; +import { DEBUGGER_TIMEOUT } from './common/constants'; const fileToDebug = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc', 'workspace5', 'remoteDebugger.py'); const ptvsdPath = path.join(__dirname, '..', '..', '..', 'pythonFiles', 'PythonTools'); @@ -30,6 +31,7 @@ suite('Attach Debugger', () => { } await sleep(1000); debugClient = new DebugClient('node', DEBUG_ADAPTER, 'python'); + debugClient.defaultTimeout = DEBUGGER_TIMEOUT; await debugClient.start(); }); teardown(async () => { @@ -51,7 +53,7 @@ suite('Attach Debugger', () => { } const port = await getFreePort({ host: 'localhost', port: 3000 }); - const args: AttachRequestArguments = { + const args: AttachRequestArgumentsV1 = { localRoot: path.dirname(fileToDebug), remoteRoot: path.dirname(fileToDebug), port: port, @@ -65,7 +67,7 @@ suite('Attach Debugger', () => { // tslint:disable-next-line:no-string-literal customEnv['PYTHONPATH'] = ptvsdPath; const procService = new ProcessService(new BufferDecoder()); - const result = procService.execObservable('python', [fileToDebug, port.toString()], { env: customEnv, cwd: path.dirname(fileToDebug) }); + const result = procService.execObservable(PYTHON_PATH, [fileToDebug, port.toString()], { env: customEnv, cwd: path.dirname(fileToDebug) }); procToKill = result.proc; const expectedOutputs = [ diff --git a/src/test/debugger/banner.unit.test.ts b/src/test/debugger/banner.unit.test.ts new file mode 100644 index 000000000000..59ac609c5c02 --- /dev/null +++ b/src/test/debugger/banner.unit.test.ts @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { DebugSession } from 'vscode'; +import { IApplicationShell, IDebugService } from '../../client/common/application/types'; +import { IBrowserService, IDisposableRegistry, IExperimentalDebuggerBanner, + ILogger, IPersistentState, IPersistentStateFactory } from '../../client/common/types'; +import { ExperimentalDebuggerBanner, PersistentStateKeys } from '../../client/debugger/banner'; +import { ExperimentalDebuggerType } from '../../client/debugger/Common/constants'; +import { IServiceContainer } from '../../client/ioc/types'; + +suite('Debugging - Banner', () => { + let serviceContainer: typemoq.IMock; + let browser: typemoq.IMock; + let launchCounterState: typemoq.IMock>; + let launchThresholdCounterState: typemoq.IMock>; + let showBannerState: typemoq.IMock>; + let debugService: typemoq.IMock; + let appShell: typemoq.IMock; + let banner: IExperimentalDebuggerBanner; + const message = 'Can you please take 2 minutes to tell us how the Experimental Debugger is working for you?'; + const yes = 'Yes, take survey now'; + const no = 'No thanks'; + + setup(() => { + serviceContainer = typemoq.Mock.ofType(); + browser = typemoq.Mock.ofType(); + debugService = typemoq.Mock.ofType(); + const logger = typemoq.Mock.ofType(); + + launchCounterState = typemoq.Mock.ofType>(); + showBannerState = typemoq.Mock.ofType>(); + appShell = typemoq.Mock.ofType(); + launchThresholdCounterState = typemoq.Mock.ofType>(); + const factory = typemoq.Mock.ofType(); + factory + .setup(f => f.createGlobalPersistentState(typemoq.It.isValue(PersistentStateKeys.DebuggerLaunchCounter), typemoq.It.isAny())) + .returns(() => launchCounterState.object); + factory + .setup(f => f.createGlobalPersistentState(typemoq.It.isValue(PersistentStateKeys.ShowBanner), typemoq.It.isAny())) + .returns(() => showBannerState.object); + factory + .setup(f => f.createGlobalPersistentState(typemoq.It.isValue(PersistentStateKeys.DebuggerLaunchThresholdCounter), typemoq.It.isAny())) + .returns(() => launchThresholdCounterState.object); + + serviceContainer.setup(s => s.get(typemoq.It.isValue(IBrowserService))).returns(() => browser.object); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IPersistentStateFactory))).returns(() => factory.object); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IDebugService))).returns(() => debugService.object); + serviceContainer.setup(s => s.get(typemoq.It.isValue(ILogger))).returns(() => logger.object); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IDisposableRegistry))).returns(() => []); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + + banner = new ExperimentalDebuggerBanner(serviceContainer.object); + }); + test('Browser is displayed when launching service along with debugger launch counter', async () => { + const debuggerLaunchCounter = 1234; + launchCounterState.setup(l => l.value).returns(() => debuggerLaunchCounter).verifiable(typemoq.Times.once()); + browser.setup(b => b.launch(typemoq.It.isValue(`https://www.research.net/r/N7B25RV?n=${debuggerLaunchCounter}`))) + .verifiable(typemoq.Times.once()); + + await banner.launchSurvey(); + + launchCounterState.verifyAll(); + browser.verifyAll(); + }); + test('Increment Debugger Launch Counter when debug session starts', async () => { + let onDidTerminateDebugSessionCb: (e: DebugSession) => Promise; + debugService.setup(d => d.onDidTerminateDebugSession(typemoq.It.isAny())) + .callback(cb => onDidTerminateDebugSessionCb = cb) + .verifiable(typemoq.Times.once()); + + const debuggerLaunchCounter = 1234; + launchCounterState.setup(l => l.value).returns(() => debuggerLaunchCounter) + .verifiable(typemoq.Times.atLeastOnce()); + launchCounterState.setup(l => l.updateValue(typemoq.It.isValue(debuggerLaunchCounter + 1))) + .verifiable(typemoq.Times.once()); + showBannerState.setup(s => s.value).returns(() => true) + .verifiable(typemoq.Times.atLeastOnce()); + + banner.initialize(); + await onDidTerminateDebugSessionCb!({ type: ExperimentalDebuggerType } as any); + + launchCounterState.verifyAll(); + browser.verifyAll(); + debugService.verifyAll(); + showBannerState.verifyAll(); + }); + test('Do not Increment Debugger Launch Counter when debug session starts and Banner is disabled', async () => { + debugService.setup(d => d.onDidTerminateDebugSession(typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + const debuggerLaunchCounter = 1234; + launchCounterState.setup(l => l.value).returns(() => debuggerLaunchCounter) + .verifiable(typemoq.Times.never()); + launchCounterState.setup(l => l.updateValue(typemoq.It.isValue(debuggerLaunchCounter + 1))) + .verifiable(typemoq.Times.never()); + showBannerState.setup(s => s.value).returns(() => false) + .verifiable(typemoq.Times.atLeastOnce()); + + banner.initialize(); + + launchCounterState.verifyAll(); + browser.verifyAll(); + debugService.verifyAll(); + showBannerState.verifyAll(); + }); + test('shouldShowBanner must return false when Banner is disabled', async () => { + showBannerState.setup(s => s.value).returns(() => false) + .verifiable(typemoq.Times.once()); + + expect(await banner.shouldShowBanner()).to.be.equal(false, 'Incorrect value'); + + showBannerState.verifyAll(); + }); + test('shouldShowBanner must return false when Banner is enabled and debug counter is not same as threshold', async () => { + showBannerState.setup(s => s.value).returns(() => true) + .verifiable(typemoq.Times.once()); + launchCounterState.setup(l => l.value).returns(() => 1) + .verifiable(typemoq.Times.once()); + launchThresholdCounterState.setup(t => t.value).returns(() => 10) + .verifiable(typemoq.Times.atLeastOnce()); + + expect(await banner.shouldShowBanner()).to.be.equal(false, 'Incorrect value'); + + showBannerState.verifyAll(); + launchCounterState.verifyAll(); + launchThresholdCounterState.verifyAll(); + }); + test('shouldShowBanner must return true when Banner is enabled and debug counter is same as threshold', async () => { + showBannerState.setup(s => s.value).returns(() => true) + .verifiable(typemoq.Times.once()); + launchCounterState.setup(l => l.value).returns(() => 10) + .verifiable(typemoq.Times.once()); + launchThresholdCounterState.setup(t => t.value).returns(() => 10) + .verifiable(typemoq.Times.atLeastOnce()); + + expect(await banner.shouldShowBanner()).to.be.equal(true, 'Incorrect value'); + + showBannerState.verifyAll(); + launchCounterState.verifyAll(); + launchThresholdCounterState.verifyAll(); + }); + test('showBanner must be invoked when shouldShowBanner returns true', async () => { + let onDidTerminateDebugSessionCb: (e: DebugSession) => Promise; + const currentLaunchCounter = 50; + + debugService.setup(d => d.onDidTerminateDebugSession(typemoq.It.isAny())) + .callback(cb => onDidTerminateDebugSessionCb = cb) + .verifiable(typemoq.Times.atLeastOnce()); + showBannerState.setup(s => s.value).returns(() => true) + .verifiable(typemoq.Times.atLeastOnce()); + launchCounterState.setup(l => l.value).returns(() => currentLaunchCounter) + .verifiable(typemoq.Times.atLeastOnce()); + launchThresholdCounterState.setup(t => t.value).returns(() => 10) + .verifiable(typemoq.Times.atLeastOnce()); + launchCounterState.setup(l => l.updateValue(typemoq.It.isValue(currentLaunchCounter + 1))) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.atLeastOnce()); + + appShell.setup(a => a.showInformationMessage(typemoq.It.isValue(message), typemoq.It.isValue(yes), typemoq.It.isValue(no))) + .verifiable(typemoq.Times.once()); + banner.initialize(); + await onDidTerminateDebugSessionCb!({ type: ExperimentalDebuggerType } as any); + + appShell.verifyAll(); + showBannerState.verifyAll(); + launchCounterState.verifyAll(); + launchThresholdCounterState.verifyAll(); + }); + test('showBanner must not be invoked the second time after dismissing the message', async () => { + let onDidTerminateDebugSessionCb: (e: DebugSession) => Promise; + let currentLaunchCounter = 50; + + debugService.setup(d => d.onDidTerminateDebugSession(typemoq.It.isAny())) + .callback(cb => onDidTerminateDebugSessionCb = cb) + .verifiable(typemoq.Times.atLeastOnce()); + showBannerState.setup(s => s.value).returns(() => true) + .verifiable(typemoq.Times.atLeastOnce()); + launchCounterState.setup(l => l.value).returns(() => currentLaunchCounter) + .verifiable(typemoq.Times.atLeastOnce()); + launchThresholdCounterState.setup(t => t.value).returns(() => 10) + .verifiable(typemoq.Times.atLeastOnce()); + launchCounterState.setup(l => l.updateValue(typemoq.It.isAny())) + .callback(() => currentLaunchCounter = currentLaunchCounter + 1); + + appShell.setup(a => a.showInformationMessage(typemoq.It.isValue(message), typemoq.It.isValue(yes), typemoq.It.isValue(no))) + .returns(() => Promise.resolve(undefined)) + .verifiable(typemoq.Times.once()); + banner.initialize(); + await onDidTerminateDebugSessionCb!({ type: ExperimentalDebuggerType } as any); + await onDidTerminateDebugSessionCb!({ type: ExperimentalDebuggerType } as any); + await onDidTerminateDebugSessionCb!({ type: ExperimentalDebuggerType } as any); + await onDidTerminateDebugSessionCb!({ type: ExperimentalDebuggerType } as any); + + appShell.verifyAll(); + showBannerState.verifyAll(); + launchCounterState.verifyAll(); + launchThresholdCounterState.verifyAll(); + expect(currentLaunchCounter).to.be.equal(54); + }); + test('Disabling banner must store value of \'false\' in global store', async () => { + showBannerState.setup(s => s.updateValue(typemoq.It.isValue(false))) + .verifiable(typemoq.Times.once()); + + await banner.disable(); + + showBannerState.verifyAll(); + }); +}); diff --git a/src/test/debugger/capabilities.test.ts b/src/test/debugger/capabilities.test.ts new file mode 100644 index 000000000000..416985681c24 --- /dev/null +++ b/src/test/debugger/capabilities.test.ts @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-suspicious-comment max-func-body-length no-invalid-this no-var-requires no-require-imports no-any + +import { expect } from 'chai'; +import { ChildProcess, spawn } from 'child_process'; +import * as getFreePort from 'get-port'; +import { Socket } from 'net'; +import * as path from 'path'; +import { PassThrough } from 'stream'; +import { Message } from 'vscode-debugadapter/lib/messages'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { sleep } from '../../client/common/core.utils'; +import { createDeferred } from '../../client/common/helpers'; +import { PTVSD_PATH } from '../../client/debugger/Common/constants'; +import { ProtocolParser } from '../../client/debugger/Common/protocolParser'; +import { ProtocolMessageWriter } from '../../client/debugger/Common/protocolWriter'; +import { PythonDebugger } from '../../client/debugger/mainV2'; +import { PYTHON_PATH } from '../common'; +import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; + +class Request extends Message implements DebugProtocol.InitializeRequest { + // tslint:disable-next-line:no-banned-terms + public arguments: any; + constructor(public command: string, args: any) { + super('request'); + this.arguments = args; + } +} + +const fileToDebug = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'remoteDebugger-start-with-ptvsd.py'); + +suite('Debugging - Capabilities', () => { + let disposables: { dispose?: Function; destroy?: Function }[]; + let proc: ChildProcess; + setup(async function () { + if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { + this.skip(); + } + this.timeout(30000); + disposables = []; + }); + teardown(() => { + disposables.forEach(disposable => { + try { + disposable.dispose!(); + // tslint:disable-next-line:no-empty + } catch { } + try { + disposable.destroy!(); + // tslint:disable-next-line:no-empty + } catch { } + }); + try { + proc.kill(); + // tslint:disable-next-line:no-empty + } catch { } + }); + test('Compare capabilities', async () => { + const protocolWriter = new ProtocolMessageWriter(); + const initializeRequest: DebugProtocol.InitializeRequest = new Request('initialize', { pathFormat: 'path' }); + + const debugClient = new PythonDebugger(undefined as any); + const inStream = new PassThrough(); + const outStream = new PassThrough(); + disposables.push(inStream); + disposables.push(outStream); + debugClient.start(inStream, outStream); + const debugClientProtocolParser = new ProtocolParser(); + debugClientProtocolParser.connect(outStream); + disposables.push(debugClientProtocolParser); + const expectedResponsePromise = new Promise(resolve => debugClientProtocolParser.once('response_initialize', resolve)); + protocolWriter.write(inStream, initializeRequest); + const expectedResponse = await expectedResponsePromise; + + const host = 'localhost'; + const port = await getFreePort({ host, port: 3000 }); + const env = { ...process.env }; + env.PYTHONPATH = PTVSD_PATH; + proc = spawn(PYTHON_PATH, ['-m', 'ptvsd', '--server', '--port', `${port}`, '--file', fileToDebug], { cwd: path.dirname(fileToDebug), env }); + await sleep(3000); + + const connected = createDeferred(); + const socket = new Socket(); + socket.on('error', connected.reject.bind(connected)); + socket.connect({ port, host }, () => connected.resolve(socket)); + await connected.promise; + const protocolParser = new ProtocolParser(); + protocolParser.connect(socket!); + disposables.push(protocolParser); + const actualResponsePromise = new Promise(resolve => protocolParser.once('response_initialize', resolve)); + protocolWriter.write(socket!, initializeRequest); + const actualResponse = await actualResponsePromise; + + expect(actualResponse.body).to.deep.equal(expectedResponse.body); + }); +}); diff --git a/src/test/debugger/common/constants.ts b/src/test/debugger/common/constants.ts new file mode 100644 index 000000000000..a9bcc64f1a24 --- /dev/null +++ b/src/test/debugger/common/constants.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// Sometimes PTVSD can take a while for thread & other events to be reported. +export const DEBUGGER_TIMEOUT = 20000; diff --git a/src/test/debugger/configProvider/provider.attach.test.ts b/src/test/debugger/configProvider/provider.attach.test.ts new file mode 100644 index 000000000000..1d2f695daa9a --- /dev/null +++ b/src/test/debugger/configProvider/provider.attach.test.ts @@ -0,0 +1,283 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-invalid-template-strings no-any no-object-literal-type-assertion no-invalid-this + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; +import { PYTHON_LANGUAGE } from '../../../client/common/constants'; +import { EnumEx } from '../../../client/common/enumUtils'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { PythonDebugConfigurationProvider, PythonV2DebugConfigurationProvider } from '../../../client/debugger'; +import { AttachRequestArguments, DebugOptions } from '../../../client/debugger/Common/Contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; + +enum OS { + Windows, + Mac, + Linux +} +[ + { debugType: 'pythonExperimental', class: PythonV2DebugConfigurationProvider }, + { debugType: 'python', class: PythonDebugConfigurationProvider } +].forEach(provider => { + EnumEx.getNamesAndValues(OS).forEach(os => { + suite(`Debugging - Config Provider attach, ${provider.debugType}, OS = ${os.name}`, () => { + let serviceContainer: TypeMoq.IMock; + let debugProvider: DebugConfigurationProvider; + let platformService: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + const debugOptionsAvailable = [DebugOptions.RedirectOutput]; + if (provider.debugType === 'pythonExperimental') { + if (os.value === OS.Windows) { + debugOptionsAvailable.push(DebugOptions.FixFilePathCase); + debugOptionsAvailable.push(DebugOptions.WindowsClient); + } else { + debugOptionsAvailable.push(DebugOptions.UnixClient); + } + } + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + platformService = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + platformService.setup(p => p.isWindows).returns(() => os.value === OS.Windows); + platformService.setup(p => p.isMac).returns(() => os.value === OS.Mac); + platformService.setup(p => p.isLinux).returns(() => os.value === OS.Linux); + debugProvider = new provider.class(serviceContainer.object); + }); + function createMoqWorkspaceFolder(folderPath: string) { + const folder = TypeMoq.Mock.ofType(); + folder.setup(f => f.uri).returns(() => Uri.file(folderPath)); + return folder.object; + } + function setupActiveEditor(fileName: string | undefined, languageId: string) { + const documentManager = TypeMoq.Mock.ofType(); + if (fileName) { + const textEditor = TypeMoq.Mock.ofType(); + const document = TypeMoq.Mock.ofType(); + document.setup(d => d.languageId).returns(() => languageId); + document.setup(d => d.fileName).returns(() => fileName); + textEditor.setup(t => t.document).returns(() => document.object); + documentManager.setup(d => d.activeTextEditor).returns(() => textEditor.object); + } else { + documentManager.setup(d => d.activeTextEditor).returns(() => undefined); + } + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDocumentManager))).returns(() => documentManager.object); + } + function setupWorkspaces(folders: string[]) { + const workspaceService = TypeMoq.Mock.ofType(); + const workspaceFolders = folders.map(createMoqWorkspaceFolder); + workspaceService.setup(w => w.workspaceFolders).returns(() => workspaceFolders); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); + } + test('Defaults should be returned when an empty object is passed with a Workspace Folder and active file', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { request: 'attach' } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + if (provider.debugType === 'python') { + expect(debugConfig).to.have.property('localRoot'); + expect(debugConfig!.localRoot!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); + } + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and active file', async () => { + const pythonFile = 'xyz.py'; + + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + const filePath = Uri.file(path.dirname('')).fsPath; + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + expect(debugConfig).to.have.property('host', 'localhost'); + if (provider.debugType === 'python') { + expect(debugConfig).to.have.property('localRoot'); + expect(debugConfig!.localRoot!.toLowerCase()).to.be.equal(filePath.toLowerCase()); + } + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and no active file', async () => { + setupActiveEditor(undefined, PYTHON_LANGUAGE); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + expect(debugConfig).to.have.property('host', 'localhost'); + if (provider.debugType === 'python') { + expect(debugConfig).to.not.have.property('localRoot'); + } + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and non python file', async () => { + const activeFile = 'xyz.js'; + + setupActiveEditor(activeFile, 'javascript'); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + expect(debugConfig).to.not.have.property('localRoot'); + expect(debugConfig).to.have.property('host', 'localhost'); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, with a workspace and an active python file', async () => { + const activeFile = 'xyz.py'; + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + const filePath = Uri.file(defaultWorkspace).fsPath; + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + expect(debugConfig).to.have.property('host', 'localhost'); + if (provider.debugType === 'python') { + expect(debugConfig).to.have.property('localRoot'); + expect(debugConfig!.localRoot!.toLowerCase()).to.be.equal(filePath.toLowerCase()); + } + }); + test('Ensure \'localRoot\' is left unaltered', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('localRoot', localRoot); + }); + ['localhost', '127.0.0.1', '::1'].forEach(host => { + test(`Ensure path mappings are automatically added when host is '${host}'`, async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, host, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('localRoot', localRoot); + if (provider.debugType === 'pythonExperimental') { + const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + expect(pathMappings).to.be.lengthOf(1); + expect(pathMappings![0].localRoot).to.be.equal(workspaceFolder.uri.fsPath); + expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); + } + }); + }); + ['192.168.1.123', 'don.debugger.com'].forEach(host => { + test(`Ensure path mappings are not automatically added when host is '${host}'`, async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, host, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('localRoot', localRoot); + if (provider.debugType === 'pythonExperimental') { + const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + expect(pathMappings).to.be.lengthOf(0); + } + }); + }); + test('Ensure \'localRoot\' and \'remoteRoot\' is used', async function () { + if (provider.debugType !== 'pythonExperimental') { + return this.skip(); + } + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; + const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, remoteRoot, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig!.pathMappings).to.be.lengthOf(1); + expect(debugConfig!.pathMappings).to.deep.include({ localRoot, remoteRoot }); + }); + test('Ensure \'localRoot\' and \'remoteRoot\' is used', async function () { + if (provider.debugType !== 'pythonExperimental') { + return this.skip(); + } + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; + const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, remoteRoot, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig!.pathMappings).to.be.lengthOf(1); + expect(debugConfig!.pathMappings).to.deep.include({ localRoot, remoteRoot }); + }); + test('Ensure \'remoteRoot\' is left unaltered', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const remoteRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { remoteRoot, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('remoteRoot', remoteRoot); + }); + test('Ensure \'port\' is left unaltered', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const port = 12341234; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { port, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('port', port); + }); + test('Ensure \'debugOptions\' are left unaltered', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugOptions = debugOptionsAvailable.slice().concat(DebugOptions.Jinja, DebugOptions.Sudo); + const expectedDebugOptions = debugOptions.slice(); + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { debugOptions, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('debugOptions').to.be.deep.equal(expectedDebugOptions); + }); + }); + }); +}); diff --git a/src/test/debugger/configProvider/provider.test.ts b/src/test/debugger/configProvider/provider.test.ts index e244e9a28c8c..888fc1b2a9da 100644 --- a/src/test/debugger/configProvider/provider.test.ts +++ b/src/test/debugger/configProvider/provider.test.ts @@ -3,17 +3,22 @@ 'use strict'; -// tslint:disable:max-func-body-length no-invalid-template-strings no-any +// tslint:disable:max-func-body-length no-invalid-template-strings no-any no-object-literal-type-assertion import { expect } from 'chai'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; -import { IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; -import { PythonLanguage } from '../../../client/common/constants'; -import { IPlatformService } from '../../../client/common/platform/types'; -import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; +import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; +import { PYTHON_LANGUAGE } from '../../../client/common/constants'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types'; +import { IConfigurationService, ILogger, IPythonSettings } from '../../../client/common/types'; import { PythonDebugConfigurationProvider, PythonV2DebugConfigurationProvider } from '../../../client/debugger'; +import { DebugOptions, LaunchRequestArguments } from '../../../client/debugger/Common/Contracts'; +import { PythonLaunchDebugConfiguration } from '../../../client/debugger/configProviders/baseProvider'; +import { ConfigurationProviderUtils } from '../../../client/debugger/configProviders/configurationProviderUtils'; +import { IConfigurationProviderUtils } from '../../../client/debugger/configProviders/types'; import { IServiceContainer } from '../../../client/ioc/types'; [ @@ -24,6 +29,10 @@ import { IServiceContainer } from '../../../client/ioc/types'; let serviceContainer: TypeMoq.IMock; let debugProvider: DebugConfigurationProvider; let platformService: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + let appShell: TypeMoq.IMock; + let pythonExecutionService: TypeMoq.IMock; + let logger: TypeMoq.IMock; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); debugProvider = new provider.class(serviceContainer.object); @@ -36,8 +45,23 @@ import { IServiceContainer } from '../../../client/ioc/types'; function setupIoc(pythonPath: string, isWindows: boolean = false, isMac: boolean = false, isLinux: boolean = false) { const confgService = TypeMoq.Mock.ofType(); platformService = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + appShell = TypeMoq.Mock.ofType(); + logger = TypeMoq.Mock.ofType(); + + pythonExecutionService = TypeMoq.Mock.ofType(); + pythonExecutionService.setup((x: any) => x.then).returns(() => undefined); + const factory = TypeMoq.Mock.ofType(); + factory.setup(f => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(pythonExecutionService.object)); + + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPythonExecutionFactory))).returns(() => factory.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => confgService.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationProviderUtils))).returns(() => new ConfigurationProviderUtils(serviceContainer.object)); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ILogger))).returns(() => logger.object); + const settings = TypeMoq.Mock.ofType(); settings.setup(s => s.pythonPath).returns(() => pythonPath); confgService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); @@ -74,7 +98,7 @@ import { IServiceContainer } from '../../../client/ioc/types'; const pythonFile = 'xyz.py'; setupIoc(pythonPath); - setupActiveEditor(pythonFile, PythonLanguage.language); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, {} as DebugConfiguration); @@ -96,7 +120,7 @@ import { IServiceContainer } from '../../../client/ioc/types'; const workspaceFolder = createMoqWorkspaceFolder(__dirname); const pythonFile = 'xyz.py'; setupIoc(pythonPath); - setupActiveEditor(pythonFile, PythonLanguage.language); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { noDebug: true } as any as DebugConfiguration); @@ -117,7 +141,7 @@ import { IServiceContainer } from '../../../client/ioc/types'; const pythonPath = `PythonPath_${new Date().toString()}`; const pythonFile = 'xyz.py'; setupIoc(pythonPath); - setupActiveEditor(pythonFile, PythonLanguage.language); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); setupWorkspaces([]); const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); @@ -139,7 +163,7 @@ import { IServiceContainer } from '../../../client/ioc/types'; test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and no active file', async () => { const pythonPath = `PythonPath_${new Date().toString()}`; setupIoc(pythonPath); - setupActiveEditor(undefined, PythonLanguage.language); + setupActiveEditor(undefined, PYTHON_LANGUAGE); setupWorkspaces([]); const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); @@ -179,7 +203,7 @@ import { IServiceContainer } from '../../../client/ioc/types'; const pythonPath = `PythonPath_${new Date().toString()}`; const activeFile = 'xyz.py'; setupIoc(pythonPath); - setupActiveEditor(activeFile, PythonLanguage.language); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); const defaultWorkspace = path.join('usr', 'desktop'); setupWorkspaces([defaultWorkspace]); @@ -204,7 +228,7 @@ import { IServiceContainer } from '../../../client/ioc/types'; const activeFile = 'xyz.py'; const workspaceFolder = createMoqWorkspaceFolder(__dirname); setupIoc(pythonPath); - setupActiveEditor(activeFile, PythonLanguage.language); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); const defaultWorkspace = path.join('usr', 'desktop'); setupWorkspaces([defaultWorkspace]); @@ -217,7 +241,7 @@ import { IServiceContainer } from '../../../client/ioc/types'; const activeFile = 'xyz.py'; const workspaceFolder = createMoqWorkspaceFolder(__dirname); setupIoc(pythonPath); - setupActiveEditor(activeFile, PythonLanguage.language); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); const defaultWorkspace = path.join('usr', 'desktop'); setupWorkspaces([defaultWorkspace]); @@ -234,7 +258,7 @@ import { IServiceContainer } from '../../../client/ioc/types'; const workspaceFolder = createMoqWorkspaceFolder(__dirname); const pythonFile = 'xyz.py'; setupIoc(pythonPath); - setupActiveEditor(pythonFile, PythonLanguage.language); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, {} as DebugConfiguration); @@ -251,41 +275,44 @@ import { IServiceContainer } from '../../../client/ioc/types'; const workspaceFolder = createMoqWorkspaceFolder(__dirname); const pythonFile = 'xyz.py'; setupIoc(pythonPath); - setupActiveEditor(pythonFile, PythonLanguage.language); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, {} as DebugConfiguration); expect(debugConfig).to.have.property('stopOnEntry', false); expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as any).debugOptions).to.be.deep.equal(['RedirectOutput']); + expect((debugConfig as any).debugOptions).to.be.deep.equal([DebugOptions.RedirectOutput]); }); - test('Test redirection of output', async () => { - if (provider.debugType === 'python') { + test('Test overriding defaults of experimental debugger', async () => { + if (provider.debugType !== 'pythonExperimental') { return; } const pythonPath = `PythonPath_${new Date().toString()}`; const workspaceFolder = createMoqWorkspaceFolder(__dirname); const pythonFile = 'xyz.py'; setupIoc(pythonPath); - setupActiveEditor(pythonFile, PythonLanguage.language); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { debugOptions: ['RedirectOutput'] } as any); + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { redirectOutput: false } as PythonLaunchDebugConfiguration); - expect(debugConfig).to.have.property('redirectOutput'); - expect((debugConfig as any).redirectOutput).to.be.equal(true, 'invalid value'); + expect(debugConfig).to.have.property('console', 'integratedTerminal'); + expect(debugConfig).to.have.property('stopOnEntry', false); + expect(debugConfig).to.have.property('debugOptions'); + expect((debugConfig as any).debugOptions).to.be.deep.equal([]); }); - async function testFixFilePathCase(isWindows: boolean, isMac: boolean, isLinux: boolean) { const pythonPath = `PythonPath_${new Date().toString()}`; const workspaceFolder = createMoqWorkspaceFolder(__dirname); const pythonFile = 'xyz.py'; setupIoc(pythonPath, isWindows, isMac, isLinux); - setupActiveEditor(pythonFile, PythonLanguage.language); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, {} as DebugConfiguration); - - expect(debugConfig).to.have.property('fixFilePathCase'); - expect((debugConfig as any).fixFilePathCase).to.be.equal(isWindows, 'invalid value (true only for windows)'); + if (isWindows) { + expect(debugConfig).to.have.property('debugOptions').contains(DebugOptions.FixFilePathCase); + } else { + expect(debugConfig).to.have.property('debugOptions').not.contains(DebugOptions.FixFilePathCase); + } } test('Test fixFilePathCase for Windows', async () => { if (provider.debugType === 'python') { @@ -305,5 +332,94 @@ import { IServiceContainer } from '../../../client/ioc/types'; } await testFixFilePathCase(false, true, false); }); + async function testPyramidConfiguration(isWindows: boolean, isLinux: boolean, isMac: boolean, addPyramidDebugOption: boolean = true, pyramidExists = true, shouldWork = true) { + const workspacePath = path.join('usr', 'development', 'wksp1'); + const pythonPath = path.join(workspacePath, 'env', 'bin', 'python'); + const pyramidFilePath = path.join(path.dirname(pythonPath), 'lib', 'site_packages', 'pyramid', '__init__.py'); + const pserveFilePath = path.join(path.dirname(pyramidFilePath), 'scripts', 'pserve.py'); + const args = ['-c', 'import pyramid;print(pyramid.__file__)']; + const workspaceFolder = createMoqWorkspaceFolder(workspacePath); + const pythonFile = 'xyz.py'; + + setupIoc(pythonPath, isWindows, isMac, isLinux); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + if (pyramidExists) { + pythonExecutionService.setup(e => e.exec(TypeMoq.It.isValue(args), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: pyramidFilePath })) + .verifiable(TypeMoq.Times.exactly(addPyramidDebugOption ? 1 : 0)); + } else { + pythonExecutionService.setup(e => e.exec(TypeMoq.It.isValue(args), TypeMoq.It.isAny())) + .returns(() => Promise.reject('No Module Available')) + .verifiable(TypeMoq.Times.exactly(addPyramidDebugOption ? 1 : 0)); + } + fileSystem.setup(f => f.fileExists(TypeMoq.It.isValue(pserveFilePath))) + .returns(() => Promise.resolve(pyramidExists)) + .verifiable(TypeMoq.Times.exactly(pyramidExists && addPyramidDebugOption ? 1 : 0)); + appShell.setup(a => a.showErrorMessage(TypeMoq.It.isAny())) + .verifiable(TypeMoq.Times.exactly(pyramidExists || !addPyramidDebugOption ? 0 : 1)); + logger.setup(a => a.logError(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .verifiable(TypeMoq.Times.exactly(pyramidExists || !addPyramidDebugOption ? 0 : 1)); + const options = addPyramidDebugOption ? { debugOptions: [DebugOptions.Pyramid], pyramid: true } : {}; + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, options as any as DebugConfiguration); + if (shouldWork) { + expect(debugConfig).to.have.property('program', pserveFilePath); + + if (provider.debugType === 'pythonExperimental') { + expect(debugConfig).to.have.property('debugOptions'); + expect((debugConfig as any).debugOptions).contains(DebugOptions.Jinja); + } + } else { + expect(debugConfig!.program).to.be.not.equal(pserveFilePath); + } + pythonExecutionService.verifyAll(); + fileSystem.verifyAll(); + appShell.verifyAll(); + logger.verifyAll(); + } + test('Program is set for Pyramid (windows)', async () => { + await testPyramidConfiguration(true, false, false); + }); + test('Program is set for Pyramid (Linux)', async () => { + await testPyramidConfiguration(false, true, false); + }); + test('Program is set for Pyramid (Mac)', async () => { + await testPyramidConfiguration(false, false, true); + }); + test('Program is not set for Pyramid when DebugOption is not set (windows)', async () => { + await testPyramidConfiguration(true, false, false, false, false, false); + }); + test('Program is not set for Pyramid when DebugOption is not set (Linux)', async () => { + await testPyramidConfiguration(false, true, false, false, false, false); + }); + test('Program is not set for Pyramid when DebugOption is not set (Mac)', async () => { + await testPyramidConfiguration(false, false, true, false, false, false); + }); + test('Message is displayed when pyramid script does not exist (windows)', async () => { + await testPyramidConfiguration(true, false, false, true, false, false); + }); + test('Message is displayed when pyramid script does not exist (Linux)', async () => { + await testPyramidConfiguration(false, true, false, true, false, false); + }); + test('Message is displayed when pyramid script does not exist (Mac)', async () => { + await testPyramidConfiguration(false, false, true, true, false, false); + }); + test('Auto detect flask debugging', async () => { + if (provider.debugType === 'python') { + return; + } + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { module: 'flask' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('debugOptions'); + expect((debugConfig as any).debugOptions).contains(DebugOptions.RedirectOutput); + expect((debugConfig as any).debugOptions).contains(DebugOptions.Jinja); + }); }); }); diff --git a/src/test/debugger/core/capabilities.test.ts b/src/test/debugger/core/capabilities.test.ts deleted file mode 100644 index 63bc7314a0e1..000000000000 --- a/src/test/debugger/core/capabilities.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// 1. Ensure the capabilitites of the debugger sent into the response to initialize matches -// that of the underlying (ptvsd) debugger -// I.e. ensure the response sent by us matches the response sent by ptvsd to the initialize request. diff --git a/src/test/debugger/debugClient.ts b/src/test/debugger/debugClient.ts new file mode 100644 index 000000000000..9c45e7f45824 --- /dev/null +++ b/src/test/debugger/debugClient.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ChildProcess, spawn, SpawnOptions } from 'child_process'; +import * as path from 'path'; +import { DebugClient } from 'vscode-debugadapter-testsupport'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { noop } from '../../client/common/core.utils'; + +export class DebugClientEx extends DebugClient { + private adapterProcess: ChildProcess | undefined; + constructor(private executable: string, debugType: string, private coverageDirectory: string, private spawnOptions?: SpawnOptions) { + super('node', '', debugType, spawnOptions); + } + /** + * Starts a new debug adapter and sets up communication via stdin/stdout. + * If a port number is specified the adapter is not launched but a connection to + * a debug adapter running in server mode is established. This is useful for debugging + * the adapter while running tests. For this reason all timeouts are disabled in server mode. + */ + public start(port?: number): Promise { + return new Promise((resolve, reject) => { + const runtime = path.join(EXTENSION_ROOT_DIR, 'node_modules', '.bin', 'istanbul'); + const args = ['cover', '--report=json', '--print=none', `--dir=${this.coverageDirectory}`, '--handle-sigint', this.executable]; + this.adapterProcess = spawn(runtime, args, this.spawnOptions); + this.adapterProcess.stderr.on('data', noop); + this.adapterProcess.on('error', (err) => { + console.error(err); + reject(err); + }); + this.adapterProcess.on('exit', noop); + this.connect(this.adapterProcess.stdout, this.adapterProcess.stdin); + resolve(); + }); + } + public stop(): Promise { + return this.disconnectRequest().then(this.stopAdapterProcess).catch(this.stopAdapterProcess); + } + private stopAdapterProcess = () => { + if (this.adapterProcess) { + this.adapterProcess.kill(); + this.adapterProcess = undefined; + } + } +} diff --git a/src/test/debugger/launcherScriptProvider.test.ts b/src/test/debugger/launcherScriptProvider.test.ts index 147869aa8f20..3a9358a9ff8e 100644 --- a/src/test/debugger/launcherScriptProvider.test.ts +++ b/src/test/debugger/launcherScriptProvider.test.ts @@ -4,7 +4,7 @@ import { expect } from 'chai'; import * as fs from 'fs'; import * as path from 'path'; -import { DebuggerLauncherScriptProvider, DebuggerV2LauncherScriptProvider, NoDebugLauncherScriptProvider } from '../../client/debugger/DebugClients/launcherProvider'; +import { DebuggerLauncherScriptProvider, NoDebugLauncherScriptProvider } from '../../client/debugger/DebugClients/launcherProvider'; suite('Debugger - Launcher Script Provider', () => { test('Ensure stable debugger gets the old launcher from PythonTools directory', () => { @@ -19,10 +19,4 @@ suite('Debugger - Launcher Script Provider', () => { expect(launcherPath).to.be.equal(expectedPath); expect(fs.existsSync(launcherPath)).to.be.equal(true, 'file does not exist'); }); - test('Ensure experimental debugger gets the new launcher from experimentals directory', () => { - const launcherPath = new DebuggerV2LauncherScriptProvider().getLauncherFilePath(); - const expectedPath = path.join(path.dirname(__dirname), '..', '..', 'pythonFiles', 'experimental', 'ptvsd_launcher.py'); - expect(launcherPath).to.be.equal(expectedPath); - expect(fs.existsSync(launcherPath)).to.be.equal(true, 'file does not exist'); - }); }); diff --git a/src/test/debugger/misc.test.ts b/src/test/debugger/misc.test.ts index 348219134d34..5e4d10adf2e6 100644 --- a/src/test/debugger/misc.test.ts +++ b/src/test/debugger/misc.test.ts @@ -3,30 +3,33 @@ // tslint:disable:no-suspicious-comment max-func-body-length no-invalid-this no-var-requires no-require-imports no-any -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; +import { expect } from 'chai'; import * as path from 'path'; import { ThreadEvent } from 'vscode-debugadapter'; import { DebugClient } from 'vscode-debugadapter-testsupport'; import { DebugProtocol } from 'vscode-debugprotocol'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; import { noop } from '../../client/common/core.utils'; +import { IS_WINDOWS } from '../../client/common/platform/constants'; import { FileSystem } from '../../client/common/platform/fileSystem'; import { PlatformService } from '../../client/common/platform/platformService'; -import { LaunchRequestArguments } from '../../client/debugger/Common/Contracts'; -import { sleep } from '../common'; +import { PTVSD_PATH } from '../../client/debugger/Common/constants'; +import { DebugOptions, LaunchRequestArguments } from '../../client/debugger/Common/Contracts'; +import { PYTHON_PATH, sleep } from '../common'; import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; +import { DEBUGGER_TIMEOUT } from './common/constants'; +import { DebugClientEx } from './debugClient'; +import { continueDebugging } from './utils'; const isProcessRunning = require('is-running') as (number) => boolean; -use(chaiAsPromised); - const debugFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'debugging'); const DEBUG_ADAPTER = path.join(__dirname, '..', '..', 'client', 'debugger', 'Main.js'); const MAX_SIGNED_INT32 = Math.pow(2, 31) - 1; const EXPERIMENTAL_DEBUG_ADAPTER = path.join(__dirname, '..', '..', 'client', 'debugger', 'mainV2.js'); -const THREAD_TIMEOUT = 10000; +let testCounter = 0; [DEBUG_ADAPTER, EXPERIMENTAL_DEBUG_ADAPTER].forEach(testAdapterFilePath => { const debugAdapterFileName = path.basename(testAdapterFilePath); const debuggerType = debugAdapterFileName === 'Main.js' ? 'python' : 'pythonExperimental'; @@ -38,7 +41,8 @@ const THREAD_TIMEOUT = 10000; this.skip(); } await new Promise(resolve => setTimeout(resolve, 1000)); - debugClient = new DebugClient('node', testAdapterFilePath, debuggerType); + debugClient = createDebugAdapter(); + debugClient.defaultTimeout = DEBUGGER_TIMEOUT; await debugClient.start(); }); teardown(async () => { @@ -50,25 +54,42 @@ const THREAD_TIMEOUT = 10000; } catch (ex) { } await sleep(1000); }); - function buildLauncArgs(pythonFile: string, stopOnEntry: boolean = false): LaunchRequestArguments { - return { + /** + * Creates the debug adapter. + * We do not need to support code coverage on AppVeyor, lets use the standard test adapter. + * @returns {DebugClient} + */ + function createDebugAdapter(): DebugClient { + if (IS_WINDOWS) { + return new DebugClient('node', testAdapterFilePath, debuggerType); + } else { + const coverageDirectory = path.join(EXTENSION_ROOT_DIR, `debug_coverage${testCounter += 1}`); + return new DebugClientEx(testAdapterFilePath, debuggerType, coverageDirectory, { cwd: EXTENSION_ROOT_DIR }); + } + } + function buildLaunchArgs(pythonFile: string, stopOnEntry: boolean = false): LaunchRequestArguments { + const env = debuggerType === 'pythonExperimental' ? { PYTHONPATH: PTVSD_PATH } : {}; + // tslint:disable-next-line:no-unnecessary-local-variable + const options: LaunchRequestArguments = { program: path.join(debugFilesPath, pythonFile), cwd: debugFilesPath, stopOnEntry, - debugOptions: ['RedirectOutput'], - pythonPath: 'python', + debugOptions: [DebugOptions.RedirectOutput], + pythonPath: PYTHON_PATH, args: [], - env: {}, + env, envFile: '', logToFile: false, type: debuggerType }; + + return options; } test('Should run program to the end', async () => { await Promise.all([ debugClient.configurationSequence(), - debugClient.launch(buildLauncArgs('simplePrint.py', false)), + debugClient.launch(buildLaunchArgs('simplePrint.py', false)), debugClient.waitForEvent('initialized'), debugClient.waitForEvent('terminated') ]); @@ -79,31 +100,26 @@ const THREAD_TIMEOUT = 10000; } await Promise.all([ debugClient.configurationSequence(), - debugClient.launch(buildLauncArgs('simplePrint.py', true)), + debugClient.launch(buildLaunchArgs('simplePrint.py', true)), debugClient.waitForEvent('initialized'), debugClient.waitForEvent('stopped') ]); }); - test('test stderr output', async function () { - if (debuggerType !== 'python') { - return this.skip(); - } + test('test stderr output for Python', async () => { + const output = debuggerType === 'python' ? 'stdout' : 'stderr'; await Promise.all([ debugClient.configurationSequence(), - debugClient.launch(buildLauncArgs('stdErrOutput.py', false)), + debugClient.launch(buildLaunchArgs('stdErrOutput.py', false)), debugClient.waitForEvent('initialized'), //TODO: ptvsd does not differentiate. - debugClient.assertOutput('stdout', 'error output'), + debugClient.assertOutput(output, 'error output'), debugClient.waitForEvent('terminated') ]); }); - test('Test stdout output', async function () { - if (debuggerType !== 'python') { - return this.skip(); - } + test('Test stdout output', async () => { await Promise.all([ debugClient.configurationSequence(), - debugClient.launch(buildLauncArgs('stdOutOutput.py', false)), + debugClient.launch(buildLaunchArgs('stdOutOutput.py', false)), debugClient.waitForEvent('initialized'), debugClient.assertOutput('stdout', 'normal output'), debugClient.waitForEvent('terminated') @@ -113,11 +129,11 @@ const THREAD_TIMEOUT = 10000; if (debuggerType !== 'python') { return this.skip(); } - const threadIdPromise = debugClient.waitForEvent('thread', THREAD_TIMEOUT); + const threadIdPromise = debugClient.waitForEvent('thread'); await Promise.all([ debugClient.configurationSequence(), - debugClient.launch(buildLauncArgs('simplePrint.py', true)), + debugClient.launch(buildLaunchArgs('simplePrint.py', true)), debugClient.waitForEvent('initialized'), debugClient.waitForEvent('stopped') ]); @@ -132,11 +148,11 @@ const THREAD_TIMEOUT = 10000; if (debuggerType !== 'python') { return this.skip(); } - const threadIdPromise = debugClient.waitForEvent('thread', THREAD_TIMEOUT); + const threadIdPromise = debugClient.waitForEvent('thread'); await Promise.all([ debugClient.configurationSequence(), - debugClient.launch(buildLauncArgs('simplePrint.py', true)), + debugClient.launch(buildLaunchArgs('simplePrint.py', true)), debugClient.waitForEvent('initialized'), debugClient.waitForEvent('stopped') ]); @@ -149,7 +165,7 @@ const THREAD_TIMEOUT = 10000; ]); }); test('Should break at print statement (line 3)', async () => { - const launchArgs = buildLauncArgs('sample2.py', false); + const launchArgs = buildLaunchArgs('sample2.py', false); const breakpointLocation = { path: path.join(debugFilesPath, 'sample2.py'), column: 1, line: 5 }; await debugClient.hitBreakpoint(launchArgs, breakpointLocation); }); @@ -157,9 +173,9 @@ const THREAD_TIMEOUT = 10000; if (debuggerType === 'python') { return this.skip(); } - const launchArgs = buildLauncArgs('sample2.py', false); + const launchArgs = buildLaunchArgs('sample2.py', false); const breakpointLocation = { path: path.join(debugFilesPath, 'sample2.py'), column: 1, line: 5 }; - const processPromise = debugClient.waitForEvent('process', THREAD_TIMEOUT) as Promise; + const processPromise = debugClient.waitForEvent('process') as Promise; await debugClient.hitBreakpoint(launchArgs, breakpointLocation); const processInfo = await processPromise; const processId = processInfo.body.systemProcessId; @@ -172,11 +188,11 @@ const THREAD_TIMEOUT = 10000; expect(isProcessRunning(processId)).to.be.equal(false, 'Python (debugee) Process is still alive'); }); test('Test conditional breakpoints', async () => { - const threadIdPromise = debugClient.waitForEvent('thread', THREAD_TIMEOUT); + const threadIdPromise = debugClient.waitForEvent('thread'); await Promise.all([ debugClient.configurationSequence(), - debugClient.launch(buildLauncArgs('forever.py', false)), + debugClient.launch(buildLaunchArgs('forever.py', false)), debugClient.waitForEvent('initialized') ]); @@ -203,10 +219,10 @@ const THREAD_TIMEOUT = 10000; expect(vari.value).to.be.equal('3'); }); test('Test variables', async () => { - const threadIdPromise = debugClient.waitForEvent('thread', THREAD_TIMEOUT); + const threadIdPromise = debugClient.waitForEvent('thread'); await Promise.all([ debugClient.configurationSequence(), - debugClient.launch(buildLauncArgs('sample2.py', false)), + debugClient.launch(buildLaunchArgs('sample2.py', false)), debugClient.waitForEvent('initialized') ]); @@ -243,7 +259,7 @@ const THREAD_TIMEOUT = 10000; test('Test editing variables', async () => { await Promise.all([ debugClient.configurationSequence(), - debugClient.launch(buildLauncArgs('sample2.py', false)), + debugClient.launch(buildLaunchArgs('sample2.py', false)), debugClient.waitForEvent('initialized') ]); @@ -273,11 +289,11 @@ const THREAD_TIMEOUT = 10000; expect(response.body.value).to.be.equal('1234'); }); test('Test evaluating expressions', async () => { - const threadIdPromise = debugClient.waitForEvent('thread', THREAD_TIMEOUT); + const threadIdPromise = debugClient.waitForEvent('thread'); await Promise.all([ debugClient.configurationSequence(), - debugClient.launch(buildLauncArgs('sample2.py', false)), + debugClient.launch(buildLaunchArgs('sample2.py', false)), debugClient.waitForEvent('initialized') ]); @@ -299,11 +315,11 @@ const THREAD_TIMEOUT = 10000; expect(response.body.result).to.be.equal('6', 'expression value is incorrect'); }); test('Test stepover', async () => { - const threadIdPromise = debugClient.waitForEvent('thread', THREAD_TIMEOUT); + const threadIdPromise = debugClient.waitForEvent('thread'); await Promise.all([ debugClient.configurationSequence(), - debugClient.launch(buildLauncArgs('sample2.py', false)), + debugClient.launch(buildLaunchArgs('sample2.py', false)), debugClient.waitForEvent('initialized') ]); @@ -337,11 +353,11 @@ const THREAD_TIMEOUT = 10000; ]); }); test('Test stepin and stepout', async () => { - const threadIdPromise = debugClient.waitForEvent('thread', THREAD_TIMEOUT); + const threadIdPromise = debugClient.waitForEvent('thread'); await Promise.all([ debugClient.configurationSequence(), - debugClient.launch(buildLauncArgs('sample2.py', false)), + debugClient.launch(buildLaunchArgs('sample2.py', false)), debugClient.waitForEvent('initialized') ]); @@ -392,9 +408,9 @@ const THREAD_TIMEOUT = 10000; await Promise.all([ debugClient.configurationSequence(), - debugClient.launch(buildLauncArgs('forever.py', false)), + debugClient.launch(buildLaunchArgs('forever.py', false)), debugClient.waitForEvent('initialized'), - debugClient.waitForEvent('process', THREAD_TIMEOUT) + debugClient.waitForEvent('process') ]); await sleep(3); @@ -413,19 +429,58 @@ const THREAD_TIMEOUT = 10000; await Promise.all([ debugClient.configurationSequence(), - debugClient.launch(buildLauncArgs('sample3WithEx.py', false)), + debugClient.launch(buildLaunchArgs('sample3WithEx.py', false)), debugClient.waitForEvent('initialized') ]); const pauseLocation = { path: path.join(debugFilesPath, 'sample3WithEx.py'), line: 5 }; await debugClient.assertStoppedLocation('exception', pauseLocation); }); + test('Test pausing on assert failures', async () => { + const pauseLocation = { path: path.join(debugFilesPath, 'sampleWithAssertEx.py'), line: 1 }; + + function waitToStopDueToException() { + return new Promise((resolve, reject) => { + debugClient.once('stopped', (event: DebugProtocol.StoppedEvent) => { + if (event.body.reason === 'exception' && + event.body.text && event.body.text!.startsWith('AssertionError')) { + resolve(); + } else { + reject(new Error('Stopped for some other reason')); + } + }); + setTimeout(() => { + reject(new Error(`waitToStopDueToException not received after ${debugClient.defaultTimeout} ms`)); + }, debugClient.defaultTimeout); + }); + } + + function setBreakpointFilter(): Promise { + if (debuggerType === 'python') { + return Promise.resolve(); + } else { + return debugClient.waitForEvent('initialized') + .then(() => debugClient.setExceptionBreakpointsRequest({ filters: ['uncaught'] })) + .then(() => debugClient.configurationDoneRequest()); + } + } + await Promise.all([ + debugClient.configurationSequence(), + setBreakpointFilter(), + debugClient.launch(buildLaunchArgs('sampleWithAssertEx.py', false)), + waitToStopDueToException(), + debugClient.assertStoppedLocation('exception', pauseLocation) + ]); + }); test('Test multi-threaded debugging', async () => { await Promise.all([ debugClient.configurationSequence(), - debugClient.launch(buildLauncArgs('multiThread.py', false)), + debugClient.launch(buildLaunchArgs('multiThread.py', false)), debugClient.waitForEvent('initialized') ]); + + // Add a delay for debugger to start (sometimes it takes a long time for new debugger to break). + await sleep(3000); const pythonFile = path.join(debugFilesPath, 'multiThread.py'); const breakpointLocation = { path: pythonFile, column: 1, line: 11 }; await debugClient.setBreakpointsRequest({ @@ -434,8 +489,49 @@ const THREAD_TIMEOUT = 10000; source: { path: breakpointLocation.path } }); - // hit breakpoint. await debugClient.assertStoppedLocation('breakpoint', breakpointLocation); + const threads = await debugClient.threadsRequest(); + expect(threads.body.threads).of.lengthOf(2, 'incorrect number of threads'); + for (const thread of threads.body.threads) { + expect(thread.id).to.be.lessThan(MAX_SIGNED_INT32 + 1, 'ThreadId is not an integer'); + } + }); + test('Test multi-threaded debugging', async function () { + this.timeout(30000); + await Promise.all([ + debugClient.launch(buildLaunchArgs('multiThread.py', false)), + debugClient.waitForEvent('initialized') + ]); + + const pythonFile = path.join(debugFilesPath, 'multiThread.py'); + const breakpointLocation = { path: pythonFile, column: 1, line: 11 }; + const breakpointRequestArgs = { + lines: [breakpointLocation.line], + breakpoints: [{ line: breakpointLocation.line, column: breakpointLocation.column }], + source: { path: breakpointLocation.path } + }; + + function waitForStoppedEventFromTwoThreads() { + return new Promise((resolve, reject) => { + let numberOfStops = 0; + debugClient.addListener('stopped', (event: DebugProtocol.StoppedEvent) => { + numberOfStops += 1; + if (numberOfStops < 2) { + return; + } + resolve(event); + }); + setTimeout(() => reject(new Error('Timeout waiting for two threads to stop at breakpoint')), DEBUGGER_TIMEOUT); + }); + } + + await Promise.all([ + debugClient.setBreakpointsRequest(breakpointRequestArgs), + debugClient.setExceptionBreakpointsRequest({ filters: [] }), + debugClient.configurationDoneRequest(), + waitForStoppedEventFromTwoThreads(), + debugClient.assertStoppedLocation('breakpoint', breakpointLocation) + ]); const threads = await debugClient.threadsRequest(); expect(threads.body.threads).of.lengthOf(2, 'incorrect number of threads'); @@ -446,7 +542,7 @@ const THREAD_TIMEOUT = 10000; test('Test stack frames', async () => { await Promise.all([ debugClient.configurationSequence(), - debugClient.launch(buildLauncArgs('stackFrame.py', false)), + debugClient.launch(buildLaunchArgs('stackFrame.py', false)), debugClient.waitForEvent('initialized') ]); const pythonFile = path.join(debugFilesPath, 'stackFrame.py'); @@ -471,5 +567,95 @@ const THREAD_TIMEOUT = 10000; expect(stackframes.body.stackFrames[2].line).to.be.equal(10); expect(fileSystem.arePathsSame(stackframes.body.stackFrames[2].source!.path!, pythonFile)).to.be.equal(true, 'paths do not match'); }); + test('Test Evaluation of Expressions', async function () { + if (debuggerType !== 'pythonExperimental') { + return this.skip(); + } + const breakpointLocation = { path: path.join(debugFilesPath, 'sample2WithoutSleep.py'), column: 1, line: 5 }; + const breakpointArgs = { + lines: [breakpointLocation.line], + breakpoints: [{ line: breakpointLocation.line, column: breakpointLocation.column }], + source: { path: breakpointLocation.path } + }; + await Promise.all([ + debugClient.launch(buildLaunchArgs('sample2WithoutSleep.py', false)), + debugClient.waitForEvent('initialized') + .then(() => debugClient.setBreakpointsRequest(breakpointArgs)) + .then(() => debugClient.configurationDoneRequest()) + .then(() => debugClient.threadsRequest()), + debugClient.waitForEvent('thread'), + debugClient.assertStoppedLocation('breakpoint', breakpointLocation) + ]); + + //Do not remove this, this is required to ensure PTVSD is ready to accept other requests. + await debugClient.threadsRequest(); + const evaluateResponse = await debugClient.evaluateRequest({ context: 'repl', expression: 'a+b+2', frameId: 1 }); + expect(evaluateResponse.body.type).to.equal('int'); + expect(evaluateResponse.body.result).to.equal('5'); + await continueDebugging(debugClient); + }); + test('Test Passing custom args to python file', async function () { + if (debuggerType !== 'pythonExperimental') { + return this.skip(); + } + const options = buildLaunchArgs('printSysArgv.py', false); + options.args = ['1', '2', '3']; + await Promise.all([ + debugClient.configurationSequence(), + debugClient.launch(options), + debugClient.assertOutput('stdout', options.args.join(',')), + debugClient.waitForEvent('terminated') + ]); + }); + test('Test Logpoints', async function () { + if (debuggerType !== 'pythonExperimental') { + return this.skip(); + } + const breakpointLocation = { path: path.join(debugFilesPath, 'logMessage.py'), line: 4 }; + const breakpointArgs: DebugProtocol.SetBreakpointsArguments = { + lines: [breakpointLocation.line], + breakpoints: [{ line: breakpointLocation.line, logMessage: 'Sum of {a} and {b} is 3' }], + source: { path: breakpointLocation.path } + }; + await Promise.all([ + debugClient.launch(buildLaunchArgs('logMessage.py', false)), + debugClient.waitForEvent('initialized') + .then(() => debugClient.setBreakpointsRequest(breakpointArgs)) + .then(() => debugClient.configurationDoneRequest()) + .then(() => debugClient.threadsRequest()), + debugClient.waitForEvent('thread') + .then(() => debugClient.threadsRequest()), + debugClient.assertOutput('stdout', 'Sum of 1 and 2 is 3'), + debugClient.waitForEvent('terminated') + ]); + }); + test('Test Hit Count Breakpoints', async function () { + if (debuggerType !== 'pythonExperimental') { + return this.skip(); + } + + const breakpointLocation = { path: path.join(debugFilesPath, 'loopyTest.py'), column: 1, line: 2 }; + const breakpointArgs: DebugProtocol.SetBreakpointsArguments = { + lines: [breakpointLocation.line], + breakpoints: [{ line: breakpointLocation.line, column: breakpointLocation.column, hitCondition: '5' }], + source: { path: breakpointLocation.path } + }; + await Promise.all([ + debugClient.launch(buildLaunchArgs('loopyTest.py', false)), + debugClient.waitForEvent('initialized') + .then(() => debugClient.setBreakpointsRequest(breakpointArgs)) + .then(() => debugClient.configurationDoneRequest()) + .then(() => debugClient.threadsRequest()), + debugClient.waitForEvent('thread'), + debugClient.assertStoppedLocation('breakpoint', breakpointLocation) + ]); + + //Do not remove this, this is required to ensure PTVSD is ready to accept other requests. + await debugClient.threadsRequest(); + const evaluateResponse = await debugClient.evaluateRequest({ context: 'repl', expression: 'i', frameId: 1 }); + expect(evaluateResponse.body.type).to.equal('int'); + expect(evaluateResponse.body.result).to.equal('4'); + await continueDebugging(debugClient); + }); }); }); diff --git a/src/test/debugger/module.test.ts b/src/test/debugger/module.test.ts new file mode 100644 index 000000000000..2e6839bdc9ef --- /dev/null +++ b/src/test/debugger/module.test.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-suspicious-comment max-func-body-length no-invalid-this no-var-requires no-require-imports no-any + +import * as path from 'path'; +import { DebugClient } from 'vscode-debugadapter-testsupport'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { noop } from '../../client/common/core.utils'; +import { PTVSD_PATH } from '../../client/debugger/Common/constants'; +import { DebugOptions, LaunchRequestArguments } from '../../client/debugger/Common/Contracts'; +import { PYTHON_PATH, sleep } from '../common'; +import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; +import { createDebugAdapter } from './utils'; + +const workspaceDirectory = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5'); +const debuggerType = 'pythonExperimental'; +suite(`Module Debugging - Misc tests: ${debuggerType}`, () => { + let debugClient: DebugClient; + setup(async function () { + if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { + this.skip(); + } + const coverageDirectory = path.join(EXTENSION_ROOT_DIR, 'debug_coverage_module'); + debugClient = await createDebugAdapter(coverageDirectory); + }); + teardown(async () => { + // Wait for a second before starting another test (sometimes, sockets take a while to get closed). + await sleep(1000); + try { + await debugClient.stop().catch(noop); + // tslint:disable-next-line:no-empty + } catch (ex) { } + await sleep(1000); + }); + function buildLaunchArgs(): LaunchRequestArguments { + // tslint:disable-next-line:no-unnecessary-local-variable + const options: LaunchRequestArguments = { + module: 'mymod', + program: '', + cwd: workspaceDirectory, + debugOptions: [DebugOptions.RedirectOutput], + pythonPath: PYTHON_PATH, + args: [], + env: { PYTHONPATH: `${PTVSD_PATH}` }, + envFile: '', + logToFile: false, + type: debuggerType + }; + + return options; + } + + test('Test stdout output', async () => { + await Promise.all([ + debugClient.configurationSequence(), + debugClient.launch(buildLaunchArgs()), + debugClient.waitForEvent('initialized'), + debugClient.assertOutput('stdout', 'Hello world!'), + debugClient.waitForEvent('exited'), + debugClient.waitForEvent('terminated') + ]); + }); +}); diff --git a/src/test/debugger/portAndHost.test.ts b/src/test/debugger/portAndHost.test.ts index 7bbee91fb728..32fdf3941a74 100644 --- a/src/test/debugger/portAndHost.test.ts +++ b/src/test/debugger/portAndHost.test.ts @@ -8,8 +8,10 @@ import * as net from 'net'; import * as path from 'path'; import { DebugClient } from 'vscode-debugadapter-testsupport'; import { noop } from '../../client/common/core.utils'; -import { LaunchRequestArguments } from '../../client/debugger/Common/Contracts'; +import { DebugOptions, LaunchRequestArguments } from '../../client/debugger/Common/Contracts'; +import { PYTHON_PATH } from '../common'; import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; +import { DEBUGGER_TIMEOUT } from './common/constants'; use(chaiAsPromised); @@ -31,6 +33,7 @@ const EXPERIMENTAL_DEBUG_ADAPTER = path.join(__dirname, '..', '..', 'client', 'd } await new Promise(resolve => setTimeout(resolve, 1000)); debugClient = new DebugClient('node', testAdapterFilePath, debuggerType); + debugClient.defaultTimeout = DEBUGGER_TIMEOUT; await debugClient.start(); }); teardown(async () => { @@ -47,8 +50,8 @@ const EXPERIMENTAL_DEBUG_ADAPTER = path.join(__dirname, '..', '..', 'client', 'd program: path.join(debugFilesPath, pythonFile), cwd: debugFilesPath, stopOnEntry, - debugOptions: ['RedirectOutput'], - pythonPath: 'python', + debugOptions: [DebugOptions.RedirectOutput], + pythonPath: PYTHON_PATH, args: [], envFile: '', host, port, diff --git a/src/test/debugger/run.test.ts b/src/test/debugger/run.test.ts new file mode 100644 index 000000000000..cca794c8e345 --- /dev/null +++ b/src/test/debugger/run.test.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-invalid-this no-require-imports no-require-imports no-var-requires + +import { expect } from 'chai'; +import * as path from 'path'; +import { DebugClient } from 'vscode-debugadapter-testsupport'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { noop } from '../../client/common/core.utils'; +import { PTVSD_PATH } from '../../client/debugger/Common/constants'; +import { DebugOptions, LaunchRequestArguments } from '../../client/debugger/Common/Contracts'; +import { PYTHON_PATH, sleep } from '../common'; +import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; +import { createDebugAdapter } from './utils'; + +const isProcessRunning = require('is-running') as (number) => boolean; + +const debugFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'debugging'); +const debuggerType = 'pythonExperimental'; +suite('Run without Debugging', () => { + let debugClient: DebugClient; + setup(async function () { + if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { + this.skip(); + } + await new Promise(resolve => setTimeout(resolve, 1000)); + const coverageDirectory = path.join(EXTENSION_ROOT_DIR, `debug_coverage_nodebug${this.currentTest.title}`); + debugClient = await createDebugAdapter(coverageDirectory); + }); + teardown(async () => { + // Wait for a second before starting another test (sometimes, sockets take a while to get closed). + await sleep(1000); + try { + await debugClient.stop().catch(noop); + // tslint:disable-next-line:no-empty + } catch (ex) { } + await sleep(1000); + }); + function buildLauncArgs(pythonFile: string, stopOnEntry: boolean = false): LaunchRequestArguments { + // tslint:disable-next-line:no-unnecessary-local-variable + const options: LaunchRequestArguments = { + program: path.join(debugFilesPath, pythonFile), + cwd: debugFilesPath, + stopOnEntry, + noDebug: true, + debugOptions: [DebugOptions.RedirectOutput], + pythonPath: PYTHON_PATH, + args: [], + env: { PYTHONPATH: PTVSD_PATH }, + envFile: '', + logToFile: false, + type: debuggerType + }; + + return options; + } + + test('Should run program to the end', async () => { + await Promise.all([ + debugClient.configurationSequence(), + debugClient.launch(buildLauncArgs('simplePrint.py', false)), + debugClient.waitForEvent('initialized'), + debugClient.waitForEvent('terminated') + ]); + }); + test('test stderr output for Python', async () => { + await Promise.all([ + debugClient.configurationSequence(), + debugClient.launch(buildLauncArgs('stdErrOutput.py', false)), + debugClient.waitForEvent('initialized'), + debugClient.assertOutput('stderr', 'error output'), + debugClient.waitForEvent('terminated') + ]); + }); + test('Test stdout output', async () => { + await Promise.all([ + debugClient.configurationSequence(), + debugClient.launch(buildLauncArgs('stdOutOutput.py', false)), + debugClient.waitForEvent('initialized'), + debugClient.assertOutput('stdout', 'normal output'), + debugClient.waitForEvent('terminated') + ]); + }); + test('Should kill python process when ending debug session', async () => { + const processIdOutput = new Promise(resolve => { + debugClient.on('output', (event: DebugProtocol.OutputEvent) => { + if (event.event === 'output' && event.body.category === 'stdout') { + resolve(parseInt(event.body.output.trim(), 10)); + } + }); + }); + await Promise.all([ + debugClient.configurationSequence(), + debugClient.launch(buildLauncArgs('sampleWithSleep.py', false)), + debugClient.waitForEvent('initialized'), + processIdOutput + ]); + + const processId = await processIdOutput; + expect(processId).to.be.greaterThan(0, 'Invalid process id'); + + await debugClient.stop(); + await sleep(1000); + + // Confirm the process is dead + expect(isProcessRunning(processId)).to.be.equal(false, 'Python program is still alive'); + }); +}); diff --git a/src/test/debugger/utils.ts b/src/test/debugger/utils.ts new file mode 100644 index 000000000000..adc4a532221a --- /dev/null +++ b/src/test/debugger/utils.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-http-string + +import { expect } from 'chai'; +import * as path from 'path'; +import * as request from 'request'; +import { DebugClient } from 'vscode-debugadapter-testsupport'; +import { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { IS_WINDOWS } from '../../client/common/platform/constants'; +import { DEBUGGER_TIMEOUT } from './common/constants'; +import { DebugClientEx } from './debugClient'; + +const testAdapterFilePath = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'debugger', 'mainV2.js'); +const debuggerType = 'pythonExperimental'; + +/** + * Creates the debug adapter. + * We do not need to support code coverage on AppVeyor, lets use the standard test adapter. + * @returns {DebugClient} + */ +export async function createDebugAdapter(coverageDirectory: string): Promise { + await new Promise(resolve => setTimeout(resolve, 1000)); + let debugClient: DebugClient; + if (IS_WINDOWS) { + debugClient = new DebugClient('node', testAdapterFilePath, debuggerType); + } else { + debugClient = new DebugClientEx(testAdapterFilePath, debuggerType, coverageDirectory, { cwd: EXTENSION_ROOT_DIR }); + } + debugClient.defaultTimeout = DEBUGGER_TIMEOUT; + await debugClient.start(); + return debugClient; +} + +export async function continueDebugging(debugClient: DebugClient) { + const threads = await debugClient.threadsRequest(); + expect(threads).to.be.not.equal(undefined, 'no threads response'); + expect(threads.body.threads).to.be.lengthOf(1); + + await debugClient.continueRequest({ threadId: threads.body.threads[0].id }); +} + +export type ExpectedVariable = { type: string; name: string; value: string }; +export async function validateVariablesInFrame(debugClient: DebugClient, + stackTrace: DebugProtocol.StackTraceResponse, + expectedVariables: ExpectedVariable[], numberOfScopes?: number) { + + const frameId = stackTrace.body.stackFrames[0].id; + + const scopes = await debugClient.scopesRequest({ frameId }); + if (numberOfScopes) { + expect(scopes.body.scopes).of.length(1, 'Incorrect number of scopes'); + } + + const variablesReference = scopes.body.scopes[0].variablesReference; + const variables = await debugClient.variablesRequest({ variablesReference }); + + for (const expectedVariable of expectedVariables) { + const variable = variables.body.variables.find(item => item.name === expectedVariable.name)!; + expect(variable).to.be.not.equal('undefined', `variable '${expectedVariable.name}' is undefined`); + expect(variable.type).to.be.equal(expectedVariable.type); + expect(variable.value).to.be.equal(expectedVariable.value); + } +} +export function makeHttpRequest(uri: string): Promise { + return new Promise((resolve, reject) => { + request.get(uri, (error: any, response: request.Response, body: any) => { + if (error) { + return reject(error); + } + if (response.statusCode !== 200) { + reject(new Error(`Status code = ${response.statusCode}`)); + } else { + resolve(body.toString()); + } + }); + }); +} +export async function hitHttpBreakpoint(debugClient: DebugClient, uri: string, file: string, line: number): Promise<[DebugProtocol.StackTraceResponse, Promise]> { + const breakpointLocation = { path: file, column: 1, line }; + await debugClient.setBreakpointsRequest({ + lines: [breakpointLocation.line], + breakpoints: [{ line: breakpointLocation.line, column: breakpointLocation.column }], + source: { path: breakpointLocation.path } + }); + + // Make the request, we want the breakpoint to be hit. + const breakpointPromise = debugClient.assertStoppedLocation('breakpoint', breakpointLocation); + const httpResult = makeHttpRequest(uri); + return [await breakpointPromise, httpResult]; +} diff --git a/src/test/debugger/web.framework.test.ts b/src/test/debugger/web.framework.test.ts new file mode 100644 index 000000000000..1528e6f2ad5f --- /dev/null +++ b/src/test/debugger/web.framework.test.ts @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-suspicious-comment max-func-body-length no-invalid-this no-var-requires no-require-imports no-any no-http-string no-string-literal no-console + +import { expect } from 'chai'; +import * as getFreePort from 'get-port'; +import * as path from 'path'; +import { DebugClient } from 'vscode-debugadapter-testsupport'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { noop } from '../../client/common/core.utils'; +import { StopWatch } from '../../client/common/stopWatch'; +import { DebugOptions, LaunchRequestArguments } from '../../client/debugger/Common/Contracts'; +import { PYTHON_PATH, sleep } from '../common'; +import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; +import { DEBUGGER_TIMEOUT } from './common/constants'; +import { continueDebugging, createDebugAdapter, ExpectedVariable, hitHttpBreakpoint, makeHttpRequest, validateVariablesInFrame } from './utils'; + +let testCounter = 0; +const debuggerType = 'pythonExperimental'; +suite(`Django and Flask Debugging: ${debuggerType}`, () => { + let debugClient: DebugClient; + setup(async function () { + if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { + this.skip(); + } + this.timeout(5 * DEBUGGER_TIMEOUT); + const coverageDirectory = path.join(EXTENSION_ROOT_DIR, `debug_coverage_django_flask${testCounter += 1}`); + debugClient = await createDebugAdapter(coverageDirectory); + }); + teardown(async () => { + // Wait for a second before starting another test (sometimes, sockets take a while to get closed). + await sleep(1000); + try { + await debugClient.stop().catch(noop); + // tslint:disable-next-line:no-empty + } catch (ex) { } + await sleep(1000); + }); + function buildLaunchArgs(workspaceDirectory: string): LaunchRequestArguments { + const env = {}; + // tslint:disable-next-line:no-string-literal + env['PYTHONPATH'] = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd'); + + // tslint:disable-next-line:no-unnecessary-local-variable + const options: LaunchRequestArguments = { + cwd: workspaceDirectory, + program: '', + debugOptions: [DebugOptions.RedirectOutput], + pythonPath: PYTHON_PATH, + args: [], + env, + envFile: '', + logToFile: false, + type: debuggerType + }; + + return options; + } + async function buildFlaskLaunchArgs(workspaceDirectory: string) { + const port = await getFreePort({ host: 'localhost' }); + const options = buildLaunchArgs(workspaceDirectory); + + options.env!['FLASK_APP'] = 'run.py'; + options.module = 'flask'; + options.debugOptions = [DebugOptions.RedirectOutput, DebugOptions.Jinja]; + options.args = [ + 'run', + '--no-debugger', + '--no-reload', + '--without-threads', + '--port', + `${port}` + ]; + + return { options, port }; + } + async function buildDjangoLaunchArgs(workspaceDirectory: string) { + const port = await getFreePort({ host: 'localhost' }); + const options = buildLaunchArgs(workspaceDirectory); + + options.program = path.join(workspaceDirectory, 'manage.py'); + options.debugOptions = [DebugOptions.RedirectOutput, DebugOptions.Django]; + options.args = [ + 'runserver', + '--noreload', + '--nothreading', + `${port}` + ]; + + return { options, port }; + } + + async function waitForWebServerToStart(url: string): Promise { + return new Promise((resolve, reject) => { + let maxTries = 50; + const timeout = 10_000; + const start = new StopWatch(); + const fn = () => { + makeHttpRequest(url) + .then(resolve) + .catch(ex => { + maxTries -= 1; + if (maxTries === 0 || start.elapsedTime >= timeout) { + reject(ex); + } else { + setTimeout(fn, 100); + } + }); + }; + fn(); + }); + } + + async function testTemplateDebugging(launchArgs: LaunchRequestArguments, port: number, viewFile: string, viewLine: number, templateFile: string, templateLine: number) { + await Promise.all([ + debugClient.configurationSequence(), + debugClient.launch(launchArgs), + debugClient.waitForEvent('initialized'), + debugClient.waitForEvent('process'), + debugClient.waitForEvent('thread') + ]); + + const url = `http://localhost:${port}`; + + await waitForWebServerToStart(url); + + const httpResult = await makeHttpRequest(url); + expect(httpResult).to.contain('Hello this_is_a_value_from_server'); + expect(httpResult).to.contain('Hello this_is_another_value_from_server'); + + await hitHttpBreakpoint(debugClient, url, viewFile, viewLine); + + await continueDebugging(debugClient); + await debugClient.setBreakpointsRequest({ breakpoints: [], lines: [], source: { path: viewFile } }); + + // Template debugging. + const [stackTrace, htmlResultPromise] = await hitHttpBreakpoint(debugClient, url, templateFile, templateLine); + + // Wait for breakpoint to hit + const expectedVariables: ExpectedVariable[] = [ + { name: 'value_from_server', type: 'str', value: '\'this_is_a_value_from_server\'' }, + { name: 'another_value_from_server', type: 'str', value: '\'this_is_another_value_from_server\'' } + ]; + await validateVariablesInFrame(debugClient, stackTrace, expectedVariables, 1); + + await debugClient.setBreakpointsRequest({ breakpoints: [], lines: [], source: { path: templateFile } }); + await continueDebugging(debugClient); + + const htmlResult = await htmlResultPromise; + expect(htmlResult).to.contain('Hello this_is_a_value_from_server'); + expect(htmlResult).to.contain('Hello this_is_another_value_from_server'); + } + + test('Test Flask Route and Template debugging', async () => { + const workspaceDirectory = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'flaskApp'); + const { options, port } = await buildFlaskLaunchArgs(workspaceDirectory); + + await testTemplateDebugging(options, port, + path.join(workspaceDirectory, 'run.py'), 7, + path.join(workspaceDirectory, 'templates', 'index.html'), 6); + }); + + test('Test Django Route and Template debugging', async () => { + const workspaceDirectory = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'djangoApp'); + const { options, port } = await buildDjangoLaunchArgs(workspaceDirectory); + + await testTemplateDebugging(options, port, + path.join(workspaceDirectory, 'home', 'views.py'), 10, + path.join(workspaceDirectory, 'home', 'templates', 'index.html'), 6); + }); +}); diff --git a/src/test/definitions/hover.test.ts b/src/test/definitions/hover.jedi.test.ts similarity index 93% rename from src/test/definitions/hover.test.ts rename to src/test/definitions/hover.jedi.test.ts index ba194a902446..103e78f4a7cc 100644 --- a/src/test/definitions/hover.test.ts +++ b/src/test/definitions/hover.jedi.test.ts @@ -1,276 +1,283 @@ -import * as assert from 'assert'; -import { EOL } from 'os'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; -import { normalizeMarkedString } from '../textUtils'; - -const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); -const hoverPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'hover'); -const fileOne = path.join(autoCompPath, 'one.py'); -const fileThree = path.join(autoCompPath, 'three.py'); -const fileEncoding = path.join(autoCompPath, 'four.py'); -const fileEncodingUsed = path.join(autoCompPath, 'five.py'); -const fileHover = path.join(autoCompPath, 'hoverTest.py'); -const fileStringFormat = path.join(hoverPath, 'stringFormat.py'); - -// tslint:disable-next-line:max-func-body-length -suite('Hover Definition', () => { - suiteSetup(initialize); - setup(initializeTest); - suiteTeardown(closeActiveWindows); - teardown(closeActiveWindows); - - test('Method', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileOne).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(30, 5); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '30,4', 'Start position is incorrect'); - assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '30,11', 'End position is incorrect'); - assert.equal(def[0].contents.length, 1, 'Invalid content items'); - // tslint:disable-next-line:prefer-template - const expectedContent = '```python' + EOL + 'def method1()' + EOL + '```' + EOL + 'This is method1'; - assert.equal(normalizeMarkedString(def[0].contents[0]), expectedContent, 'function signature incorrect'); - }).then(done, done); - }); - - test('Across files', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileThree).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(1, 12); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '1,9', 'Start position is incorrect'); - assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '1,12', 'End position is incorrect'); - // tslint:disable-next-line:prefer-template - assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + 'def fun()' + EOL + '```' + EOL + 'This is fun', 'Invalid conents'); - }).then(done, done); - }); - - test('With Unicode Characters', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileEncoding).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(25, 6); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '25,4', 'Start position is incorrect'); - assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '25,7', 'End position is incorrect'); - // tslint:disable-next-line:prefer-template - assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + 'def bar()' + EOL + '```' + EOL + - '说明 - keep this line, it works' + EOL + 'delete following line, it works' + - EOL + '如果存在需要等待审批或正在执行的任务,将不刷新页面', 'Invalid conents'); - }).then(done, done); - }); - - test('Across files with Unicode Characters', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileEncodingUsed).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(1, 11); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '1,5', 'Start position is incorrect'); - assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '1,16', 'End position is incorrect'); - // tslint:disable-next-line:prefer-template - assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + - 'def showMessage()' + EOL + - '```' + EOL + - 'Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи. ' + EOL + - 'Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку.', 'Invalid conents'); - }).then(done, done); - }); - - test('Nothing for keywords (class)', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileOne).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(5, 1); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(def => { - assert.equal(def!.length, 0, 'Definition length is incorrect'); - }).then(done, done); - }); - - test('Nothing for keywords (for)', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileHover).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(3, 1); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(def => { - assert.equal(def!.length, 0, 'Definition length is incorrect'); - }).then(done, done); - }); - - test('Highlighting Class', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileHover).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(11, 15); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '11,12', 'Start position is incorrect'); - assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '11,18', 'End position is incorrect'); - // tslint:disable-next-line:prefer-template - const documentation = '```python' + EOL + - 'class Random(x=None)' + EOL + - '```' + EOL + - 'Random number generator base class used by bound module functions.' + EOL + - '' + EOL + - 'Used to instantiate instances of Random to get generators that don\'t' + EOL + - 'share state.' + EOL + - '' + EOL + - 'Class Random can also be subclassed if you want to use a different basic' + EOL + - 'generator of your own devising: in that case, override the following' + EOL + - 'methods: random(), seed(), getstate(), and setstate().' + EOL + - 'Optionally, implement a getrandbits() method so that randrange()' + EOL + - 'can cover arbitrarily large ranges.'; - - assert.equal(normalizeMarkedString(def[0].contents[0]), documentation, 'Invalid conents'); - }).then(done, done); - }); - - test('Highlight Method', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileHover).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(12, 10); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '12,5', 'Start position is incorrect'); - assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '12,12', 'End position is incorrect'); - // tslint:disable-next-line:prefer-template - assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + - 'def randint(a, b)' + EOL + - '```' + EOL + - 'Return random integer in range [a, b], including both end points.', 'Invalid conents'); - }).then(done, done); - }); - - test('Highlight Function', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileHover).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(8, 14); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '8,11', 'Start position is incorrect'); - assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '8,15', 'End position is incorrect'); - // tslint:disable-next-line:prefer-template - assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + - 'def acos(x)' + EOL + - '```' + EOL + - 'Return the arc cosine (measured in radians) of x.', 'Invalid conents'); - }).then(done, done); - }); - - test('Highlight Multiline Method Signature', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileHover).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(14, 14); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '14,9', 'Start position is incorrect'); - assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '14,15', 'End position is incorrect'); - // tslint:disable-next-line:prefer-template - assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + - 'class Thread(group=None, target=None, name=None, args=(), kwargs=None, verbose=None)' + EOL + - '```' + EOL + - 'A class that represents a thread of control.' + EOL + - '' + EOL + - 'This class can be safely subclassed in a limited fashion.', 'Invalid content items'); - }).then(done, done); - }); - - test('Variable', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileHover).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(6, 2); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(def[0].contents.length, 1, 'Only expected one result'); - const contents = normalizeMarkedString(def[0].contents[0]); - if (contents.indexOf('```python') === -1) { - assert.fail(contents, '', 'First line is incorrect', 'compare'); - } - if (contents.indexOf('rnd: Random') === -1) { - assert.fail(contents, '', 'Variable name or type are missing', 'compare'); - } - }).then(done, done); - }); - - test('format().capitalize()', async () => { - const textDocument = await vscode.workspace.openTextDocument(fileStringFormat); - await vscode.window.showTextDocument(textDocument); - const position = new vscode.Position(5, 41); - const def = (await vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position))!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(def[0].contents.length, 1, 'Only expected one result'); - const contents = normalizeMarkedString(def[0].contents[0]); - if (contents.indexOf('def capitalize') === -1) { - assert.fail(contents, '', '\'def capitalize\' is missing', 'compare'); - } - if (contents.indexOf('Return a capitalized version of S') === -1 && - contents.indexOf('Return a copy of the string S with only its first character') === -1) { - assert.fail(contents, '', '\'Return a capitalized version of S/Return a copy of the string S with only its first character\' message missing', 'compare'); - } - }); -}); +import * as assert from 'assert'; +import { EOL } from 'os'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { IsLanguageServerTest } from '../constants'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { normalizeMarkedString } from '../textUtils'; + +const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); +const hoverPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'hover'); +const fileOne = path.join(autoCompPath, 'one.py'); +const fileThree = path.join(autoCompPath, 'three.py'); +const fileEncoding = path.join(autoCompPath, 'four.py'); +const fileEncodingUsed = path.join(autoCompPath, 'five.py'); +const fileHover = path.join(autoCompPath, 'hoverTest.py'); +const fileStringFormat = path.join(hoverPath, 'functionHover.py'); + +// tslint:disable-next-line:max-func-body-length +suite('Hover Definition (Jedi)', () => { + suiteSetup(async function () { + if (IsLanguageServerTest()) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + test('Method', done => { + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileOne).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(30, 5); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(result => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '30,4', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '30,11', 'End position is incorrect'); + assert.equal(def[0].contents.length, 1, 'Invalid content items'); + // tslint:disable-next-line:prefer-template + const expectedContent = '```python' + EOL + 'def method1()' + EOL + '```' + EOL + 'This is method1'; + assert.equal(normalizeMarkedString(def[0].contents[0]), expectedContent, 'function signature incorrect'); + }).then(done, done); + }); + + test('Across files', done => { + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileThree).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(1, 12); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(result => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '1,9', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '1,12', 'End position is incorrect'); + // tslint:disable-next-line:prefer-template + assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + 'def fun()' + EOL + '```' + EOL + 'This is fun', 'Invalid conents'); + }).then(done, done); + }); + + test('With Unicode Characters', done => { + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileEncoding).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(25, 6); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(result => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '25,4', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '25,7', 'End position is incorrect'); + // tslint:disable-next-line:prefer-template + assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + 'def bar()' + EOL + '```' + EOL + + '说明 - keep this line, it works' + EOL + 'delete following line, it works' + + EOL + '如果存在需要等待审批或正在执行的任务,将不刷新页面', 'Invalid conents'); + }).then(done, done); + }); + + test('Across files with Unicode Characters', done => { + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileEncodingUsed).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(1, 11); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(result => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '1,5', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '1,16', 'End position is incorrect'); + // tslint:disable-next-line:prefer-template + assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + + 'def showMessage()' + EOL + + '```' + EOL + + 'Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи. ' + EOL + + 'Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку.', 'Invalid conents'); + }).then(done, done); + }); + + test('Nothing for keywords (class)', done => { + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileOne).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(5, 1); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(def => { + assert.equal(def!.length, 0, 'Definition length is incorrect'); + }).then(done, done); + }); + + test('Nothing for keywords (for)', done => { + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileHover).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(3, 1); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(def => { + assert.equal(def!.length, 0, 'Definition length is incorrect'); + }).then(done, done); + }); + + test('Highlighting Class', done => { + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileHover).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(11, 15); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(result => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '11,12', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '11,18', 'End position is incorrect'); + // tslint:disable-next-line:prefer-template + const documentation = '```python' + EOL + + 'class Random(x=None)' + EOL + + '```' + EOL + + 'Random number generator base class used by bound module functions.' + EOL + + '' + EOL + + 'Used to instantiate instances of Random to get generators that don\'t' + EOL + + 'share state.' + EOL + + '' + EOL + + 'Class Random can also be subclassed if you want to use a different basic' + EOL + + 'generator of your own devising: in that case, override the following' + EOL + + 'methods: random(), seed(), getstate(), and setstate().' + EOL + + 'Optionally, implement a getrandbits() method so that randrange()' + EOL + + 'can cover arbitrarily large ranges.'; + + assert.equal(normalizeMarkedString(def[0].contents[0]), documentation, 'Invalid conents'); + }).then(done, done); + }); + + test('Highlight Method', done => { + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileHover).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(12, 10); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(result => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '12,5', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '12,12', 'End position is incorrect'); + // tslint:disable-next-line:prefer-template + assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + + 'def randint(a, b)' + EOL + + '```' + EOL + + 'Return random integer in range [a, b], including both end points.', 'Invalid conents'); + }).then(done, done); + }); + + test('Highlight Function', done => { + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileHover).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(8, 14); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(result => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '8,11', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '8,15', 'End position is incorrect'); + // tslint:disable-next-line:prefer-template + assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + + 'def acos(x)' + EOL + + '```' + EOL + + 'Return the arc cosine (measured in radians) of x.', 'Invalid conents'); + }).then(done, done); + }); + + test('Highlight Multiline Method Signature', done => { + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileHover).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(14, 14); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(result => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '14,9', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '14,15', 'End position is incorrect'); + // tslint:disable-next-line:prefer-template + assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + + 'class Thread(group=None, target=None, name=None, args=(), kwargs=None, verbose=None)' + EOL + + '```' + EOL + + 'A class that represents a thread of control.' + EOL + + '' + EOL + + 'This class can be safely subclassed in a limited fashion.', 'Invalid content items'); + }).then(done, done); + }); + + test('Variable', done => { + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileHover).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(6, 2); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(result => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(def[0].contents.length, 1, 'Only expected one result'); + const contents = normalizeMarkedString(def[0].contents[0]); + if (contents.indexOf('```python') === -1) { + assert.fail(contents, '', 'First line is incorrect', 'compare'); + } + if (contents.indexOf('rnd: Random') === -1) { + assert.fail(contents, '', 'Variable name or type are missing', 'compare'); + } + }).then(done, done); + }); + + test('Hover over method shows proper text.', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileStringFormat); + await vscode.window.showTextDocument(textDocument); + const position = new vscode.Position(8, 4); + const def = (await vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position))!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(def[0].contents.length, 1, 'Only expected one result'); + const contents = normalizeMarkedString(def[0].contents[0]); + if (contents.indexOf('def my_func') === -1) { + assert.fail(contents, '', '\'def my_func\' is missing', 'compare'); + } + if (contents.indexOf('This is a test.') === -1 && + contents.indexOf('It also includes this text, too.') === -1) { + assert.fail(contents, '', 'Expected custom function text missing', 'compare'); + } + }); +}); diff --git a/src/test/definitions/hover.ls.test.ts b/src/test/definitions/hover.ls.test.ts new file mode 100644 index 000000000000..43dc6c0fdb2a --- /dev/null +++ b/src/test/definitions/hover.ls.test.ts @@ -0,0 +1,240 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import '../../client/common/extensions'; +import { IsLanguageServerTest } from '../constants'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { normalizeMarkedString } from '../textUtils'; + +const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); +const hoverPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'hover'); +const fileOne = path.join(autoCompPath, 'one.py'); +const fileThree = path.join(autoCompPath, 'three.py'); +const fileEncoding = path.join(autoCompPath, 'four.py'); +const fileEncodingUsed = path.join(autoCompPath, 'five.py'); +const fileHover = path.join(autoCompPath, 'hoverTest.py'); +const fileStringFormat = path.join(hoverPath, 'stringFormat.py'); + +let textDocument: vscode.TextDocument; + +// tslint:disable-next-line:max-func-body-length +suite('Hover Definition (Language Server)', () => { + suiteSetup(async function () { + if (!IsLanguageServerTest()) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + async function openAndHover(file: string, line: number, character: number): Promise { + textDocument = await vscode.workspace.openTextDocument(file); + await vscode.window.showTextDocument(textDocument); + const position = new vscode.Position(line, character); + const result = await vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + return result ? result : []; + } + + test('Method', async () => { + const def = await openAndHover(fileOne, 30, 5); + assert.equal(def.length, 1, 'Definition length is incorrect'); + + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '30,0', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '30,11', 'End position is incorrect'); + assert.equal(def[0].contents.length, 1, 'Invalid content items'); + + const actual = normalizeMarkedString(def[0].contents[0]).splitLines(); + const expected = [ + 'obj.method1:', + '```python', + 'method method1 of one.Class1 objects', + '```', + 'This is method1' + ]; + verifySignatureLines(actual, expected); + }); + + test('Across files', async () => { + const def = await openAndHover(fileThree, 1, 12); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '1,0', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '1,12', 'End position is incorrect'); + + const actual = normalizeMarkedString(def[0].contents[0]).splitLines(); + const expected = [ + 'two.ct().fun:', + '```python', + 'method fun of two.ct objects', + '```', + 'This is fun' + ]; + verifySignatureLines(actual, expected); + }); + + test('With Unicode Characters', async () => { + const def = await openAndHover(fileEncoding, 25, 6); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '25,0', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '25,7', 'End position is incorrect'); + + const actual = normalizeMarkedString(def[0].contents[0]).splitLines(); + const expected = [ + '```python', + 'four.Foo.bar() -> bool', + 'declared in Foo', + '```', + '说明 - keep this line, it works', + 'delete following line, it works', + '如果存在需要等待审批或正在执行的任务,将不刷新页面' + ]; + verifySignatureLines(actual, expected); + }); + + test('Across files with Unicode Characters', async () => { + const def = await openAndHover(fileEncodingUsed, 1, 11); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '1,0', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '1,16', 'End position is incorrect'); + + const actual = normalizeMarkedString(def[0].contents[0]).splitLines(); + const expected = [ + '```python', + 'four.showMessage()', + '```', + 'Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи.', + 'Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку.' + ]; + verifySignatureLines(actual, expected); + }); + + test('Nothing for keywords (class)', async () => { + const def = await openAndHover(fileOne, 5, 1); + if (def.length > 0) { + const actual = normalizeMarkedString(def[0].contents[0]); + assert.equal(actual, '', 'Definition length is incorrect'); + } + }); + + test('Nothing for keywords (for)', async () => { + const def = await openAndHover(fileHover, 3, 1); + if (def.length > 0) { + const actual = normalizeMarkedString(def[0].contents[0]); + assert.equal(actual, '', 'Definition length is incorrect'); + } + }); + + test('Highlighting Class', async () => { + const def = await openAndHover(fileHover, 11, 15); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '11,7', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '11,18', 'End position is incorrect'); + + const actual = normalizeMarkedString(def[0].contents[0]).splitLines(); + const expected = [ + '```python', + 'class misc.Random(_random.Random)', + '```', + 'Random number generator base class used by bound module functions.', + 'Used to instantiate instances of Random to get generators that don\'t', + 'share state.', + 'Class Random can also be subclassed if you want to use a different basic', + 'generator of your own devising: in that case, override the following', + 'methods: random(), seed(), getstate(), and setstate().', + 'Optionally, implement a getrandbits() method so that randrange()', + 'can cover arbitrarily large ranges.' + ]; + verifySignatureLines(actual, expected); + }); + + test('Highlight Method', async () => { + const def = await openAndHover(fileHover, 12, 10); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '12,0', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '12,12', 'End position is incorrect'); + + const actual = normalizeMarkedString(def[0].contents[0]).splitLines(); + const expected = [ + 'rnd2.randint:', + '```python', + 'method randint of misc.Random objects -> int', + '```', + 'Return random integer in range [a, b], including both end points.' + ]; + verifySignatureLines(actual, expected); + }); + + test('Highlight Function', async () => { + const def = await openAndHover(fileHover, 8, 14); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '8,6', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '8,15', 'End position is incorrect'); + + const actual = normalizeMarkedString(def[0].contents[0]).splitLines(); + const expected = [ + '```python', + 'acos(x)', + '```', + 'acos(x)', + 'Return the arc cosine (measured in radians) of x.' + ]; + verifySignatureLines(actual, expected); + }); + + test('Highlight Multiline Method Signature', async () => { + const def = await openAndHover(fileHover, 14, 14); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '14,4', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '14,15', 'End position is incorrect'); + + const actual = normalizeMarkedString(def[0].contents[0]).splitLines(); + const expected = [ + '```python', + 'class misc.Thread(_Verbose)', + '```', + 'A class that represents a thread of control.', + 'This class can be safely subclassed in a limited fashion.' + ]; + verifySignatureLines(actual, expected); + }); + + test('Variable', async () => { + const def = await openAndHover(fileHover, 6, 2); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(def[0].contents.length, 1, 'Only expected one result'); + const contents = normalizeMarkedString(def[0].contents[0]); + if (contents.indexOf('Random') === -1) { + assert.fail(contents, '', 'Variable type is missing', 'compare'); + } + }); + + test('format().capitalize()', async function () { + // https://github.com/Microsoft/PTVS/issues/3868 + // tslint:disable-next-line:no-invalid-this + this.skip(); + const def = await openAndHover(fileStringFormat, 5, 41); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(def[0].contents.length, 1, 'Only expected one result'); + const contents = normalizeMarkedString(def[0].contents[0]); + if (contents.indexOf('capitalize') === -1) { + assert.fail(contents, '', '\'capitalize\' is missing', 'compare'); + } + if (contents.indexOf('Return a capitalized version of S') === -1 && + contents.indexOf('Return a copy of the string S with only its first character') === -1) { + assert.fail(contents, '', '\'Return a capitalized version of S/Return a copy of the string S with only its first character\' message missing', 'compare'); + } + }); + + function verifySignatureLines(actual: string[], expected: string[]) { + assert.equal(actual.length, expected.length, 'incorrect number of lines'); + for (let i = 0; i < actual.length; i += 1) { + actual[i] = actual[i].replace(new RegExp(' ', 'g'), ' '); + assert.equal(actual[i].trim(), expected[i], `signature line ${i + 1} is incorrect`); + } + } +}); diff --git a/src/test/definitions/navigation.test.ts b/src/test/definitions/navigation.test.ts new file mode 100644 index 000000000000..71fe9bc6a217 --- /dev/null +++ b/src/test/definitions/navigation.test.ts @@ -0,0 +1,134 @@ +// Licensed under the MIT License. + +'use strict'; +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { isLanguageServerTest } from '../../client/common/constants'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; + +const decoratorsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'definition', 'navigation'); +const fileDefinitions = path.join(decoratorsPath, 'definitions.py'); +const fileUsages = path.join(decoratorsPath, 'usages.py'); + +// tslint:disable-next-line:max-func-body-length +suite('Definition Navigation', () => { + suiteSetup(initialize); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + const assertFile = (expectedLocation: string, location: vscode.Uri) => { + const relLocation = vscode.workspace.asRelativePath(location); + const expectedRelLocation = vscode.workspace.asRelativePath(expectedLocation); + assert.equal(expectedRelLocation, relLocation, 'Position is in wrong file'); + }; + + const formatPosition = (position: vscode.Position) => { + return `${position.line},${position.character}`; + }; + + const assertRange = (expectedRange: vscode.Range, range: vscode.Range) => { + assert.equal(formatPosition(expectedRange.start), formatPosition(range.start), 'Start position is incorrect'); + assert.equal(formatPosition(expectedRange.end), formatPosition(range.end), 'End position is incorrect'); + }; + + const buildTest = (startFile: string, startPosition: vscode.Position, expectedFiles: string[], expectedRanges: vscode.Range[]) => { + return async () => { + const textDocument = await vscode.workspace.openTextDocument(startFile); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + + const locations = await vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, startPosition); + assert.equal(expectedFiles.length, locations!.length, 'Wrong number of results'); + + for (let i = 0; i < locations!.length; i += 1) { + assertFile(expectedFiles[i], locations![i].uri); + assertRange(expectedRanges[i], locations![i].range!); + } + }; + }; + + test('From own definition', buildTest( + fileDefinitions, + new vscode.Position(2, 6), + [fileDefinitions], + isLanguageServerTest() ? [new vscode.Range(2, 4, 2, 16)] : [new vscode.Range(2, 0, 11, 17)] + )); + + test('Nested function', buildTest( + fileDefinitions, + new vscode.Position(11, 16), + [fileDefinitions], + isLanguageServerTest() ? [new vscode.Range(6, 8, 6, 15)] : [new vscode.Range(6, 4, 10, 16)] + )); + + test('Decorator usage', buildTest( + fileDefinitions, + new vscode.Position(13, 1), + [fileDefinitions], + isLanguageServerTest() ? [new vscode.Range(2, 4, 2, 16)] : [new vscode.Range(2, 0, 11, 17)] + )); + + test('Function decorated by stdlib', buildTest( + fileDefinitions, + new vscode.Position(29, 6), + [fileDefinitions], + isLanguageServerTest() ? [new vscode.Range(21, 4, 21, 22)] : [new vscode.Range(21, 0, 27, 17)] + )); + + test('Function decorated by local decorator', buildTest( + fileDefinitions, + new vscode.Position(30, 6), + [fileDefinitions], + isLanguageServerTest() ? [new vscode.Range(14, 4, 14, 9)] : [new vscode.Range(14, 0, 18, 7)] + )); + + test('Module imported decorator usage', buildTest( + fileUsages, + new vscode.Position(3, 15), + [fileDefinitions], + isLanguageServerTest() ? [new vscode.Range(2, 4, 2, 16)] : [new vscode.Range(2, 0, 11, 17)] + )); + + test('Module imported function decorated by stdlib', buildTest( + fileUsages, + new vscode.Position(11, 19), + [fileDefinitions], + isLanguageServerTest() ? [new vscode.Range(21, 4, 21, 22)] : [new vscode.Range(21, 0, 27, 17)] + )); + + test('Module imported function decorated by local decorator', buildTest( + fileUsages, + new vscode.Position(12, 19), + [fileDefinitions], + isLanguageServerTest() ? [new vscode.Range(14, 4, 14, 9)] : [new vscode.Range(14, 0, 18, 7)] + )); + + test('Specifically imported decorator usage', buildTest( + fileUsages, + new vscode.Position(7, 1), + isLanguageServerTest() ? [fileDefinitions] : [fileDefinitions], + isLanguageServerTest() + ? [new vscode.Range(2, 4, 2, 16)] + : [new vscode.Range(2, 0, 11, 17)] + )); + + test('Specifically imported function decorated by stdlib', buildTest( + fileUsages, + new vscode.Position(14, 6), + isLanguageServerTest() ? [fileDefinitions] : [fileDefinitions], + isLanguageServerTest() + ? [new vscode.Range(21, 4, 21, 22)] + : [new vscode.Range(21, 0, 27, 17)] + )); + + test('Specifically imported function decorated by local decorator', buildTest( + fileUsages, + new vscode.Position(15, 6), + isLanguageServerTest() ? [fileDefinitions] : [fileDefinitions], + isLanguageServerTest() + ? [new vscode.Range(14, 4, 14, 9)] + : [new vscode.Range(14, 0, 18, 7)] + )); +}); diff --git a/src/test/definitions/parallel.test.ts b/src/test/definitions/parallel.jedi.test.ts similarity index 87% rename from src/test/definitions/parallel.test.ts rename to src/test/definitions/parallel.jedi.test.ts index 535587ba5e03..e2ed7c814d61 100644 --- a/src/test/definitions/parallel.test.ts +++ b/src/test/definitions/parallel.jedi.test.ts @@ -1,44 +1,50 @@ -import * as assert from 'assert'; -import { EOL } from 'os'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { Hover } from 'vscode'; -import { IS_WINDOWS } from '../../client/common/platform/constants'; -import { closeActiveWindows, initialize } 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); - - 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'); - const expectedPath = IS_WINDOWS ? fileOne.toUpperCase() : fileOne; - const actualPath = IS_WINDOWS ? codeDef![0].uri.fsPath.toUpperCase() : codeDef![0].uri.fsPath; - assert.equal(actualPath, expectedPath, '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'); - // tslint:disable-next-line:prefer-template - const expectedContent = '```python' + EOL + 'def method1()' + EOL + '```' + EOL + 'This is method1'; - assert.equal(normalizeMarkedString(hoverDef![0].contents[0]), expectedContent, 'function signature incorrect'); - }); -}); +import * as assert from 'assert'; +import { EOL } from 'os'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { IS_WINDOWS } from '../../client/common/platform/constants'; +import { IsLanguageServerTest } from '../constants'; +import { closeActiveWindows, initialize } 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 (Jedi)', () => { + suiteSetup(async function () { + if (IsLanguageServerTest()) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + test('All three together', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileOne); + + 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'); + const expectedPath = IS_WINDOWS ? fileOne.toUpperCase() : fileOne; + const actualPath = IS_WINDOWS ? codeDef![0].uri.fsPath.toUpperCase() : codeDef![0].uri.fsPath; + assert.equal(actualPath, expectedPath, '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'); + // tslint:disable-next-line:prefer-template + 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/definitions/parallel.ls.test.ts b/src/test/definitions/parallel.ls.test.ts new file mode 100644 index 000000000000..f54d563c3b06 --- /dev/null +++ b/src/test/definitions/parallel.ls.test.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { EOL } from 'os'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { IS_WINDOWS } from '../../client/common/platform/constants'; +import { IsLanguageServerTest } from '../constants'; +import { closeActiveWindows, initialize } 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 (Language Server)', () => { + suiteSetup(async function () { + // https://github.com/Microsoft/vscode-python/issues/1061 + // tslint:disable-next-line:no-invalid-this + this.skip(); + + if (!IsLanguageServerTest()) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + test('All three together', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileOne); + + 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'); + const expectedPath = IS_WINDOWS ? fileOne.toUpperCase() : fileOne; + const actualPath = IS_WINDOWS ? codeDef![0].uri.fsPath.toUpperCase() : codeDef![0].uri.fsPath; + assert.equal(actualPath, expectedPath, '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'); + // tslint:disable-next-line:prefer-template + 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 fb74ecb0e522..3fd753f2222b 100644 --- a/src/test/format/extension.format.test.ts +++ b/src/test/format/extension.format.test.ts @@ -1,26 +1,28 @@ import * as fs from 'fs-extra'; import * as path from 'path'; -import * as vscode from 'vscode'; -import { CancellationTokenSource, Uri } from 'vscode'; -import { IProcessService, IPythonExecutionFactory } from '../../client/common/process/types'; +import { CancellationTokenSource, Position, Uri, window, workspace } from 'vscode'; +import { IProcessServiceFactory, IPythonExecutionFactory } from '../../client/common/process/types'; import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter'; +import { BlackFormatter } from '../../client/formatters/blackFormatter'; import { YapfFormatter } from '../../client/formatters/yapfFormatter'; +import { PythonVersionInformation } from '../../client/unittests/common/types'; import { closeActiveWindows, initialize, initializeTest } from '../initialize'; import { MockProcessService } from '../mocks/proc'; import { compareFiles } from '../textUtils'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; -const ch = vscode.window.createOutputChannel('Tests'); +const ch = window.createOutputChannel('Tests'); const formatFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); const workspaceRootPath = path.join(__dirname, '..', '..', '..', 'src', 'test'); const originalUnformattedFile = path.join(formatFilesPath, 'fileToFormat.py'); const autoPep8FileToFormat = path.join(formatFilesPath, 'autoPep8FileToFormat.py'); -const autoPep8FileToAutoFormat = path.join(formatFilesPath, 'autoPep8FileToAutoFormat.py'); +const blackFileToFormat = path.join(formatFilesPath, 'blackFileToFormat.py'); +const blackReferenceFile = path.join(formatFilesPath, 'blackFileReference.py'); const yapfFileToFormat = path.join(formatFilesPath, 'yapfFileToFormat.py'); -const yapfFileToAutoFormat = path.join(formatFilesPath, 'yapfFileToAutoFormat.py'); let formattedYapf = ''; +let formattedBlack = ''; let formattedAutoPep8 = ''; // tslint:disable-next-line:max-func-body-length @@ -30,16 +32,30 @@ suite('Formatting', () => { suiteSetup(async () => { await initialize(); initializeDI(); - [autoPep8FileToFormat, autoPep8FileToAutoFormat, yapfFileToFormat, yapfFileToAutoFormat].forEach(file => { + [autoPep8FileToFormat, blackFileToFormat, blackReferenceFile, yapfFileToFormat].forEach(file => { fs.copySync(originalUnformattedFile, file, { overwrite: true }); }); fs.ensureDirSync(path.dirname(autoPep8FileToFormat)); - const pythonProcess = await ioc.serviceContainer.get(IPythonExecutionFactory).create(Uri.file(workspaceRootPath)); + const pythonProcess = await ioc.serviceContainer.get(IPythonExecutionFactory).create({ resource: Uri.file(workspaceRootPath) }); + const py2 = await ioc.getPythonMajorVersion(Uri.parse(originalUnformattedFile)) === 2; const yapf = pythonProcess.execModule('yapf', [originalUnformattedFile], { cwd: workspaceRootPath }); const autoPep8 = pythonProcess.execModule('autopep8', [originalUnformattedFile], { cwd: workspaceRootPath }); - await Promise.all([yapf, autoPep8]).then(formattedResults => { + const formatters = [yapf, autoPep8]; + // When testing against 3.5 and older, this will break. + if (!py2) { + // Black doesn't support emitting only to stdout; it either works + // through a pipe, emits a diff, or rewrites the file in-place. + // Thus it's easier to let it do its in-place rewrite and then + // read the reference file from there. + const black = pythonProcess.execModule('black', [blackReferenceFile], { cwd: workspaceRootPath }); + formatters.push(black); + } + await Promise.all(formatters).then(formattedResults => { formattedYapf = formattedResults[0].stdout; formattedAutoPep8 = formattedResults[1].stdout; + if (!py2) { + formattedBlack = fs.readFileSync(blackReferenceFile).toString(); + } }); }); setup(async () => { @@ -47,7 +63,7 @@ suite('Formatting', () => { initializeDI(); }); suiteTeardown(async () => { - [autoPep8FileToFormat, autoPep8FileToAutoFormat, yapfFileToFormat, yapfFileToAutoFormat].forEach(file => { + [autoPep8FileToFormat, blackFileToFormat, blackReferenceFile, yapfFileToFormat].forEach(file => { if (fs.existsSync(file)) { fs.unlinkSync(file); } @@ -70,8 +86,8 @@ suite('Formatting', () => { ioc.registerMockProcessTypes(); } - function injectFormatOutput(outputFileName: string) { - const procService = ioc.serviceContainer.get(IProcessService); + async function injectFormatOutput(outputFileName: string) { + const procService = await ioc.serviceContainer.get(IProcessServiceFactory).create() as MockProcessService; procService.onExecObservable((file, args, options, callback) => { if (args.indexOf('--diff') >= 0) { callback({ @@ -82,12 +98,12 @@ suite('Formatting', () => { }); } - async function testFormatting(formatter: AutoPep8Formatter | YapfFormatter, formattedContents: string, fileToFormat: string, outputFileName: string) { - const textDocument = await vscode.workspace.openTextDocument(fileToFormat); - const textEditor = await vscode.window.showTextDocument(textDocument); + async function testFormatting(formatter: AutoPep8Formatter | BlackFormatter | YapfFormatter, formattedContents: string, fileToFormat: string, outputFileName: string) { + const textDocument = await workspace.openTextDocument(fileToFormat); + const textEditor = await window.showTextDocument(textDocument); const options = { insertSpaces: textEditor.options.insertSpaces! as boolean, tabSize: textEditor.options.tabSize! as number }; - injectFormatOutput(outputFileName); + await injectFormatOutput(outputFileName); const edits = await formatter.formatDocument(textDocument, options, new CancellationTokenSource().token); await textEditor.edit(editBuilder => { @@ -96,8 +112,24 @@ suite('Formatting', () => { compareFiles(formattedContents, textEditor.document.getText()); } - test('AutoPep8', async () => await testFormatting(new AutoPep8Formatter(ioc.serviceContainer), formattedAutoPep8, autoPep8FileToFormat, 'autopep8.output')); - test('Yapf', async () => await testFormatting(new YapfFormatter(ioc.serviceContainer), formattedYapf, yapfFileToFormat, 'yapf.output')); + test('AutoPep8', async () => { + await testFormatting( + new AutoPep8Formatter(ioc.serviceContainer), + formattedAutoPep8, + autoPep8FileToFormat, + 'autopep8.output'); + }); + // tslint:disable-next-line:no-function-expression + test('Black', async function () { + const pyVersion: PythonVersionInformation = await ioc.getPythonMajorMinorVersion(Uri.parse(blackFileToFormat)); + + if (pyVersion && (pyVersion.major < 3 || (pyVersion.major === 3 && pyVersion.minor < 6))) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + await testFormatting(new BlackFormatter(ioc.serviceContainer), formattedBlack, blackFileToFormat, 'black.output'); + }); + test('Yapf', async () => testFormatting(new YapfFormatter(ioc.serviceContainer), formattedYapf, yapfFileToFormat, 'yapf.output')); test('Yapf on dirty file', async () => { const sourceDir = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); @@ -114,11 +146,11 @@ suite('Formatting', () => { fs.copySync(path.join(sourceDir, originalName), fileToFormat, { overwrite: true }); fs.copySync(path.join(sourceDir, resultsName), formattedFile, { overwrite: true }); - const textDocument = await vscode.workspace.openTextDocument(fileToFormat); - const textEditor = await vscode.window.showTextDocument(textDocument); + const textDocument = await workspace.openTextDocument(fileToFormat); + const textEditor = await window.showTextDocument(textDocument); await textEditor.edit(builder => { // Make file dirty. Trailing blanks will be removed. - builder.insert(new vscode.Position(0, 0), '\n \n'); + builder.insert(new Position(0, 0), '\n \n'); }); const dir = path.dirname(fileToFormat); diff --git a/src/test/format/extension.lineFormatter.test.ts b/src/test/format/extension.lineFormatter.test.ts index 842cb02d735d..0cfaaaa180f4 100644 --- a/src/test/format/extension.lineFormatter.test.ts +++ b/src/test/format/extension.lineFormatter.test.ts @@ -3,66 +3,228 @@ // Licensed under the MIT License. import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Position, Range, TextDocument, TextLine } from 'vscode'; +import '../../client/common/extensions'; import { LineFormatter } from '../../client/formatters/lineFormatter'; +const formatFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); +const grammarFile = path.join(formatFilesPath, 'pythonGrammar.py'); + // https://www.python.org/dev/peps/pep-0008/#code-lay-out +// tslint:disable-next-line:max-func-body-length suite('Formatting - line formatter', () => { const formatter = new LineFormatter(); test('Operator spacing', () => { - const actual = formatter.formatLine('( x +1 )*y/ 3'); - assert.equal(actual, '(x + 1) * y / 3'); + testFormatLine('( x +1 )*y/ 3', '(x + 1) * y / 3'); }); test('Braces spacing', () => { - const actual = formatter.formatLine('foo =(0 ,)'); - assert.equal(actual, 'foo = (0,)'); - }); - test('Function arguments', () => { - const actual = formatter.formatLine('foo (0 , x= 1, (3+7) , y , z )'); - assert.equal(actual, 'foo(0, x=1, (3 + 7), y, z)'); + testFormatMultiline('foo =(0 ,)', 0, 'foo = (0,)'); }); test('Colon regular', () => { - const actual = formatter.formatLine('if x == 4 : print x,y; x,y= y, x'); - assert.equal(actual, 'if x == 4: print x, y; x, y = y, x'); + testFormatMultiline('if x == 4 : print x,y; x,y= y, x', 0, 'if x == 4: print x, y; x, y = y, x'); }); test('Colon slices', () => { - const actual = formatter.formatLine('x[1: 30]'); - assert.equal(actual, 'x[1:30]'); + testFormatLine('x[1: 30]', 'x[1:30]'); }); test('Colon slices in arguments', () => { - const actual = formatter.formatLine('spam ( ham[ 1 :3], {eggs : 2})'); - assert.equal(actual, 'spam(ham[1:3], {eggs: 2})'); + testFormatLine('spam ( ham[ 1 :3], {eggs : 2})', + 'spam(ham[1:3], {eggs: 2})'); }); test('Colon slices with double colon', () => { - const actual = formatter.formatLine('ham [1:9 ], ham[ 1: 9: 3], ham[: 9 :3], ham[1: :3], ham [ 1: 9:]'); - assert.equal(actual, 'ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:]'); + testFormatLine('ham [1:9 ], ham[ 1: 9: 3], ham[: 9 :3], ham[1: :3], ham [ 1: 9:]', + 'ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:]'); }); test('Colon slices with operators', () => { - const actual = formatter.formatLine('ham [lower+ offset :upper+offset]'); - assert.equal(actual, 'ham[lower + offset:upper + offset]'); + testFormatLine('ham [lower+ offset :upper+offset]', + 'ham[lower + offset:upper + offset]'); }); test('Colon slices with functions', () => { - const actual = formatter.formatLine('ham[ : upper_fn ( x) : step_fn(x )], ham[ :: step_fn(x)]'); - assert.equal(actual, 'ham[:upper_fn(x):step_fn(x)], ham[::step_fn(x)]'); + testFormatLine('ham[ : upper_fn ( x) : step_fn(x )], ham[ :: step_fn(x)]', + 'ham[:upper_fn(x):step_fn(x)], ham[::step_fn(x)]'); }); test('Colon in for loop', () => { - const actual = formatter.formatLine('for index in range( len(fruits) ): '); - assert.equal(actual, 'for index in range(len(fruits)):'); + testFormatLine('for index in range( len(fruits) ): ', 'for index in range(len(fruits)):'); }); test('Nested braces', () => { - const actual = formatter.formatLine('[ 1 :[2: (x,),y]]{1}'); - assert.equal(actual, '[1:[2:(x,), y]]{1}'); + testFormatLine('[ 1 :[2: (x,),y]]{1}', '[1:[2:(x,), y]]{1}'); }); test('Trailing comment', () => { - const actual = formatter.formatLine('x=1 # comment'); - assert.equal(actual, 'x = 1 # comment'); + testFormatMultiline('x=1 # comment', 0, 'x = 1 # comment'); }); test('Single comment', () => { - const actual = formatter.formatLine('# comment'); - assert.equal(actual, '# comment'); + testFormatLine('# comment', '# comment'); }); test('Comment with leading whitespace', () => { - const actual = formatter.formatLine(' # comment'); - assert.equal(actual, ' # comment'); + testFormatLine(' # comment', ' # comment'); + }); + test('Operators without following space', () => { + testFormatLine('foo( *a, ** b, ! c)', 'foo(*a, **b, !c)'); + }); + test('Brace after keyword', () => { + testFormatLine('for x in(1,2,3)', 'for x in (1, 2, 3)'); + testFormatLine('assert(1,2,3)', 'assert (1, 2, 3)'); + testFormatLine('if (True|False)and(False/True)not (! x )', 'if (True | False) and (False / True) not (!x)'); + testFormatLine('while (True|False)', 'while (True | False)'); + testFormatLine('yield(a%b)', 'yield (a % b)'); + }); + test('Dot operator', () => { + testFormatLine('x.y', 'x.y'); + testFormatLine('5 .y', '5.y'); + }); + test('Unknown tokens no space', () => { + testFormatLine('abc\\n\\', 'abc\\n\\'); + }); + test('Unknown tokens with space', () => { + testFormatLine('abc \\n \\', 'abc \\n \\'); + }); + test('Double asterisk', () => { + testFormatLine('a**2, ** k', 'a ** 2, **k'); + }); + test('Lambda', () => { + testFormatLine('lambda * args, :0', 'lambda *args,: 0'); + }); + test('Comma expression', () => { + testFormatMultiline('x=1,2,3', 0, 'x = 1, 2, 3'); + }); + test('is exression', () => { + testFormatLine('a( (False is 2) is 3)', 'a((False is 2) is 3)'); + }); + test('Function returning tuple', () => { + testFormatMultiline('x,y=f(a)', 0, 'x, y = f(a)'); + }); + test('from. import A', () => { + testFormatLine('from. import A', 'from . import A'); + }); + test('from .. import', () => { + testFormatLine('from ..import', 'from .. import'); + }); + test('from..x import', () => { + testFormatLine('from..x import', 'from ..x import'); + }); + test('Raw strings', () => { + testFormatMultiline('z=r""', 0, 'z = r""'); + testFormatMultiline('z=rf""', 0, 'z = rf""'); + testFormatMultiline('z=R""', 0, 'z = R""'); + testFormatMultiline('z=RF""', 0, 'z = RF""'); + }); + test('Binary @', () => { + testFormatLine('a@ b', 'a @ b'); + }); + test('Unary operators', () => { + testFormatMultiline('x= - y', 0, 'x = -y'); + testFormatMultiline('x= + y', 0, 'x = +y'); + testFormatMultiline('x= ~ y', 0, 'x = ~y'); + testFormatMultiline('x=-1', 0, 'x = -1'); + testFormatMultiline('x= +1', 0, 'x = +1'); + testFormatMultiline('x= ~1 ', 0, 'x = ~1'); + }); + test('Equals with type hints', () => { + testFormatMultiline('def foo(x:int=3,x=100.)', 0, 'def foo(x: int = 3, x=100.)'); }); + test('Trailing comma', () => { + testFormatLine('a, =[1]', 'a, = [1]'); + }); + test('if()', () => { + testFormatLine('if(True) :', 'if (True):'); + }); + test('lambda arguments', () => { + testFormatMultiline('l4= lambda x =lambda y =lambda z= 1: z: y(): x()', 0, 'l4 = lambda x=lambda y=lambda z=1: z: y(): x()'); + }); + test('star in multiline arguments', () => { + testFormatMultiline('x = [\n * param1,\n * param2\n]', 1, ' *param1,'); + testFormatMultiline('x = [\n * param1,\n * param2\n]', 2, ' *param2'); + }); + test('arrow operator', () => { + //testFormatMultiline('def f(a, b: 1, e: 3 = 4, f =5, * g: 6, ** k: 11) -> 12: pass', 0, 'def f(a, b: 1, e: 3 = 4, f=5, *g: 6, **k: 11) -> 12: pass'); + testFormatMultiline('def f(a, \n ** k: 11) -> 12: pass', 1, ' **k: 11) -> 12: pass'); + }); + + test('Multiline function call', () => { + testFormatMultiline('def foo(x = 1)', 0, 'def foo(x=1)'); + testFormatMultiline('def foo(a\n, x = 1)', 1, ', x=1)'); + testFormatMultiline('foo(a ,b,\n x = 1)', 1, ' x=1)'); + testFormatMultiline('if True:\n if False:\n foo(a , bar(\n x = 1)', 3, ' x=1)'); + testFormatMultiline('z=foo (0 , x= 1, (3+7) , y , z )', 0, 'z = foo(0, x=1, (3 + 7), y, z)'); + testFormatMultiline('foo (0,\n x= 1,', 1, ' x=1,'); + testFormatMultiline( +// tslint:disable-next-line:no-multiline-string +`async def fetch(): + async with aiohttp.ClientSession() as session: + async with session.ws_connect( + "http://127.0.0.1:8000/", headers = cookie) as ws: # add unwanted spaces`, 3, + ' "http://127.0.0.1:8000/", headers=cookie) as ws: # add unwanted spaces'); + testFormatMultiline('def pos0key1(*, key): return key\npos0key1(key= 100)', 1, 'pos0key1(key=100)'); + testFormatMultiline('def test_string_literals(self):\n x= 1; y =2; self.assertTrue(len(x) == 0 and x == y)', 1, + ' x = 1; y = 2; self.assertTrue(len(x) == 0 and x == y)'); + }); + test('Grammar file', () => { + const content = fs.readFileSync(grammarFile).toString('utf8'); + const lines = content.splitLines({ trim: false, removeEmptyEntries: false }); + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i]; + const actual = formatMultiline(content, i); + assert.equal(actual, line, `Line ${i + 1} changed: '${line.trim()}' to '${actual.trim()}'`); + } + }); + + function testFormatLine(text: string, expected: string): void { + const actual = formatLine(text); + assert.equal(actual, expected); + } + + function testFormatMultiline(content: string, lineNumber: number, expected: string): void { + const actual = formatMultiline(content, lineNumber); + assert.equal(actual, expected); + } + + function formatMultiline(content: string, lineNumber: number): string { + const lines = content.splitLines({ trim: false, removeEmptyEntries: false }); + + const document = TypeMoq.Mock.ofType(); + document.setup(x => x.lineAt(TypeMoq.It.isAnyNumber())).returns(n => { + const line = TypeMoq.Mock.ofType(); + line.setup(x => x.text).returns(() => lines[n]); + line.setup(x => x.range).returns(() => new Range(new Position(n, 0), new Position(n, lines[n].length))); + return line.object; + }); + document.setup(x => x.getText(TypeMoq.It.isAny())).returns(o => { + const r = o as Range; + const bits: string[] = []; + + if (r.start.line === r.end.line) { + return lines[r.start.line].substring(r.start.character, r.end.character); + } + + bits.push(lines[r.start.line].substr(r.start.character)); + for (let i = r.start.line + 1; i < r.end.line; i += 1) { + bits.push(lines[i]); + } + bits.push(lines[r.end.line].substring(0, r.end.character)); + return bits.join('\n'); + }); + document.setup(x => x.offsetAt(TypeMoq.It.isAny())).returns(o => { + const p = o as Position; + let offset = 0; + for (let i = 0; i < p.line; i += 1) { + offset += lines[i].length + 1; // Accounting for the line break + } + return offset + p.character; + }); + + return formatter.formatLine(document.object, lineNumber); + } + + function formatLine(text: string): string { + const line = TypeMoq.Mock.ofType(); + line.setup(x => x.text).returns(() => text); + + const document = TypeMoq.Mock.ofType(); + document.setup(x => x.lineAt(TypeMoq.It.isAnyNumber())).returns(() => line.object); + + return formatter.formatLine(document.object, 0); + } }); diff --git a/src/test/format/extension.onEnterFormat.test.ts b/src/test/format/extension.onEnterFormat.test.ts index 74597ce19be7..c6560833edaf 100644 --- a/src/test/format/extension.onEnterFormat.test.ts +++ b/src/test/format/extension.onEnterFormat.test.ts @@ -46,7 +46,7 @@ suite('Formatting - OnEnter provider', () => { test('Formatting line ending in comment', async () => { const text = await formatAtPosition(6, 0); - assert.equal(text, 'x + 1 # ', 'Line ending in comment was not formatted'); + assert.equal(text, 'x + 1 # ', 'Line ending in comment was not formatted'); }); test('Formatting line with @', async () => { @@ -59,8 +59,28 @@ suite('Formatting - OnEnter provider', () => { assert.equal(text, 'x.y', 'Line ending with period was reformatted'); }); - test('Formatting line ending in string', async () => { + test('Formatting line with unknown neighboring tokens', async () => { const text = await formatAtPosition(9, 0); + assert.equal(text, 'if x <= 1:', 'Line with unknown neighboring tokens was not formatted'); + }); + + test('Formatting line with unknown neighboring tokens', async () => { + const text = await formatAtPosition(10, 0); + assert.equal(text, 'if 1 <= x:', 'Line with unknown neighboring tokens was not formatted'); + }); + + test('Formatting method definition with arguments', async () => { + const text = await formatAtPosition(11, 0); + assert.equal(text, 'def __init__(self, age=23)', 'Method definition with arguments was not formatted'); + }); + + test('Formatting space after open brace', async () => { + const text = await formatAtPosition(12, 0); + assert.equal(text, 'while (1)', 'Space after open brace was not formatted'); + }); + + test('Formatting line ending in string', async () => { + const text = await formatAtPosition(13, 0); assert.equal(text, 'x + """', 'Line ending in multiline string was not formatted'); }); diff --git a/src/test/format/extension.sort.test.ts b/src/test/format/extension.sort.test.ts index af88891e6e91..884113bdb77b 100644 --- a/src/test/format/extension.sort.test.ts +++ b/src/test/format/extension.sort.test.ts @@ -3,7 +3,6 @@ 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 { IProcessService, IPythonExecutionFactory } from '../../client/common/process/types'; import { PythonImportSortProvider } from '../../client/providers/importSortProvider'; import { updateSetting } from '../common'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; @@ -39,9 +38,7 @@ suite('Sorting', () => { fs.writeFileSync(fileToFormatWithConfig1, fs.readFileSync(originalFileToFormatWithConfig1)); await updateSetting('sortImports.args', [], Uri.file(sortingPath), configTarget); await closeActiveWindows(); - const pythonExecutionFactory = ioc.serviceContainer.get(IPythonExecutionFactory); - const processService = ioc.serviceContainer.get(IProcessService); - sorter = new PythonImportSortProvider(pythonExecutionFactory, processService); + sorter = new PythonImportSortProvider(ioc.serviceContainer); }); teardown(async () => { ioc.dispose(); diff --git a/src/test/format/format.helper.test.ts b/src/test/format/format.helper.test.ts index 51afda1a64db..d6c732e3e131 100644 --- a/src/test/format/format.helper.test.ts +++ b/src/test/format/format.helper.test.ts @@ -25,7 +25,7 @@ suite('Formatting - Helper', () => { }); test('Ensure product is set in Execution Info', async () => { - [Product.autopep8, Product.yapf].forEach(formatter => { + [Product.autopep8, Product.black, Product.yapf].forEach(formatter => { const info = formatHelper.getExecutionInfo(formatter, []); assert.equal(info.product, formatter, `Incorrect products for ${formatHelper.translateToId(formatter)}`); }); @@ -34,7 +34,7 @@ suite('Formatting - Helper', () => { test('Ensure executable is set in Execution Info', async () => { const settings = PythonSettings.getInstance(); - [Product.autopep8, Product.yapf].forEach(formatter => { + [Product.autopep8, Product.black, Product.yapf].forEach(formatter => { const info = formatHelper.getExecutionInfo(formatter, []); const names = formatHelper.getSettingsPropertyNames(formatter); const execPath = settings.formatting[names.pathName] as string; @@ -47,7 +47,7 @@ suite('Formatting - Helper', () => { const settings = PythonSettings.getInstance(); const customArgs = ['1', '2', '3']; - [Product.autopep8, Product.yapf].forEach(formatter => { + [Product.autopep8, Product.black, Product.yapf].forEach(formatter => { const names = formatHelper.getSettingsPropertyNames(formatter); const args: string[] = Array.isArray(settings.formatting[names.argsName]) ? settings.formatting[names.argsName] as string[] : []; const expectedArgs = args.concat(customArgs).join(','); @@ -58,7 +58,7 @@ suite('Formatting - Helper', () => { }); test('Ensure correct setting names are returned', async () => { - [Product.autopep8, Product.yapf].forEach(formatter => { + [Product.autopep8, Product.black, Product.yapf].forEach(formatter => { const translatedId = formatHelper.translateToId(formatter)!; const settings = { argsName: `${translatedId}Args` as keyof IFormattingSettings, @@ -72,9 +72,10 @@ suite('Formatting - Helper', () => { test('Ensure translation of ids works', async () => { const formatterMapping = new Map(); formatterMapping.set(Product.autopep8, 'autopep8'); + formatterMapping.set(Product.black, 'black'); formatterMapping.set(Product.yapf, 'yapf'); - [Product.autopep8, Product.yapf].forEach(formatter => { + [Product.autopep8, Product.black, Product.yapf].forEach(formatter => { const translatedId = formatHelper.translateToId(formatter); assert.equal(translatedId, formatterMapping.get(formatter)!, `Incorrect translation for product ${formatHelper.translateToId(formatter)}`); }); @@ -83,6 +84,7 @@ suite('Formatting - Helper', () => { EnumEx.getValues(Product).forEach(product => { const formatterMapping = new Map(); formatterMapping.set(Product.autopep8, 'autopep8'); + formatterMapping.set(Product.black, 'black'); formatterMapping.set(Product.yapf, 'yapf'); if (formatterMapping.has(product)) { return; diff --git a/src/test/index.ts b/src/test/index.ts index 3717f37fd021..b63fe1ba47b4 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -3,27 +3,63 @@ if ((Reflect as any).metadata === undefined) { // tslint:disable-next-line:no-require-imports no-var-requires require('reflect-metadata'); } -import { MochaSetupOptions } from 'vscode/lib/testrunner'; -import { IS_CI_SERVER, IS_CI_SERVER_TEST_DEBUGGER, IS_MULTI_ROOT_TEST } from './constants'; + +import { + IS_CI_SERVER, IS_CI_SERVER_TEST_DEBUGGER, + IS_VSTS, MOCHA_CI_PROPERTIES, MOCHA_CI_REPORTER_ID, + MOCHA_CI_REPORTFILE, MOCHA_REPORTER_JUNIT +} from './ciConstants'; +import { IS_MULTI_ROOT_TEST } from './constants'; import * as testRunner from './testRunner'; process.env.VSC_PYTHON_CI_TEST = '1'; -process.env.IS_MULTI_ROOT_TEST = IS_MULTI_ROOT_TEST; +process.env.IS_MULTI_ROOT_TEST = IS_MULTI_ROOT_TEST.toString(); // If running on CI server and we're running the debugger tests, then ensure we only run debug tests. // We do this to ensure we only run debugger test, as debugger tests are very flaky on CI. // So the solution is to run them separately and first on CI. const grep = IS_CI_SERVER && IS_CI_SERVER_TEST_DEBUGGER ? 'Debug' : undefined; +const testFilesSuffix = process.env.TEST_FILES_SUFFIX; // 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. // Hack, as retries is not supported as setting in tsd. -const options: MochaSetupOptions & { retries: number } = { +const options: testRunner.SetupOptions & { retries: number } = { ui: 'tdd', useColors: true, timeout: 25000, retries: 3, - grep + grep, + testFilesSuffix }; + +// VSTS CI doesn't display colours correctly (yet). +if (IS_VSTS) { + options.useColors = false; +} + +// CI can ask for a JUnit reporter if the environment variable +// 'MOCHA_REPORTER_JUNIT' is defined, further control is afforded +// by other 'MOCHA_CI_...' variables. See constants.ts for info. +if (MOCHA_REPORTER_JUNIT) { + options.reporter = MOCHA_CI_REPORTER_ID; + options.reporterOptions = { + mochaFile: MOCHA_CI_REPORTFILE, + properties: MOCHA_CI_PROPERTIES + }; +} + +process.on('unhandledRejection', (ex: string | Error, a) => { + const message = [`${ex}`]; + if (typeof ex !== 'string' && ex && ex.message) { + message.push(ex.name); + message.push(ex.message); + if (ex.stack) { + message.push(ex.stack); + } + } + console.error(`Unhandled Promise Rejection with the message ${message.join(', ')}`); +}); + testRunner.configure(options, { coverageConfig: '../coverconfig.json' }); module.exports = testRunner; diff --git a/src/test/initialize.ts b/src/test/initialize.ts index 5914d38185e9..ff764d7c917e 100644 --- a/src/test/initialize.ts +++ b/src/test/initialize.ts @@ -1,13 +1,13 @@ // tslint:disable:no-string-literal -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'; +import { clearPythonPathInWorkspaceFolder, PYTHON_PATH, resetGlobalPythonPathSetting, setPythonPathInWorkspaceRoot } from './common'; export * from './constants'; +export * from './ciConstants'; const dummyPythonFile = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); const multirootPath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc'); @@ -16,8 +16,6 @@ const workspace3Uri = vscode.Uri.file(path.join(multirootPath, 'workspace3')); //First thing to be executed. process.env['VSC_PYTHON_CI_TEST'] = '1'; -const PYTHON_PATH = getPythonPath(); - // Ability to use custom python environments for testing export async function initializePython() { await resetGlobalPythonPathSetting(); @@ -42,18 +40,15 @@ export async function initializeTest(): Promise { // Dispose any cached python settings (used only in test env). PythonSettings.dispose(); } - export async function closeActiveWindows(): Promise { - return new Promise((resolve, reject) => vscode.commands.executeCommand('workbench.action.closeAllEditors') - // tslint:disable-next-line:no-unnecessary-callback-wrapper - .then(() => resolve(), reject)); -} - -function getPythonPath(): string { - // 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'; + return new Promise((resolve, reject) => { + vscode.commands.executeCommand('workbench.action.closeAllEditors') + // tslint:disable-next-line:no-unnecessary-callback-wrapper + .then(() => resolve(), reject); + // Attempt to fix #1301. + // Lets not waste too much time. + setTimeout(() => { + reject(new Error('Command \'workbench.action.closeAllEditors\' timedout')); + }, 15000); + }); } diff --git a/src/test/install/channelManager.channels.test.ts b/src/test/install/channelManager.channels.test.ts index 714f9c65a8f8..3662859ee1fc 100644 --- a/src/test/install/channelManager.channels.test.ts +++ b/src/test/install/channelManager.channels.test.ts @@ -8,12 +8,26 @@ import { QuickPickOptions } from 'vscode'; import { IApplicationShell } from '../../client/common/application/types'; import { InstallationChannelManager } from '../../client/common/installer/channelManager'; import { IModuleInstaller } from '../../client/common/installer/types'; +import { Architecture } from '../../client/common/platform/types'; import { Product } from '../../client/common/types'; import { IInterpreterLocatorService, InterpreterType, PIPENV_SERVICE, PythonInterpreter } from '../../client/interpreter/contracts'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceContainer } from '../../client/ioc/types'; +const info: PythonInterpreter = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + type: InterpreterType.Unknown, + version: '', + version_info: [0, 0, 0, 'alpha'], + sysPrefix: '', + sysVersion: '' +}; + // tslint:disable-next-line:max-func-body-length suite('Installation - installation channels', () => { let serviceManager: ServiceManager; @@ -55,6 +69,7 @@ suite('Installation - installation channels', () => { const pipenvInstaller = mockInstaller(true, 'pipenv', 10); const interpreter: PythonInterpreter = { + ...info, path: 'pipenv', type: InterpreterType.VirtualEnv }; diff --git a/src/test/install/channelManager.messages.test.ts b/src/test/install/channelManager.messages.test.ts index 0762b4242014..0cafd2f64b0c 100644 --- a/src/test/install/channelManager.messages.test.ts +++ b/src/test/install/channelManager.messages.test.ts @@ -6,13 +6,27 @@ import { Container } from 'inversify'; import * as TypeMoq from 'typemoq'; import { IApplicationShell } from '../../client/common/application/types'; import { InstallationChannelManager } from '../../client/common/installer/channelManager'; -import { IPlatformService } from '../../client/common/platform/types'; +import { IModuleInstaller } from '../../client/common/installer/types'; +import { Architecture, IPlatformService } from '../../client/common/platform/types'; import { Product } from '../../client/common/types'; import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceContainer } from '../../client/ioc/types'; +const info: PythonInterpreter = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + type: InterpreterType.Unknown, + version: '', + version_info: [0, 0, 0, 'alpha'], + sysPrefix: '', + sysVersion: '' +}; + // tslint:disable-next-line:max-func-body-length suite('Installation - channel messages', () => { let serviceContainer: IServiceContainer; @@ -33,13 +47,15 @@ suite('Installation - channel messages', () => { interpreters = TypeMoq.Mock.ofType(); serviceManager.addSingletonInstance(IInterpreterService, interpreters.object); + + const moduleInstaller = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(IModuleInstaller, moduleInstaller.object); }); test('No installers message: Unknown/Windows', async () => { platform.setup(x => x.isWindows).returns(() => true); await testInstallerMissingMessage(InterpreterType.Unknown, - async (channels: InstallationChannelManager, message: string, url: string) => { - await channels.showNoInstallersMessage(); + async (message: string, url: string) => { verifyMessage(message, ['Pip'], ['Conda']); verifyUrl(url, ['Windows', 'Pip']); }); @@ -48,8 +64,7 @@ suite('Installation - channel messages', () => { test('No installers message: Conda/Windows', async () => { platform.setup(x => x.isWindows).returns(() => true); await testInstallerMissingMessage(InterpreterType.Conda, - async (channels: InstallationChannelManager, message: string, url: string) => { - await channels.showNoInstallersMessage(); + async (message: string, url: string) => { verifyMessage(message, ['Pip', 'Conda'], []); verifyUrl(url, ['Windows', 'Pip', 'Conda']); }); @@ -59,8 +74,7 @@ suite('Installation - channel messages', () => { platform.setup(x => x.isWindows).returns(() => false); platform.setup(x => x.isMac).returns(() => true); await testInstallerMissingMessage(InterpreterType.Unknown, - async (channels: InstallationChannelManager, message: string, url: string) => { - await channels.showNoInstallersMessage(); + async (message: string, url: string) => { verifyMessage(message, ['Pip'], ['Conda']); verifyUrl(url, ['Mac', 'Pip']); }); @@ -70,8 +84,7 @@ suite('Installation - channel messages', () => { platform.setup(x => x.isWindows).returns(() => false); platform.setup(x => x.isMac).returns(() => true); await testInstallerMissingMessage(InterpreterType.Conda, - async (channels: InstallationChannelManager, message: string, url: string) => { - await channels.showNoInstallersMessage(); + async (message: string, url: string) => { verifyMessage(message, ['Pip', 'Conda'], []); verifyUrl(url, ['Mac', 'Pip', 'Conda']); }); @@ -82,8 +95,7 @@ suite('Installation - channel messages', () => { platform.setup(x => x.isMac).returns(() => false); platform.setup(x => x.isLinux).returns(() => true); await testInstallerMissingMessage(InterpreterType.Unknown, - async (channels: InstallationChannelManager, message: string, url: string) => { - await channels.showNoInstallersMessage(); + async (message: string, url: string) => { verifyMessage(message, ['Pip'], ['Conda']); verifyUrl(url, ['Linux', 'Pip']); }); @@ -94,8 +106,7 @@ suite('Installation - channel messages', () => { platform.setup(x => x.isMac).returns(() => false); platform.setup(x => x.isLinux).returns(() => true); await testInstallerMissingMessage(InterpreterType.Conda, - async (channels: InstallationChannelManager, message: string, url: string) => { - await channels.showNoInstallersMessage(); + async (message: string, url: string) => { verifyMessage(message, ['Pip', 'Conda'], []); verifyUrl(url, ['Linux', 'Pip', 'Conda']); }); @@ -104,11 +115,10 @@ suite('Installation - channel messages', () => { test('No channels message', async () => { platform.setup(x => x.isWindows).returns(() => true); await testInstallerMissingMessage(InterpreterType.Unknown, - async (channels: InstallationChannelManager, message: string, url: string) => { - await channels.getInstallationChannel(Product.pylint); + async (message: string, url: string) => { verifyMessage(message, ['Pip'], ['Conda']); verifyUrl(url, ['Windows', 'Pip']); - }); + }, 'getInstallationChannel'); }); function verifyMessage(message: string, present: string[], missing: string[]) { @@ -129,9 +139,11 @@ suite('Installation - channel messages', () => { async function testInstallerMissingMessage( interpreterType: InterpreterType, - verify: (c: InstallationChannelManager, m: string, u: string) => void): Promise { + verify: (m: string, u: string) => Promise, + methodType: 'showNoInstallersMessage' | 'getInstallationChannel' = 'showNoInstallersMessage'): Promise { const activeInterpreter: PythonInterpreter = { + ...info, type: interpreterType, path: '' }; @@ -153,6 +165,11 @@ suite('Installation - channel messages', () => { appShell.setup(x => x.openUrl(TypeMoq.It.isAnyString())).callback((s: string) => { url = s; }); - verify(channels, message, url); + if (methodType === 'showNoInstallersMessage') { + await channels.showNoInstallersMessage(); + } else { + await channels.getInstallationChannel(Product.pylint); + } + await verify(message, url); } }); diff --git a/src/test/install/pythonInstallation.test.ts b/src/test/install/pythonInstallation.test.ts index 23e5429c1bb9..b6131cc9c077 100644 --- a/src/test/install/pythonInstallation.test.ts +++ b/src/test/install/pythonInstallation.test.ts @@ -7,15 +7,27 @@ import { Container } from 'inversify'; import * as TypeMoq from 'typemoq'; import { IApplicationShell } from '../../client/common/application/types'; import { PythonInstaller } from '../../client/common/installer/pythonInstallation'; -import { IPlatformService } from '../../client/common/platform/types'; +import { Architecture, IPlatformService } from '../../client/common/platform/types'; import { IPythonSettings } from '../../client/common/types'; -import { IInterpreterLocatorService, IInterpreterService } from '../../client/interpreter/contracts'; -import { InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; +import { IInterpreterLocatorService, IInterpreterService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceContainer } from '../../client/ioc/types'; import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +const info: PythonInterpreter = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + type: InterpreterType.Unknown, + version: '', + version_info: [0, 0, 0, 'alpha'], + sysPrefix: '', + sysVersion: '' +}; + class TestContext { public serviceManager: ServiceManager; public serviceContainer: IServiceContainer; @@ -36,6 +48,7 @@ class TestContext { this.settings = TypeMoq.Mock.ofType(); const activeInterpreter: PythonInterpreter = { + ...info, type: InterpreterType.Unknown, path: '' }; @@ -116,6 +129,7 @@ suite('Installation', () => { c.appShell.setup(x => x.showWarningMessage(TypeMoq.It.isAnyString())).callback(() => called = true); c.settings.setup(x => x.pythonPath).returns(() => 'python'); const interpreter: PythonInterpreter = { + ...info, path: 'python', type: InterpreterType.Unknown }; diff --git a/src/test/interpreters/condaEnvFileService.test.ts b/src/test/interpreters/condaEnvFileService.test.ts index 2207be82732a..062722354c90 100644 --- a/src/test/interpreters/condaEnvFileService.test.ts +++ b/src/test/interpreters/condaEnvFileService.test.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { IFileSystem } from '../../client/common/platform/types'; import { ILogger, IPersistentStateFactory } from '../../client/common/types'; -import { ICondaService, IInterpreterLocatorService, IInterpreterVersionService, InterpreterType } from '../../client/interpreter/contracts'; +import { ICondaService, IInterpreterHelper, IInterpreterLocatorService, InterpreterType } from '../../client/interpreter/contracts'; import { AnacondaCompanyName, AnacondaCompanyNames, AnacondaDisplayName } from '../../client/interpreter/locators/services/conda'; import { CondaEnvFileService } from '../../client/interpreter/locators/services/condaEnvFileService'; import { IServiceContainer } from '../../client/ioc/types'; @@ -18,7 +18,7 @@ const environmentsFilePath = path.join(environmentsPath, 'environments.txt'); suite('Interpreters from Conda Environments Text File', () => { let logger: TypeMoq.IMock; let condaService: TypeMoq.IMock; - let interpreterVersion: TypeMoq.IMock; + let interpreterHelper: TypeMoq.IMock; let condaFileProvider: IInterpreterLocatorService; let fileSystem: TypeMoq.IMock; suiteSetup(initialize); @@ -31,22 +31,22 @@ suite('Interpreters from Conda Environments Text File', () => { stateFactory.setup(s => s.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => state); condaService = TypeMoq.Mock.ofType(); - interpreterVersion = TypeMoq.Mock.ofType(); + interpreterHelper = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); logger = TypeMoq.Mock.ofType(); - condaFileProvider = new CondaEnvFileService(interpreterVersion.object, condaService.object, fileSystem.object, serviceContainer.object, logger.object); + condaFileProvider = new CondaEnvFileService(interpreterHelper.object, condaService.object, fileSystem.object, serviceContainer.object, logger.object); }); test('Must return an empty list if environment file cannot be found', async () => { condaService.setup(c => c.condaEnvironmentsFile).returns(() => undefined); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('Mock Name')); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: 'Mock Name' })); const interpreters = await condaFileProvider.getInterpreters(); assert.equal(interpreters.length, 0, 'Incorrect number of entries'); }); test('Must return an empty list for an empty file', async () => { condaService.setup(c => c.condaEnvironmentsFile).returns(() => environmentsFilePath); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve(true)); fileSystem.setup(fs => fs.readFile(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve('')); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('Mock Name')); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: 'Mock Name' })); const interpreters = await condaFileProvider.getInterpreters(); assert.equal(interpreters.length, 0, 'Incorrect number of entries'); }); @@ -73,15 +73,15 @@ suite('Interpreters from Conda Environments Text File', () => { }); return Promise.resolve(condaEnvironments); }); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve(true)); fileSystem.setup(fs => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((p1: string, p2: string) => isWindows ? p1 === p2 : p1.toUpperCase() === p2.toUpperCase()); validPaths.forEach(validPath => { const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); }); fileSystem.setup(fs => fs.readFile(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve(interpreterPaths.join(EOL))); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('Mock Name')); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: 'Mock Name' })); const interpreters = await condaFileProvider.getInterpreters(); @@ -107,13 +107,13 @@ suite('Interpreters from Conda Environments Text File', () => { const pythonPath = path.join(interpreterPaths[0], 'pythonPath'); condaService.setup(c => c.condaEnvironmentsFile).returns(() => environmentsFilePath); condaService.setup(c => c.getInterpreterPath(TypeMoq.It.isAny())).returns(() => pythonPath); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve(true)); fileSystem.setup(fs => fs.readFile(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve(interpreterPaths.join(EOL))); for (const companyName of AnacondaCompanyNames) { const versionWithCompanyName = `Mock Version :: ${companyName}`; - interpreterVersion.setup(c => c.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(versionWithCompanyName)); + interpreterHelper.setup(c => c.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: versionWithCompanyName })); const interpreters = await condaFileProvider.getInterpreters(); assert.equal(interpreters.length, 1, 'Incorrect number of entries'); diff --git a/src/test/interpreters/condaEnvService.test.ts b/src/test/interpreters/condaEnvService.test.ts index a3c7b93c7acc..2523ce3e6a0a 100644 --- a/src/test/interpreters/condaEnvService.test.ts +++ b/src/test/interpreters/condaEnvService.test.ts @@ -3,7 +3,8 @@ import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { IFileSystem } from '../../client/common/platform/types'; import { ILogger, IPersistentStateFactory } from '../../client/common/types'; -import { ICondaService, IInterpreterVersionService, InterpreterType } from '../../client/interpreter/contracts'; +import { ICondaService, InterpreterType } from '../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../client/interpreter/helpers'; import { AnacondaCompanyName, AnacondaDisplayName } from '../../client/interpreter/locators/services/conda'; import { CondaEnvService } from '../../client/interpreter/locators/services/condaEnvService'; import { IServiceContainer } from '../../client/ioc/types'; @@ -14,12 +15,12 @@ import { MockState } from './mocks'; const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); // tslint:disable-next-line:max-func-body-length -suite('Interpreters from Conda Environments', () => { +suite('Interpreters from Conda Environmentsx', () => { let ioc: UnitTestIocContainer; let logger: TypeMoq.IMock; let condaProvider: CondaEnvService; let condaService: TypeMoq.IMock; - let interpreterVersion: TypeMoq.IMock; + let interpreterHelper: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; suiteSetup(initialize); setup(async () => { @@ -32,9 +33,9 @@ suite('Interpreters from Conda Environments', () => { stateFactory.setup(s => s.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => state); condaService = TypeMoq.Mock.ofType(); - interpreterVersion = TypeMoq.Mock.ofType(); + interpreterHelper = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); - condaProvider = new CondaEnvService(condaService.object, interpreterVersion.object, logger.object, serviceContainer.object, fileSystem.object); + condaProvider = new CondaEnvService(condaService.object, interpreterHelper.object, logger.object, serviceContainer.object, fileSystem.object); }); teardown(() => ioc.dispose()); function initializeDI() { @@ -63,9 +64,9 @@ suite('Interpreters from Conda Environments', () => { }); info.envs.forEach(validPath => { const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); }); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: '' })); const interpreters = await condaProvider.parseCondaInfo(info); assert.equal(interpreters.length, 2, 'Incorrect number of entries'); @@ -100,9 +101,9 @@ suite('Interpreters from Conda Environments', () => { }); info.envs.forEach(validPath => { const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); }); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: '' })); condaService.setup(c => c.getCondaFile()).returns(() => Promise.resolve('conda')); condaService.setup(c => c.getCondaInfo()).returns(() => Promise.resolve(info)); condaService.setup(c => c.getCondaEnvironments(TypeMoq.It.isAny())).returns(() => Promise.resolve([ @@ -145,9 +146,9 @@ suite('Interpreters from Conda Environments', () => { }); info.envs.forEach(validPath => { const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); }); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: '' })); const interpreters = await condaProvider.parseCondaInfo(info); assert.equal(interpreters.length, 1, 'Incorrect number of entries'); @@ -171,7 +172,7 @@ suite('Interpreters from Conda Environments', () => { default_prefix: '', 'sys.version': '3.6.1 |Anaonda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]' }; - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: '' })); condaService.setup(c => c.getCondaInfo()).returns(() => Promise.resolve(info)); condaService.setup(c => c.getCondaEnvironments(TypeMoq.It.isAny())).returns(() => Promise.resolve([ { name: 'base', path: environmentsPath }, @@ -183,9 +184,9 @@ suite('Interpreters from Conda Environments', () => { }); info.envs.forEach(validPath => { const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); }); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); fileSystem.setup(fs => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((p1: string, p2: string) => isWindows ? p1 === p2 : p1.toUpperCase() === p2.toUpperCase()); const interpreters = await condaProvider.getInterpreters(); @@ -213,9 +214,9 @@ suite('Interpreters from Conda Environments', () => { }); info.envs.forEach(validPath => { const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); }); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: '' })); const interpreters = await condaProvider.parseCondaInfo(info); assert.equal(interpreters.length, 1, 'Incorrect number of entries'); @@ -243,9 +244,9 @@ suite('Interpreters from Conda Environments', () => { }); info.envs.forEach(validPath => { const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); }); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: '' })); condaService.setup(c => c.getCondaFile()).returns(() => Promise.resolve('conda')); condaService.setup(c => c.getCondaInfo()).returns(() => Promise.resolve(info)); condaService.setup(c => c.getCondaEnvironments(TypeMoq.It.isAny())).returns(() => Promise.resolve([ @@ -279,8 +280,8 @@ suite('Interpreters from Conda Environments', () => { return isWindows ? path.join(environmentPath, 'python.exe') : path.join(environmentPath, 'bin', 'python'); }); const pythonPath = isWindows ? path.join(info.default_prefix, 'python.exe') : path.join(info.default_prefix, 'bin', 'python'); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: '' })); const interpreters = await condaProvider.parseCondaInfo(info); assert.equal(interpreters.length, 1, 'Incorrect number of entries'); @@ -308,13 +309,13 @@ suite('Interpreters from Conda Environments', () => { path.join(environmentsPath, 'path3', 'three.exe')] }; const validPaths = info.envs.filter((_, index) => index % 2 === 0); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: '' })); validPaths.forEach(envPath => { condaService.setup(c => c.getInterpreterPath(TypeMoq.It.isValue(envPath))).returns(environmentPath => { return isWindows ? path.join(environmentPath, 'python.exe') : path.join(environmentPath, 'bin', 'python'); }); const pythonPath = isWindows ? path.join(envPath, 'python.exe') : path.join(envPath, 'bin', 'python'); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); }); const interpreters = await condaProvider.parseCondaInfo(info); diff --git a/src/test/interpreters/condaService.test.ts b/src/test/interpreters/condaService.test.ts index f6a16891e71a..fae42a258bc9 100644 --- a/src/test/interpreters/condaService.test.ts +++ b/src/test/interpreters/condaService.test.ts @@ -1,23 +1,34 @@ +// tslint:disable:no-require-imports no-var-requires no-any max-func-body-length import * as assert from 'assert'; import { expect } from 'chai'; import { EOL } from 'os'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { FileSystem } from '../../client/common/platform/fileSystem'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { IProcessService } from '../../client/common/process/types'; +import { Architecture, IFileSystem, IPlatformService } from '../../client/common/platform/types'; +import { IProcessService, IProcessServiceFactory } from '../../client/common/process/types'; import { ILogger, IPersistentStateFactory } from '../../client/common/types'; import { IInterpreterLocatorService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; -import { CondaService, KNOWN_CONDA_LOCATIONS } from '../../client/interpreter/locators/services/condaService'; +import { CondaService } from '../../client/interpreter/locators/services/condaService'; import { IServiceContainer } from '../../client/ioc/types'; import { MockState } from './mocks'; -// tslint:disable-next-line:no-require-imports no-var-requires const untildify: (value: string) => string = require('untildify'); const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); +const info: PythonInterpreter = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + type: InterpreterType.Unknown, + version: '', + version_info: [0, 0, 0, 'alpha'], + sysPrefix: '', + sysVersion: '' +}; -// tslint:disable-next-line:max-func-body-length suite('Interpreters Conda Service', () => { let processService: TypeMoq.IMock; let platformService: TypeMoq.IMock; @@ -25,14 +36,20 @@ suite('Interpreters Conda Service', () => { let fileSystem: TypeMoq.IMock; let registryInterpreterLocatorService: TypeMoq.IMock; let serviceContainer: TypeMoq.IMock; + let procServiceFactory: TypeMoq.IMock; + let logger: TypeMoq.IMock; setup(async () => { - const logger = TypeMoq.Mock.ofType(); + logger = TypeMoq.Mock.ofType(); processService = TypeMoq.Mock.ofType(); platformService = TypeMoq.Mock.ofType(); registryInterpreterLocatorService = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); + procServiceFactory = TypeMoq.Mock.ofType(); + processService.setup((x: any) => x.then).returns(() => undefined); + procServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); + serviceContainer = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessService), TypeMoq.It.isAny())).returns(() => processService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())).returns(() => procServiceFactory.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())).returns(() => platformService.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ILogger), TypeMoq.It.isAny())).returns(() => logger.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())).returns(() => fileSystem.object); @@ -54,19 +71,19 @@ suite('Interpreters Conda Service', () => { test('Correctly identifies a python path as a conda environment (windows)', async () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); await identifyPythonPathAsCondaEnvironment(true, false, false, pythonPath); }); test('Correctly identifies a python path as a conda environment (linux)', async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); await identifyPythonPathAsCondaEnvironment(false, false, true, pythonPath); }); test('Correctly identifies a python path as a conda environment (osx)', async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); await identifyPythonPathAsCondaEnvironment(false, true, false, pythonPath); }); @@ -75,8 +92,8 @@ suite('Interpreters Conda Service', () => { platformService.setup(p => p.isWindows).returns(() => isWindows); platformService.setup(p => p.isMac).returns(() => isOsx); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(false)); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(false)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(false)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(false)); const isCondaEnv = await condaService.isCondaEnvironment(pythonPath); expect(isCondaEnv).to.be.equal(false, 'Path incorrectly identified as a conda path'); @@ -123,7 +140,7 @@ suite('Interpreters Conda Service', () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'one', 'python.exe'); const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); await checkCondaNameAndPathForCondaEnvironments(true, false, false, pythonPath, condaEnvDir, { name: 'One', path: path.dirname(pythonPath) }); }); @@ -131,7 +148,7 @@ suite('Interpreters Conda Service', () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'eight 8', 'python.exe'); const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); await checkCondaNameAndPathForCondaEnvironments(true, false, false, pythonPath, condaEnvDir, { name: 'Eight', path: path.dirname(pythonPath) }); }); @@ -139,7 +156,7 @@ suite('Interpreters Conda Service', () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'one', 'bin', 'python'); const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); await checkCondaNameAndPathForCondaEnvironments(false, true, false, pythonPath, condaEnvDir, { name: 'One', path: path.join(path.dirname(pythonPath), '..') }); }); @@ -147,7 +164,7 @@ suite('Interpreters Conda Service', () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'Eight 8', 'bin', 'python'); const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); await checkCondaNameAndPathForCondaEnvironments(false, true, false, pythonPath, condaEnvDir, { name: 'Eight', path: path.join(path.dirname(pythonPath), '..') }); }); @@ -155,7 +172,7 @@ suite('Interpreters Conda Service', () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'one', 'bin', 'python'); const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); await checkCondaNameAndPathForCondaEnvironments(false, false, true, pythonPath, condaEnvDir, { name: 'One', path: path.join(path.dirname(pythonPath), '..') }); }); @@ -163,7 +180,7 @@ suite('Interpreters Conda Service', () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'Eight 8', 'bin', 'python'); const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); await checkCondaNameAndPathForCondaEnvironments(false, false, true, pythonPath, condaEnvDir, { name: 'Eight', path: path.join(path.dirname(pythonPath), '..') }); }); @@ -183,7 +200,7 @@ suite('Interpreters Conda Service', () => { platformService.setup(p => p.isWindows).returns(() => true); platformService.setup(p => p.isMac).returns(() => false); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); const stateFactory = TypeMoq.Mock.ofType(); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory))).returns(() => stateFactory.object); const state = new MockState({ data: condaEnvironments }); @@ -224,7 +241,7 @@ suite('Interpreters Conda Service', () => { platformService.setup(p => p.isWindows).returns(() => true); platformService.setup(p => p.isMac).returns(() => false); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); const stateFactory = TypeMoq.Mock.ofType(); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory))).returns(() => stateFactory.object); const state = new MockState({ data: condaEnvironments }); @@ -252,16 +269,18 @@ suite('Interpreters Conda Service', () => { const condaPythonExePath = path.join('dumyPath', 'environments', 'conda', 'Scripts', 'python.exe'); const registryInterpreters: PythonInterpreter[] = [ { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1', type: InterpreterType.Unknown }, - { displayName: 'Anaconda', path: condaPythonExePath, companyDisplayName: 'Two 2', version: '1.11.0', type: InterpreterType.Unknown }, + { displayName: 'Anaconda', path: condaPythonExePath, companyDisplayName: 'Two 2', version: '1.11.0', enyTpe: InterpreterType.Unknown }, { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } - ]; + ].map(item => { + return { ...info, ...item }; + }); const condaInterpreterIndex = registryInterpreters.findIndex(i => i.displayName === 'Anaconda'); const expectedCodnaPath = path.join(path.dirname(registryInterpreters[condaInterpreterIndex].path), 'conda.exe'); platformService.setup(p => p.isWindows).returns(() => true); processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); registryInterpreterLocatorService.setup(r => r.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(registryInterpreters)); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isAny())).returns((file: string) => Promise.resolve(file === expectedCodnaPath)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isAny())).returns((file: string) => Promise.resolve(file === expectedCodnaPath)); const condaExe = await condaService.getCondaFile(); assert.equal(condaExe, expectedCodnaPath, 'Failed to identify conda.exe'); @@ -277,13 +296,15 @@ suite('Interpreters Conda Service', () => { { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda221', 'Scripts', 'python.exe'), companyDisplayName: 'Two 2.21', version: '2.21.0', type: InterpreterType.Unknown }, { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } - ]; + ].map(item => { + return { ...info, ...item }; + }); const indexOfLatestVersion = 3; const expectedCodnaPath = path.join(path.dirname(registryInterpreters[indexOfLatestVersion].path), 'conda.exe'); platformService.setup(p => p.isWindows).returns(() => true); processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); registryInterpreterLocatorService.setup(r => r.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(registryInterpreters)); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isAny())).returns((file: string) => Promise.resolve(file === expectedCodnaPath)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isAny())).returns((file: string) => Promise.resolve(file === expectedCodnaPath)); const condaExe = await condaService.getCondaFile(); assert.equal(condaExe, expectedCodnaPath, 'Failed to identify conda.exe'); @@ -299,11 +320,11 @@ suite('Interpreters Conda Service', () => { { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda221', 'Scripts', 'python.exe'), companyDisplayName: 'Two 2.21', version: '2.21.0', type: InterpreterType.Unknown }, { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } - ]; + ].map(item => { return { ...info, ...item }; }); platformService.setup(p => p.isWindows).returns(() => true); processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); registryInterpreterLocatorService.setup(r => r.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(registryInterpreters)); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isAny())).returns((file: string) => Promise.resolve(false)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isAny())).returns((file: string) => Promise.resolve(false)); const condaExe = await condaService.getCondaFile(); assert.equal(condaExe, 'conda', 'Failed to identify conda.exe'); @@ -335,22 +356,26 @@ suite('Interpreters Conda Service', () => { processService.verify(p => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); }); - KNOWN_CONDA_LOCATIONS.forEach(knownLocation => { - test(`Must return conda path from known location '${knownLocation}' (non windows)`, async () => { - const expectedCondaLocation = untildify(knownLocation); - platformService.setup(p => p.isWindows).returns(() => false); - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isAny())).returns((file: string) => Promise.resolve(file === expectedCondaLocation)); + ['~/anaconda/bin/conda', '~/miniconda/bin/conda', '~/anaconda2/bin/conda', + '~/miniconda2/bin/conda', '~/anaconda3/bin/conda', '~/miniconda3/bin/conda'] + .forEach(knownLocation => { + test(`Must return conda path from known location '${knownLocation}' (non windows)`, async () => { + const expectedCondaLocation = untildify(knownLocation); + platformService.setup(p => p.isWindows).returns(() => false); + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); + fileSystem.setup(fs => fs.search(TypeMoq.It.isAny())).returns(() => Promise.resolve([expectedCondaLocation])); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(expectedCondaLocation))).returns(() => Promise.resolve(true)); - const condaExe = await condaService.getCondaFile(); - assert.equal(condaExe, expectedCondaLocation, 'Failed to identify'); + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, expectedCondaLocation, 'Failed to identify'); + }); }); - }); test('Must return \'conda\' if conda could not be found in known locations', async () => { platformService.setup(p => p.isWindows).returns(() => false); processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isAny())).returns((file: string) => Promise.resolve(false)); + fileSystem.setup(fs => fs.search(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isAny())).returns((file: string) => Promise.resolve(false)); const condaExe = await condaService.getCondaFile(); assert.equal(condaExe, 'conda', 'Failed to identify'); @@ -371,17 +396,17 @@ suite('Interpreters Conda Service', () => { }); test('Returns condaInfo when conda exists', async () => { - const info = { + const expectedInfo = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy'), path.join(environmentsPath, 'conda', 'envs', 'scipy')], default_prefix: '', 'sys.version': '3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]' }; processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'xyz' })); - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['info', '--json']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: JSON.stringify(info) })); + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['info', '--json']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: JSON.stringify(expectedInfo) })); const condaInfo = await condaService.getCondaInfo(); - assert.deepEqual(condaInfo, info, 'Conda info does not match'); + assert.deepEqual(condaInfo, expectedInfo, 'Conda info does not match'); }); test('Returns undefined if there\'s and error in getting the info', async () => { @@ -395,7 +420,6 @@ suite('Interpreters Conda Service', () => { test('Returns conda environments when conda exists', async () => { const stateFactory = TypeMoq.Mock.ofType(); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory))).returns(() => stateFactory.object); - // tslint:disable-next-line:no-any const state = new MockState(undefined); stateFactory.setup(s => s.createGlobalPersistentState(TypeMoq.It.isValue('CONDA_ENVIRONMENTS'), TypeMoq.It.isValue(undefined))).returns(() => state); @@ -405,10 +429,24 @@ suite('Interpreters Conda Service', () => { assert.equal(environments, undefined, 'Conda environments do not match'); }); + test('Logs information message when conda does not exist', async () => { + const stateFactory = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory))).returns(() => stateFactory.object); + const state = new MockState(undefined); + stateFactory.setup(s => s.createGlobalPersistentState(TypeMoq.It.isValue('CONDA_ENVIRONMENTS'), TypeMoq.It.isValue(undefined))).returns(() => state); + + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['env', 'list']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); + logger.setup(l => l.logInformation(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .verifiable(TypeMoq.Times.once()); + const environments = await condaService.getCondaEnvironments(true); + assert.equal(environments, undefined, 'Conda environments do not match'); + logger.verifyAll(); + }); + test('Returns cached conda environments', async () => { const stateFactory = TypeMoq.Mock.ofType(); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory))).returns(() => stateFactory.object); - // tslint:disable-next-line:no-any const state = new MockState({ data: 'CachedInfo' }); stateFactory.setup(s => s.createGlobalPersistentState(TypeMoq.It.isValue('CONDA_ENVIRONMENTS'), TypeMoq.It.isValue(undefined))).returns(() => state); @@ -421,7 +459,6 @@ suite('Interpreters Conda Service', () => { test('Subsequent list of environments will be retrieved from cache', async () => { const stateFactory = TypeMoq.Mock.ofType(); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory))).returns(() => stateFactory.object); - // tslint:disable-next-line:no-any const state = new MockState(undefined); stateFactory.setup(s => s.createGlobalPersistentState(TypeMoq.It.isValue('CONDA_ENVIRONMENTS'), TypeMoq.It.isValue(undefined))).returns(() => state); @@ -460,13 +497,15 @@ suite('Interpreters Conda Service', () => { { displayName: 'Anaconda', path: condaPythonExePath, companyDisplayName: 'Two 2', version: '1.11.0', type: InterpreterType.Unknown }, { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } - ]; + ].map(item => { + return { ...info, ...item }; + }); const expectedCodaExe = path.join(path.dirname(condaPythonExePath), 'conda.exe'); platformService.setup(p => p.isWindows).returns(() => true); processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(expectedCodaExe))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(expectedCodaExe))).returns(() => Promise.resolve(true)); registryInterpreterLocatorService.setup(r => r.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(registryInterpreters)); const condaExe = await condaService.getCondaFile(); @@ -481,7 +520,7 @@ suite('Interpreters Conda Service', () => { test('isAvailable will return false if conda is not available', async () => { processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('not found'))); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); platformService.setup(p => p.isWindows).returns(() => false); const isAvailable = await condaService.isCondaAvailable(); @@ -504,7 +543,7 @@ suite('Interpreters Conda Service', () => { test('isCondaInCurrentPath will return false if conda is not available', async () => { processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('not found'))); - fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); platformService.setup(p => p.isWindows).returns(() => false); const isAvailable = await condaService.isCondaInCurrentPath(); @@ -527,17 +566,17 @@ suite('Interpreters Conda Service', () => { } test('Fails to identify an environment as a conda env (windows)', async () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'one', 'python.exe'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); await testFailureOfGettingCondaEnvironments(true, false, false, pythonPath); }); test('Fails to identify an environment as a conda env (linux)', async () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'one', 'python'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); await testFailureOfGettingCondaEnvironments(false, false, true, pythonPath); }); test('Fails to identify an environment as a conda env (osx)', async () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'one', 'python'); - fileSystem.setup(f => f.directoryExistsAsync(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); await testFailureOfGettingCondaEnvironments(false, true, false, pythonPath); }); }); diff --git a/src/test/interpreters/currentPathService.test.ts b/src/test/interpreters/currentPathService.test.ts new file mode 100644 index 000000000000..87949998b5e6 --- /dev/null +++ b/src/test/interpreters/currentPathService.test.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { IFileSystem } from '../../client/common/platform/types'; +import { IProcessService, IProcessServiceFactory } from '../../client/common/process/types'; +import { IConfigurationService, IPersistentState, IPersistentStateFactory, IPythonSettings } from '../../client/common/types'; +import { IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../client/interpreter/helpers'; +import { CurrentPathService } from '../../client/interpreter/locators/services/currentPathService'; +import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; +import { IServiceContainer } from '../../client/ioc/types'; + +suite('Interpreters CurrentPath Service', () => { + let processService: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + let serviceContainer: TypeMoq.IMock; + let virtualEnvironmentManager: TypeMoq.IMock; + let interpreterHelper: TypeMoq.IMock; + let pythonSettings: TypeMoq.IMock; + let currentPathService: CurrentPathService; + let persistentState: TypeMoq.IMock>; + setup(async () => { + processService = TypeMoq.Mock.ofType(); + virtualEnvironmentManager = TypeMoq.Mock.ofType(); + interpreterHelper = TypeMoq.Mock.ofType(); + const configurationService = TypeMoq.Mock.ofType(); + pythonSettings = TypeMoq.Mock.ofType(); + configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + const persistentStateFactory = TypeMoq.Mock.ofType(); + persistentState = TypeMoq.Mock.ofType>(); + processService.setup((x: any) => x.then).returns(() => undefined); + persistentState.setup(p => p.value).returns(() => undefined as any); + persistentState.setup(p => p.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + fileSystem = TypeMoq.Mock.ofType(); + persistentStateFactory.setup(p => p.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => persistentState.object); + const procServiceFactory = TypeMoq.Mock.ofType(); + procServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); + + serviceContainer = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IVirtualEnvironmentManager), TypeMoq.It.isAny())).returns(() => virtualEnvironmentManager.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterVersionService), TypeMoq.It.isAny())).returns(() => interpreterHelper.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())).returns(() => fileSystem.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory), TypeMoq.It.isAny())).returns(() => persistentStateFactory.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())).returns(() => configurationService.object); + + currentPathService = new CurrentPathService(virtualEnvironmentManager.object, interpreterHelper.object, procServiceFactory.object, serviceContainer.object); + }); + + test('Interpreters that do not exist on the file system are not excluded from the list', async () => { + // Specific test for 1305 + const version = 'mockVersion'; + const envName = 'mockEnvName'; + interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version })); + virtualEnvironmentManager.setup(v => v.getEnvironmentName(TypeMoq.It.isAny())).returns(() => Promise.resolve(envName)); + virtualEnvironmentManager.setup(v => v.getEnvironmentType(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(InterpreterType.VirtualEnv)); + + const execArgs = ['-c', 'import sys;print(sys.executable)']; + pythonSettings.setup(p => p.pythonPath).returns(() => 'root:Python'); + processService.setup(p => p.exec(TypeMoq.It.isValue('root:Python'), TypeMoq.It.isValue(execArgs), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'c:/root:python' })).verifiable(TypeMoq.Times.once()); + processService.setup(p => p.exec(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(execArgs), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'c:/python1' })).verifiable(TypeMoq.Times.once()); + processService.setup(p => p.exec(TypeMoq.It.isValue('python2'), TypeMoq.It.isValue(execArgs), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'c:/python2' })).verifiable(TypeMoq.Times.once()); + processService.setup(p => p.exec(TypeMoq.It.isValue('python3'), TypeMoq.It.isValue(execArgs), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'c:/python3' })).verifiable(TypeMoq.Times.once()); + + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue('c:/root:python'))).returns(() => Promise.resolve(true)).verifiable(TypeMoq.Times.once()); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue('c:/python1'))).returns(() => Promise.resolve(false)).verifiable(TypeMoq.Times.once()); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue('c:/python2'))).returns(() => Promise.resolve(false)).verifiable(TypeMoq.Times.once()); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue('c:/python3'))).returns(() => Promise.resolve(true)).verifiable(TypeMoq.Times.once()); + + const interpreters = await currentPathService.getInterpreters(); + processService.verifyAll(); + fileSystem.verifyAll(); + expect(interpreters).to.be.of.length(2); + expect(interpreters).to.deep.include({ version, envName, displayName: `${version} (${envName})`, path: 'c:/root:python', type: InterpreterType.VirtualEnv }); + expect(interpreters).to.deep.include({ version, envName, displayName: `${version} (${envName})`, path: 'c:/python3', type: InterpreterType.VirtualEnv }); + }); +}); diff --git a/src/test/interpreters/display.test.ts b/src/test/interpreters/display.test.ts index fad85341c2a4..b87c1ac7698f 100644 --- a/src/test/interpreters/display.test.ts +++ b/src/test/interpreters/display.test.ts @@ -4,13 +4,26 @@ import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, Disposable, StatusBarAlignment, StatusBarItem, Uri, WorkspaceFolder } from 'vscode'; import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; -import { IFileSystem } from '../../client/common/platform/types'; +import { Architecture, IFileSystem } from '../../client/common/platform/types'; import { IConfigurationService, IDisposableRegistry, IPythonSettings } from '../../client/common/types'; -import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService, IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; +import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { InterpreterDisplay } from '../../client/interpreter/display'; import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; import { IServiceContainer } from '../../client/ioc/types'; +const info: PythonInterpreter = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + type: InterpreterType.Unknown, + version: '', + version_info: [0, 0, 0, 'alpha'], + sysPrefix: '', + sysVersion: '' +}; + // tslint:disable-next-line:max-func-body-length suite('Interpreters Display', () => { let applicationShell: TypeMoq.IMock; @@ -18,7 +31,6 @@ suite('Interpreters Display', () => { let serviceContainer: TypeMoq.IMock; let interpreterService: TypeMoq.IMock; let virtualEnvMgr: TypeMoq.IMock; - let versionProvider: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; let disposableRegistry: Disposable[]; let statusBar: TypeMoq.IMock; @@ -32,7 +44,6 @@ suite('Interpreters Display', () => { applicationShell = TypeMoq.Mock.ofType(); interpreterService = TypeMoq.Mock.ofType(); virtualEnvMgr = TypeMoq.Mock.ofType(); - versionProvider = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); interpreterHelper = TypeMoq.Mock.ofType(); disposableRegistry = []; @@ -44,7 +55,6 @@ suite('Interpreters Display', () => { serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => applicationShell.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterService))).returns(() => interpreterService.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IVirtualEnvironmentManager))).returns(() => virtualEnvMgr.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterVersionService))).returns(() => versionProvider.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => disposableRegistry); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configurationService.object); @@ -72,6 +82,7 @@ suite('Interpreters Display', () => { const resource = Uri.file('x'); const workspaceFolder = Uri.file('workspace'); const activeInterpreter: PythonInterpreter = { + ...info, displayName: 'Dummy_Display_Name', type: InterpreterType.Unknown, path: path.join('user', 'development', 'env', 'bin', 'python') @@ -89,6 +100,7 @@ suite('Interpreters Display', () => { const resource = Uri.file('x'); const workspaceFolder = Uri.file('workspace'); const activeInterpreter: PythonInterpreter = { + ...info, displayName: 'Dummy_Display_Name', type: InterpreterType.Unknown, companyDisplayName: 'Company Name', @@ -114,7 +126,7 @@ suite('Interpreters Display', () => { configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('')); - versionProvider.setup(v => v.getVersion(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())).returns((_path, defaultDisplayName) => Promise.resolve(defaultDisplayName)); + interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(undefined)); await interpreterDisplay.refresh(resource); @@ -132,7 +144,7 @@ suite('Interpreters Display', () => { pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); // tslint:disable-next-line:no-any virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('Mock Name')); - versionProvider.setup(v => v.getVersion(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())).returns((_path, defaultDisplayName) => Promise.resolve(defaultDisplayName)); + interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(undefined)); await interpreterDisplay.refresh(resource); @@ -149,9 +161,8 @@ suite('Interpreters Display', () => { interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve(undefined)); configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); - fileSystem.setup(f => f.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(false)); - const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; - versionProvider.setup(v => v.getVersion(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())).returns(() => Promise.resolve(defaultDisplayName)); + fileSystem.setup(f => f.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(false)); + interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(undefined)); virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('')); await interpreterDisplay.refresh(resource); @@ -169,9 +180,9 @@ suite('Interpreters Display', () => { interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve(undefined)); configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); - fileSystem.setup(f => f.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; - versionProvider.setup(v => v.getVersion(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())).returns(() => Promise.resolve(defaultDisplayName)); + interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(undefined)); // tslint:disable-next-line:no-any virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('Mock Env Name')); const expectedText = `${defaultDisplayName} (Mock Env Name)`; @@ -190,9 +201,9 @@ suite('Interpreters Display', () => { interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve(undefined)); configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); - fileSystem.setup(f => f.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + fileSystem.setup(f => f.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); const displayName = 'Version from Interperter'; - versionProvider.setup(v => v.getVersion(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())).returns(() => Promise.resolve(displayName)); + interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve({ version: displayName })); // tslint:disable-next-line:no-any virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('')); @@ -204,6 +215,7 @@ suite('Interpreters Display', () => { const workspaceFolder = Uri.file('x'); const resource = workspaceFolder; const activeInterpreter: PythonInterpreter = { + ...info, displayName: 'Dummy_Display_Name', type: InterpreterType.Unknown, companyDisplayName: 'Company Name', @@ -211,6 +223,7 @@ suite('Interpreters Display', () => { }; interpreterService.setup(i => i.getInterpreters(TypeMoq.It.isValue(resource))).returns(() => Promise.resolve([])); interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(resource))).returns(() => Promise.resolve(activeInterpreter)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); const expectedTooltip = `${activeInterpreter.path}${EOL}${activeInterpreter.companyDisplayName}`; interpreterHelper.setup(i => i.getActiveWorkspaceUri()).returns(() => { return { folderUri: workspaceFolder, configTarget: ConfigurationTarget.Workspace }; }); diff --git a/src/test/interpreters/helper.test.ts b/src/test/interpreters/helper.test.ts index ecab21922e6c..cca633519d2d 100644 --- a/src/test/interpreters/helper.test.ts +++ b/src/test/interpreters/helper.test.ts @@ -7,7 +7,10 @@ import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, TextDocument, TextEditor, Uri } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { Architecture } from '../../client/common/platform/types'; +import { InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { InterpreterHelper } from '../../client/interpreter/helpers'; +import { fixInterpreterDisplayName } from '../../client/interpreter/locators/helpers'; import { IServiceContainer } from '../../client/ioc/types'; // tslint:disable-next-line:max-func-body-length @@ -28,7 +31,7 @@ suite('Interpreters Display Helper', () => { }); test('getActiveWorkspaceUri should return undefined if there are no workspaces', () => { workspaceService.setup(w => w.workspaceFolders).returns(() => []); - + documentManager.setup(doc => doc.activeTextEditor).returns(() => undefined); const workspace = helper.getActiveWorkspaceUri(); expect(workspace).to.be.equal(undefined, 'incorrect value'); }); @@ -85,4 +88,29 @@ suite('Interpreters Display Helper', () => { expect(workspace!.folderUri).to.be.equal(documentWorkspaceFolderUri); expect(workspace!.configTarget).to.be.equal(ConfigurationTarget.WorkspaceFolder); }); + test('Ensure Python prefix is added to displayName', () => { + const interpreter: PythonInterpreter = { + path: '', + type: InterpreterType.Unknown, + version: 'Something', + sysPrefix: '', + architecture: Architecture.Unknown, + sysVersion: '', + version_info: [0, 0, 0, 'alpha'] + }; + const expectedDisplayName = `Python ${interpreter.version!}`; + expect(fixInterpreterDisplayName(interpreter)).to.have.property('displayName', expectedDisplayName); + }); + test('Ensure Python prefix is not added to displayName', () => { + const interpreter: PythonInterpreter = { + path: '', + type: InterpreterType.Unknown, + version: 'Python Something', + sysPrefix: '', + architecture: Architecture.Unknown, + sysVersion: '', + version_info: [0, 0, 0, 'alpha'] + }; + expect(fixInterpreterDisplayName(interpreter)).to.have.property('displayName', interpreter.version); + }); }); diff --git a/src/test/interpreters/interpreterService.test.ts b/src/test/interpreters/interpreterService.test.ts index b0c1bc64f1c7..f6033919d9b0 100644 --- a/src/test/interpreters/interpreterService.test.ts +++ b/src/test/interpreters/interpreterService.test.ts @@ -2,14 +2,18 @@ // Licensed under the MIT License. import { expect } from 'chai'; +import { EventEmitter } from 'events'; import { Container } from 'inversify'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { IFileSystem } from '../../client/common/platform/types'; +import { ConfigurationTarget, Disposable, TextDocument, TextEditor, Uri, WorkspaceConfiguration } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { noop } from '../../client/common/core.utils'; +import { Architecture, IFileSystem } from '../../client/common/platform/types'; +import { IConfigurationService, IDisposableRegistry } from '../../client/common/types'; import { IPythonPathUpdaterServiceManager } from '../../client/interpreter/configuration/types'; import { + IInterpreterDisplay, IInterpreterHelper, IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE, @@ -23,6 +27,19 @@ import { InterpreterService } from '../../client/interpreter/interpreterService' import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; +const info: PythonInterpreter = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + type: InterpreterType.Unknown, + version: '', + version_info: [0, 0, 0, 'alpha'], + sysPrefix: '', + sysVersion: '' +}; + // tslint:disable-next-line:max-func-body-length suite('Interpreters service', () => { let serviceManager: ServiceManager; @@ -35,6 +52,7 @@ suite('Interpreters service', () => { let pipenvLocator: TypeMoq.IMock; let wksLocator: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; + let interpreterDisplay: TypeMoq.IMock; setup(async () => { const cont = new Container(); @@ -47,13 +65,16 @@ suite('Interpreters service', () => { workspace = TypeMoq.Mock.ofType(); config = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); + interpreterDisplay = TypeMoq.Mock.ofType(); workspace.setup(x => x.getConfiguration('python', TypeMoq.It.isAny())).returns(() => config.object); + serviceManager.addSingletonInstance(IDisposableRegistry, []); serviceManager.addSingletonInstance(IInterpreterHelper, helper.object); serviceManager.addSingletonInstance(IPythonPathUpdaterServiceManager, updater.object); serviceManager.addSingletonInstance(IWorkspaceService, workspace.object); serviceManager.addSingletonInstance(IInterpreterLocatorService, locator.object, INTERPRETER_LOCATOR_SERVICE); serviceManager.addSingletonInstance(IFileSystem, fileSystem.object); + serviceManager.addSingletonInstance(IInterpreterDisplay, interpreterDisplay.object); pipenvLocator = TypeMoq.Mock.ofType(); wksLocator = TypeMoq.Mock.ofType(); @@ -77,6 +98,7 @@ suite('Interpreters service', () => { return { key: 'python' }; }); const interpreter: PythonInterpreter = { + ...info, path: path.join(path.sep, 'folder', 'py1', 'bin', 'python.exe'), type: InterpreterType.Unknown }; @@ -99,6 +121,7 @@ suite('Interpreters service', () => { return { key: 'python', workspaceValue: 'python' }; }); const interpreter: PythonInterpreter = { + ...info, path: 'python', type: InterpreterType.VirtualEnv }; @@ -112,6 +135,7 @@ suite('Interpreters service', () => { return { key: 'python', workspaceValue: 'elsewhere' }; }); const interpreter: PythonInterpreter = { + ...info, path: 'elsewhere', type: InterpreterType.Unknown }; @@ -127,6 +151,7 @@ suite('Interpreters service', () => { }); const intPath = path.join(path.sep, 'root', 'under', 'bin', 'python.exe'); const interpreter: PythonInterpreter = { + ...info, path: intPath, type: InterpreterType.Unknown }; @@ -183,4 +208,51 @@ suite('Interpreters service', () => { serviceManager.addSingletonInstance(IInterpreterLocatorService, wksLocator.object, WORKSPACE_VIRTUAL_ENV_SERVICE); } + + test('Changes to active document should invoke intrepreter.refresh method', async () => { + const service = new InterpreterService(serviceContainer); + const configService = TypeMoq.Mock.ofType(); + const documentManager = TypeMoq.Mock.ofType(); + + let activeTextEditorChangeHandler: Function | undefined; + documentManager.setup(d => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(handler => { + activeTextEditorChangeHandler = handler; + return { dispose: noop }; + }); + serviceManager.addSingletonInstance(IConfigurationService, configService.object); + serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); + + // tslint:disable-next-line:no-any + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => new EventEmitter() as any); + service.initialize(); + const textEditor = TypeMoq.Mock.ofType(); + const uri = Uri.file(path.join('usr', 'file.py')); + const document = TypeMoq.Mock.ofType(); + textEditor.setup(t => t.document).returns(() => document.object); + document.setup(d => d.uri).returns(() => uri); + activeTextEditorChangeHandler!(textEditor.object); + + interpreterDisplay.verify(i => i.refresh(TypeMoq.It.isValue(uri)), TypeMoq.Times.once()); + }); + + test('If there is no active document then intrepreter.refresh should not be invoked', async () => { + const service = new InterpreterService(serviceContainer); + const configService = TypeMoq.Mock.ofType(); + const documentManager = TypeMoq.Mock.ofType(); + + let activeTextEditorChangeHandler: Function | undefined; + documentManager.setup(d => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(handler => { + activeTextEditorChangeHandler = handler; + return { dispose: noop }; + }); + serviceManager.addSingletonInstance(IConfigurationService, configService.object); + serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); + + // tslint:disable-next-line:no-any + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => new EventEmitter() as any); + service.initialize(); + activeTextEditorChangeHandler!(); + + interpreterDisplay.verify(i => i.refresh(TypeMoq.It.isValue(undefined)), TypeMoq.Times.never()); + }); }); diff --git a/src/test/interpreters/interpreterVersion.test.ts b/src/test/interpreters/interpreterVersion.test.ts index 6bd728d669ab..63b0dc45b3b9 100644 --- a/src/test/interpreters/interpreterVersion.test.ts +++ b/src/test/interpreters/interpreterVersion.test.ts @@ -4,9 +4,10 @@ import { assert, expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import '../../client/common/extensions'; -import { IProcessService } from '../../client/common/process/types'; +import { IProcessServiceFactory } from '../../client/common/process/types'; import { IInterpreterVersionService } from '../../client/interpreter/contracts'; import { PIP_VERSION_REGEX } from '../../client/interpreter/interpreterVersion'; +import { PYTHON_PATH } from '../common'; import { initialize, initializeTest } from '../initialize'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; @@ -30,11 +31,11 @@ suite('Interpreters display version', () => { } test('Must return the Python Version', async () => { - const pythonProcess = ioc.serviceContainer.get(IProcessService); - const output = await pythonProcess.exec('python', ['--version'], { cwd: __dirname, mergeStdOutErr: true }); + const pythonProcess = await ioc.serviceContainer.get(IProcessServiceFactory).create(); + const output = await pythonProcess.exec(PYTHON_PATH, ['--version'], { cwd: __dirname, mergeStdOutErr: true }); const version = output.stdout.splitLines()[0]; const interpreterVersion = ioc.serviceContainer.get(IInterpreterVersionService); - const pyVersion = await interpreterVersion.getVersion('python', 'DEFAULT_TEST_VALUE'); + const pyVersion = await interpreterVersion.getVersion(PYTHON_PATH, 'DEFAULT_TEST_VALUE'); assert.equal(pyVersion, version, 'Incorrect version'); }); test('Must return the default value when Python path is invalid', async () => { @@ -42,9 +43,9 @@ suite('Interpreters display version', () => { const pyVersion = await interpreterVersion.getVersion('INVALID_INTERPRETER', 'DEFAULT_TEST_VALUE'); assert.equal(pyVersion, 'DEFAULT_TEST_VALUE', 'Incorrect version'); }); - test('Must return the pip Version', async () => { - const pythonProcess = ioc.serviceContainer.get(IProcessService); - const result = await pythonProcess.exec('python', ['-m', 'pip', '--version'], { cwd: __dirname, mergeStdOutErr: true }); + test('Must return the pip Version.', async () => { + const pythonProcess = await ioc.serviceContainer.get(IProcessServiceFactory).create(); + const result = await pythonProcess.exec(PYTHON_PATH, ['-m', 'pip', '--version'], { cwd: __dirname, mergeStdOutErr: true }); const output = result.stdout.splitLines()[0]; // Take the second part, see below example. // pip 9.0.1 from /Users/donjayamanne/anaconda3/lib/python3.6/site-packages (python 3.6). @@ -55,11 +56,11 @@ suite('Interpreters display version', () => { assert.isAtLeast(matches!.length, 1, 'Version number not found'); const interpreterVersion = ioc.serviceContainer.get(IInterpreterVersionService); - const pipVersionPromise = interpreterVersion.getPipVersion('python'); + const pipVersionPromise = interpreterVersion.getPipVersion(PYTHON_PATH); // tslint:disable-next-line:no-non-null-assertion await expect(pipVersionPromise).to.eventually.equal(matches![0].trim()); }); - test('Must throw an exceptionn when pip version cannot be determine', async () => { + test('Must throw an exception when pip version cannot be determined', async () => { const interpreterVersion = ioc.serviceContainer.get(IInterpreterVersionService); const pipVersionPromise = interpreterVersion.getPipVersion('INVALID_INTERPRETER'); await expect(pipVersionPromise).to.be.rejectedWith(); diff --git a/src/test/interpreters/pipEnvService.unit.test.ts b/src/test/interpreters/pipEnvService.unit.test.ts new file mode 100644 index 000000000000..aba7709b35be --- /dev/null +++ b/src/test/interpreters/pipEnvService.unit.test.ts @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; +import { EnumEx } from '../../client/common/enumUtils'; +import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; +import { IProcessService, IProcessServiceFactory } from '../../client/common/process/types'; +import { ICurrentProcess, ILogger, IPersistentState, IPersistentStateFactory } from '../../client/common/types'; +import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; +import { IInterpreterHelper, IInterpreterLocatorService } from '../../client/interpreter/contracts'; +import { PipEnvService } from '../../client/interpreter/locators/services/pipEnvService'; +import { IServiceContainer } from '../../client/ioc/types'; + +enum OS { + Mac, Windows, Linux +} + +suite('Interpreters - PipEnv', () => { + const rootWorkspace = Uri.file(path.join('usr', 'desktop', 'wkspc1')).fsPath; + EnumEx.getNamesAndValues(OS).forEach(os => { + [undefined, Uri.file(path.join(rootWorkspace, 'one.py'))].forEach(resource => { + const testSuffix = ` (${os.name}, ${resource ? 'with' : 'without'} a workspace)`; + + let pipEnvService: IInterpreterLocatorService; + let serviceContainer: TypeMoq.IMock; + let interpreterHelper: TypeMoq.IMock; + let processService: TypeMoq.IMock; + let currentProcess: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + let appShell: TypeMoq.IMock; + let persistentStateFactory: TypeMoq.IMock; + let envVarsProvider: TypeMoq.IMock; + let procServiceFactory: TypeMoq.IMock; + let logger: TypeMoq.IMock; + let platformService: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + const workspaceService = TypeMoq.Mock.ofType(); + interpreterHelper = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + processService = TypeMoq.Mock.ofType(); + appShell = TypeMoq.Mock.ofType(); + currentProcess = TypeMoq.Mock.ofType(); + persistentStateFactory = TypeMoq.Mock.ofType(); + envVarsProvider = TypeMoq.Mock.ofType(); + procServiceFactory = TypeMoq.Mock.ofType(); + logger = TypeMoq.Mock.ofType(); + platformService = TypeMoq.Mock.ofType(); + processService.setup((x: any) => x.then).returns(() => undefined); + procServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); + + // tslint:disable-next-line:no-any + const persistentState = TypeMoq.Mock.ofType>(); + persistentStateFactory.setup(p => p.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => persistentState.object); + persistentStateFactory.setup(p => p.createWorkspacePersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => persistentState.object); + persistentState.setup(p => p.value).returns(() => undefined); + persistentState.setup(p => p.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + + const workspaceFolder = TypeMoq.Mock.ofType(); + workspaceFolder.setup(w => w.uri).returns(() => Uri.file(rootWorkspace)); + workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); + workspaceService.setup(w => w.rootPath).returns(() => rootWorkspace); + + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())).returns(() => procServiceFactory.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterHelper))).returns(() => interpreterHelper.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICurrentProcess))).returns(() => currentProcess.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory))).returns(() => persistentStateFactory.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider))).returns(() => envVarsProvider.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ILogger))).returns(() => logger.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.object); + + pipEnvService = new PipEnvService(serviceContainer.object); + }); + + test(`Should return an empty list'${testSuffix}`, () => { + const environments = pipEnvService.getInterpreters(resource); + expect(environments).to.be.eventually.deep.equal([]); + }); + test(`Should return an empty list if there is no \'PipFile\'${testSuffix}`, async () => { + const env = {}; + envVarsProvider.setup(e => e.getEnvironmentVariables(TypeMoq.It.isAny())).returns(() => Promise.resolve({})).verifiable(TypeMoq.Times.once()); + currentProcess.setup(c => c.env).returns(() => env); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))).returns(() => Promise.resolve(false)).verifiable(TypeMoq.Times.once()); + const environments = await pipEnvService.getInterpreters(resource); + + expect(environments).to.be.deep.equal([]); + fileSystem.verifyAll(); + }); + test(`Should display warning message if there is a \'PipFile\' but \'pipenv --venv\' failes ${testSuffix}`, async () => { + const env = {}; + currentProcess.setup(c => c.env).returns(() => env); + processService.setup(p => p.exec(TypeMoq.It.isValue('pipenv'), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.reject('')); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))).returns(() => Promise.resolve(true)); + appShell.setup(a => a.showWarningMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('')).verifiable(TypeMoq.Times.once()); + logger.setup(l => l.logWarning(TypeMoq.It.isAny(), TypeMoq.It.isAny())).verifiable(TypeMoq.Times.exactly(2)); + const environments = await pipEnvService.getInterpreters(resource); + + expect(environments).to.be.deep.equal([]); + appShell.verifyAll(); + logger.verifyAll(); + }); + test(`Should display warning message if there is a \'PipFile\' but \'pipenv --venv\' failes with stderr ${testSuffix}`, async () => { + const env = {}; + currentProcess.setup(c => c.env).returns(() => env); + processService.setup(p => p.exec(TypeMoq.It.isValue('pipenv'), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stderr: 'PipEnv Failed', stdout: '' })); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))).returns(() => Promise.resolve(true)); + appShell.setup(a => a.showWarningMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('')).verifiable(TypeMoq.Times.once()); + logger.setup(l => l.logWarning(TypeMoq.It.isAny(), TypeMoq.It.isAny())).verifiable(TypeMoq.Times.exactly(2)); + const environments = await pipEnvService.getInterpreters(resource); + + expect(environments).to.be.deep.equal([]); + appShell.verifyAll(); + logger.verifyAll(); + }); + test(`Should return interpreter information${testSuffix}`, async () => { + const env = {}; + const pythonPath = 'one'; + envVarsProvider.setup(e => e.getEnvironmentVariables(TypeMoq.It.isAny())).returns(() => Promise.resolve({})).verifiable(TypeMoq.Times.once()); + currentProcess.setup(c => c.env).returns(() => env); + processService.setup(p => p.exec(TypeMoq.It.isValue('pipenv'), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: pythonPath })); + interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: 'xyz' })); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))).returns(() => Promise.resolve(true)).verifiable(); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)).verifiable(); + const environments = await pipEnvService.getInterpreters(resource); + + expect(environments).to.be.lengthOf(1); + fileSystem.verifyAll(); + }); + test(`Should return interpreter information using PipFile defined in Env variable${testSuffix}`, async () => { + const envPipFile = 'XYZ'; + const env = { + PIPENV_PIPFILE: envPipFile + }; + const pythonPath = 'one'; + envVarsProvider.setup(e => e.getEnvironmentVariables(TypeMoq.It.isAny())).returns(() => Promise.resolve({})).verifiable(TypeMoq.Times.once()); + currentProcess.setup(c => c.env).returns(() => env); + processService.setup(p => p.exec(TypeMoq.It.isValue('pipenv'), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: pythonPath })); + interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: 'xyz' })); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))).returns(() => Promise.resolve(false)).verifiable(TypeMoq.Times.never()); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, envPipFile)))).returns(() => Promise.resolve(true)).verifiable(TypeMoq.Times.once()); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)).verifiable(); + const environments = await pipEnvService.getInterpreters(resource); + + expect(environments).to.be.lengthOf(1); + fileSystem.verifyAll(); + }); + }); + }); +}); diff --git a/src/test/interpreters/virtualEnvManager.test.ts b/src/test/interpreters/virtualEnvManager.test.ts index bfd498b55de3..de16e18c03bd 100644 --- a/src/test/interpreters/virtualEnvManager.test.ts +++ b/src/test/interpreters/virtualEnvManager.test.ts @@ -1,20 +1,27 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// tslint:disable:no-any + import { expect } from 'chai'; import { Container } from 'inversify'; import * as TypeMoq from 'typemoq'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { FileSystem } from '../../client/common/platform/fileSystem'; +import { PlatformService } from '../../client/common/platform/platformService'; +import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; import { BufferDecoder } from '../../client/common/process/decoder'; import { ProcessService } from '../../client/common/process/proc'; -import { IBufferDecoder, IProcessService } from '../../client/common/process/types'; +import { IBufferDecoder, IProcessService, IProcessServiceFactory } from '../../client/common/process/types'; +import { IPipEnvService } from '../../client/interpreter/contracts'; import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; +import { PYTHON_PATH } from '../common'; suite('Virtual environment manager', () => { let serviceManager: ServiceManager; let serviceContainer: ServiceContainer; - let process: TypeMoq.IMock; setup(async () => { const cont = new Container(); @@ -22,32 +29,44 @@ suite('Virtual environment manager', () => { serviceContainer = new ServiceContainer(cont); }); - test('Plain Python environment suffix', async () => await testSuffix('')); - test('Venv environment suffix', async () => await testSuffix('venv')); - test('Virtualenv Python environment suffix', async () => await testSuffix('virtualenv')); + test('Plain Python environment suffix', async () => testSuffix('')); + test('Venv environment suffix', async () => testSuffix('venv')); + test('Virtualenv Python environment suffix', async () => testSuffix('virtualenv')); test('Run actual virtual env detection code', async () => { - serviceManager.addSingleton(IProcessService, ProcessService); + const processServiceFactory = TypeMoq.Mock.ofType(); + processServiceFactory.setup(f => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(new ProcessService(new BufferDecoder(), process.env as any))); + serviceManager.addSingletonInstance(IProcessServiceFactory, processServiceFactory.object); serviceManager.addSingleton(IBufferDecoder, BufferDecoder); + serviceManager.addSingleton(IFileSystem, FileSystem); + serviceManager.addSingleton(IPlatformService, PlatformService); + serviceManager.addSingletonInstance(IPipEnvService, TypeMoq.Mock.ofType().object); + serviceManager.addSingletonInstance(IWorkspaceService, TypeMoq.Mock.ofType().object); const venvManager = new VirtualEnvironmentManager(serviceContainer); - const name = await venvManager.getEnvironmentName('python'); + const name = await venvManager.getEnvironmentName(PYTHON_PATH); const result = name === '' || name === 'venv' || name === 'virtualenv'; expect(result).to.be.equal(true, 'Running venv detection code failed.'); }); async function testSuffix(expectedName: string) { - process = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IProcessService, process.object); + const processService = TypeMoq.Mock.ofType(); + const processServiceFactory = TypeMoq.Mock.ofType(); + processService.setup((x: any) => x.then).returns(() => undefined); + processServiceFactory.setup(f => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); + serviceManager.addSingletonInstance(IProcessServiceFactory, processServiceFactory.object); + serviceManager.addSingletonInstance(IFileSystem, TypeMoq.Mock.ofType().object); + serviceManager.addSingletonInstance(IPipEnvService, TypeMoq.Mock.ofType().object); + serviceManager.addSingletonInstance(IWorkspaceService, TypeMoq.Mock.ofType().object); const venvManager = new VirtualEnvironmentManager(serviceContainer); - process - .setup(x => x.exec('python', TypeMoq.It.isAny())) + processService + .setup(x => x.exec(PYTHON_PATH, TypeMoq.It.isAny())) .returns(() => Promise.resolve({ stdout: expectedName, stderr: '' })); - const name = await venvManager.getEnvironmentName('python'); + const name = await venvManager.getEnvironmentName(PYTHON_PATH); expect(name).to.be.equal(expectedName, 'Virtual envrironment name suffix is incorrect.'); } }); diff --git a/src/test/interpreters/windowsRegistryService.test.ts b/src/test/interpreters/windowsRegistryService.test.ts deleted file mode 100644 index eba551ea4b50..000000000000 --- a/src/test/interpreters/windowsRegistryService.test.ts +++ /dev/null @@ -1,288 +0,0 @@ -import * as assert from 'assert'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { Architecture, RegistryHive } from '../../client/common/platform/types'; -import { IPersistentStateFactory } from '../../client/common/types'; -import { IS_WINDOWS } from '../../client/debugger/Common/Utils'; -import { WindowsRegistryService } from '../../client/interpreter/locators/services/windowsRegistryService'; -import { IServiceContainer } from '../../client/ioc/types'; -import { initialize, initializeTest } from '../initialize'; -import { MockRegistry, MockState } from './mocks'; - -const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); - -// tslint:disable-next-line:max-func-body-length -suite('Interpreters from Windows Registry', () => { - let serviceContainer: TypeMoq.IMock; - suiteSetup(initialize); - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - const stateFactory = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory))).returns(() => stateFactory.object); - const state = new MockState(undefined); - stateFactory.setup(s => s.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => state); - return initializeTest(); - }); - if (IS_WINDOWS) { - test('Must return an empty list (x86)', async () => { - const registry = new MockRegistry([], []); - const winRegistry = new WindowsRegistryService(registry, false, serviceContainer.object); - - const interpreters = await winRegistry.getInterpreters(); - assert.equal(interpreters.length, 0, 'Incorrect number of entries'); - }); - test('Must return an empty list (x64)', async () => { - const registry = new MockRegistry([], []); - const winRegistry = new WindowsRegistryService(registry, true, serviceContainer.object); - - const interpreters = await winRegistry.getInterpreters(); - assert.equal(interpreters.length, 0, 'Incorrect number of entries'); - }); - test('Must return a single entry', async () => { - const registryKeys = [ - { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One'] }, - { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1'] } - ]; - const registryValues = [ - { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1', 'one.exe'), name: 'ExecutablePath' }, - { key: '\\Software\\Python\\Company One\\Tag1', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag1', name: 'SysVersion' }, - { key: '\\Software\\Python\\Company One\\Tag1', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' } - ]; - const registry = new MockRegistry(registryKeys, registryValues); - const winRegistry = new WindowsRegistryService(registry, false, serviceContainer.object); - - const interpreters = await winRegistry.getInterpreters(); - - assert.equal(interpreters.length, 1, 'Incorrect number of entries'); - assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); - assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); - assert.equal(interpreters[0].displayName, 'DisplayName.Tag1', 'Incorrect display name'); - assert.equal(interpreters[0].path, path.join(environmentsPath, 'path1', 'one.exe'), 'Incorrect executable path'); - assert.equal(interpreters[0].version, 'Version.Tag1', 'Incorrect version'); - }); - test('Must default names for PythonCore and exe', async () => { - const registryKeys = [ - { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PythonCore'] }, - { key: '\\Software\\Python\\PythonCore', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PythonCore\\Tag1'] } - ]; - const registryValues = [ - { key: '\\Software\\Python\\PythonCore\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') } - ]; - const registry = new MockRegistry(registryKeys, registryValues); - const winRegistry = new WindowsRegistryService(registry, false, serviceContainer.object); - - const interpreters = await winRegistry.getInterpreters(); - - assert.equal(interpreters.length, 1, 'Incorrect number of entries'); - assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); - assert.equal(interpreters[0].companyDisplayName, 'Python Software Foundation', 'Incorrect company name'); - assert.equal(interpreters[0].displayName, undefined, 'Incorrect display name'); - assert.equal(interpreters[0].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); - assert.equal(interpreters[0].version, 'Tag1', 'Incorrect version'); - }); - test('Must ignore company \'PyLauncher\'', async () => { - const registryKeys = [ - { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PyLauncher'] }, - { key: '\\Software\\Python\\PythonCore', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PyLauncher\\Tag1'] } - ]; - const registryValues = [ - { key: '\\Software\\Python\\PyLauncher\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'c:/temp/Install Path Tag1' } - ]; - const registry = new MockRegistry(registryKeys, registryValues); - const winRegistry = new WindowsRegistryService(registry, false, serviceContainer.object); - - const interpreters = await winRegistry.getInterpreters(); - - assert.equal(interpreters.length, 0, 'Incorrect number of entries'); - }); - test('Must return a single entry and when registry contains only the InstallPath', async () => { - const registryKeys = [ - { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One'] }, - { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1'] } - ]; - const registryValues = [ - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') } - ]; - const registry = new MockRegistry(registryKeys, registryValues); - const winRegistry = new WindowsRegistryService(registry, false, serviceContainer.object); - - const interpreters = await winRegistry.getInterpreters(); - - assert.equal(interpreters.length, 1, 'Incorrect number of entries'); - assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); - assert.equal(interpreters[0].companyDisplayName, 'Company One', 'Incorrect company name'); - assert.equal(interpreters[0].displayName, undefined, 'Incorrect display name'); - assert.equal(interpreters[0].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); - assert.equal(interpreters[0].version, 'Tag1', 'Incorrect version'); - }); - test('Must return multiple entries', async () => { - const registryKeys = [ - { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One', '\\Software\\Python\\Company Two', '\\Software\\Python\\Company Three'] }, - { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1', '\\Software\\Python\\Company One\\Tag2'] }, - { key: '\\Software\\Python\\Company Two', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Two\\Tag A', '\\Software\\Python\\Company Two\\Tag B', '\\Software\\Python\\Company Two\\Tag C'] }, - { key: '\\Software\\Python\\Company Three', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Three\\Tag !'] }, - { key: '\\Software\\Python', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['A'] }, - { key: '\\Software\\Python\\Company A', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['Another Tag'] } - ]; - const registryValues = [ - { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1', 'python.exe'), name: 'ExecutablePath' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2'), name: 'SysVersion' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' }, - - { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2') }, - { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2', 'python.exe'), name: 'ExecutablePath' }, - - { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path3') }, - { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag A', name: 'SysVersion' }, - - { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, - { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag B', name: 'DisplayName' }, - { key: '\\Software\\Python\\Company Two\\Tag C\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy') }, - - { key: '\\Software\\Python\\Company Three\\Tag !\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, - - { key: '\\Software\\Python\\Company A\\Another Tag\\InstallPath', hive: RegistryHive.HKLM, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe') } - ]; - const registry = new MockRegistry(registryKeys, registryValues); - const winRegistry = new WindowsRegistryService(registry, false, serviceContainer.object); - - const interpreters = await winRegistry.getInterpreters(); - - assert.equal(interpreters.length, 4, 'Incorrect number of entries'); - assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); - assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); - assert.equal(interpreters[0].displayName, undefined, 'Incorrect display name'); - assert.equal(interpreters[0].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); - assert.equal(interpreters[0].version, 'Tag1', 'Incorrect version'); - - assert.equal(interpreters[1].architecture, Architecture.x86, 'Incorrect arhictecture'); - assert.equal(interpreters[1].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); - assert.equal(interpreters[1].displayName, undefined, 'Incorrect display name'); - assert.equal(interpreters[1].path, path.join(environmentsPath, 'path2', 'python.exe'), 'Incorrect path'); - assert.equal(interpreters[1].version, 'Tag2', 'Incorrect version'); - - assert.equal(interpreters[2].architecture, Architecture.x86, 'Incorrect arhictecture'); - assert.equal(interpreters[2].companyDisplayName, 'Company Two', 'Incorrect company name'); - assert.equal(interpreters[2].displayName, undefined, 'Incorrect display name'); - assert.equal(interpreters[2].path, path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), 'Incorrect path'); - assert.equal(interpreters[2].version, 'Tag B', 'Incorrect version'); - }); - test('Must return multiple entries excluding the invalid registry items and duplicate paths', async () => { - const registryKeys = [ - { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One', '\\Software\\Python\\Company Two', '\\Software\\Python\\Company Three', '\\Software\\Python\\Company Four', '\\Software\\Python\\Company Five', 'Missing Tag'] }, - { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1', '\\Software\\Python\\Company One\\Tag2'] }, - { key: '\\Software\\Python\\Company Two', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Two\\Tag A', '\\Software\\Python\\Company Two\\Tag B', '\\Software\\Python\\Company Two\\Tag C'] }, - { key: '\\Software\\Python\\Company Three', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Three\\Tag !'] }, - { key: '\\Software\\Python\\Company Four', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Four\\Four !'] }, - { key: '\\Software\\Python\\Company Five', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Five\\Five !'] }, - { key: '\\Software\\Python', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['A'] }, - { key: '\\Software\\Python\\Company A', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['Another Tag'] } - ]; - const registryValues: { key: string, hive: RegistryHive, arch?: Architecture, value: string, name?: string }[] = [ - { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), name: 'ExecutablePath' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag1', name: 'SysVersion' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' }, - - { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy') }, - { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe'), name: 'ExecutablePath' }, - - { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') }, - { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag A', name: 'SysVersion' }, - - { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2') }, - { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag B', name: 'DisplayName' }, - { key: '\\Software\\Python\\Company Two\\Tag C\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, - - // tslint:disable-next-line:no-any - { key: '\\Software\\Python\\Company Five\\Five !\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: undefined }, - - { key: '\\Software\\Python\\Company Three\\Tag !\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, - - { key: '\\Software\\Python\\Company A\\Another Tag\\InstallPath', hive: RegistryHive.HKLM, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') } - ]; - const registry = new MockRegistry(registryKeys, registryValues); - const winRegistry = new WindowsRegistryService(registry, false, serviceContainer.object); - - const interpreters = await winRegistry.getInterpreters(); - - assert.equal(interpreters.length, 4, 'Incorrect number of entries'); - assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); - assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); - assert.equal(interpreters[0].displayName, undefined, 'Incorrect display name'); - assert.equal(interpreters[0].path, path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), 'Incorrect path'); - assert.equal(interpreters[0].version, 'Tag1', 'Incorrect version'); - - assert.equal(interpreters[1].architecture, Architecture.x86, 'Incorrect arhictecture'); - assert.equal(interpreters[1].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); - assert.equal(interpreters[1].displayName, undefined, 'Incorrect display name'); - assert.equal(interpreters[1].path, path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe'), 'Incorrect path'); - assert.equal(interpreters[1].version, 'Tag2', 'Incorrect version'); - - assert.equal(interpreters[2].architecture, Architecture.x86, 'Incorrect arhictecture'); - assert.equal(interpreters[2].companyDisplayName, 'Company Two', 'Incorrect company name'); - assert.equal(interpreters[2].displayName, undefined, 'Incorrect display name'); - assert.equal(interpreters[2].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); - assert.equal(interpreters[2].version, 'Tag A', 'Incorrect version'); - }); - test('Must return multiple entries excluding the invalid registry items and nonexistent paths', async () => { - const registryKeys = [ - { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One', '\\Software\\Python\\Company Two', '\\Software\\Python\\Company Three', '\\Software\\Python\\Company Four', '\\Software\\Python\\Company Five', 'Missing Tag'] }, - { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1', '\\Software\\Python\\Company One\\Tag2'] }, - { key: '\\Software\\Python\\Company Two', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Two\\Tag A', '\\Software\\Python\\Company Two\\Tag B', '\\Software\\Python\\Company Two\\Tag C'] }, - { key: '\\Software\\Python\\Company Three', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Three\\Tag !'] }, - { key: '\\Software\\Python\\Company Four', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Four\\Four !'] }, - { key: '\\Software\\Python\\Company Five', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Five\\Five !'] }, - { key: '\\Software\\Python', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['A'] }, - { key: '\\Software\\Python\\Company A', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['Another Tag'] } - ]; - const registryValues: { key: string, hive: RegistryHive, arch?: Architecture, value: string, name?: string }[] = [ - { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), name: 'ExecutablePath' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag1', name: 'SysVersion' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' }, - - { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'scipy') }, - { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'scipy', 'python.exe'), name: 'ExecutablePath' }, - - { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path') }, - { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag A', name: 'SysVersion' }, - - { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2') }, - { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag B', name: 'DisplayName' }, - { key: '\\Software\\Python\\Company Two\\Tag C\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy') }, - - // tslint:disable-next-line:no-any - { key: '\\Software\\Python\\Company Five\\Five !\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: undefined }, - - { key: '\\Software\\Python\\Company Three\\Tag !\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy') }, - - { key: '\\Software\\Python\\Company A\\Another Tag\\InstallPath', hive: RegistryHive.HKLM, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy') } - ]; - const registry = new MockRegistry(registryKeys, registryValues); - const winRegistry = new WindowsRegistryService(registry, false, serviceContainer.object); - - const interpreters = await winRegistry.getInterpreters(); - - assert.equal(interpreters.length, 2, 'Incorrect number of entries'); - - assert.equal(interpreters[0].architecture, Architecture.x86, '1. Incorrect arhictecture'); - assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', '1. Incorrect company name'); - assert.equal(interpreters[0].displayName, undefined, '1. Incorrect display name'); - assert.equal(interpreters[0].path, path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), '1. Incorrect path'); - assert.equal(interpreters[0].version, 'Tag1', '1. Incorrect version'); - - assert.equal(interpreters[1].architecture, Architecture.x86, '2. Incorrect arhictecture'); - assert.equal(interpreters[1].companyDisplayName, 'Company Two', '2. Incorrect company name'); - assert.equal(interpreters[1].displayName, undefined, '2. Incorrect display name'); - assert.equal(interpreters[1].path, path.join(environmentsPath, 'path2', 'python.exe'), '2. Incorrect path'); - assert.equal(interpreters[1].version, 'Tag B', '2. Incorrect version'); - }); - } -}); diff --git a/src/test/interpreters/windowsRegistryService.unit.test.ts b/src/test/interpreters/windowsRegistryService.unit.test.ts new file mode 100644 index 000000000000..15a11aec7401 --- /dev/null +++ b/src/test/interpreters/windowsRegistryService.unit.test.ts @@ -0,0 +1,290 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Architecture, RegistryHive } from '../../client/common/platform/types'; +import { IPathUtils, IPersistentStateFactory } from '../../client/common/types'; +import { IInterpreterHelper } from '../../client/interpreter/contracts'; +import { WindowsRegistryService } from '../../client/interpreter/locators/services/windowsRegistryService'; +import { IServiceContainer } from '../../client/ioc/types'; +import { MockRegistry, MockState } from './mocks'; + +const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); + +// tslint:disable-next-line:max-func-body-length +suite('Interpreters from Windows Registry (unit)', () => { + let serviceContainer: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + const stateFactory = TypeMoq.Mock.ofType(); + const interpreterHelper = TypeMoq.Mock.ofType(); + const pathUtils = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory))).returns(() => stateFactory.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterHelper))).returns(() => interpreterHelper.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); + pathUtils.setup(p => p.basename(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((p: string) => p.split(/[\\,\/]/).reverse()[0]); + const state = new MockState(undefined); + // tslint:disable-next-line:no-empty no-any + interpreterHelper.setup(h => h.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({} as any)); + stateFactory.setup(s => s.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => state); + }); + test('Must return an empty list (x86)', async () => { + const registry = new MockRegistry([], []); + const winRegistry = new WindowsRegistryService(registry, false, serviceContainer.object); + + const interpreters = await winRegistry.getInterpreters(); + assert.equal(interpreters.length, 0, 'Incorrect number of entries'); + }); + test('Must return an empty list (x64)', async () => { + const registry = new MockRegistry([], []); + const winRegistry = new WindowsRegistryService(registry, true, serviceContainer.object); + + const interpreters = await winRegistry.getInterpreters(); + assert.equal(interpreters.length, 0, 'Incorrect number of entries'); + }); + test('Must return a single entry', async () => { + const registryKeys = [ + { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One'] }, + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1'] } + ]; + const registryValues = [ + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1', 'one.exe'), name: 'ExecutablePath' }, + { key: '\\Software\\Python\\Company One\\Tag1', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag1', name: 'SysVersion' }, + { key: '\\Software\\Python\\Company One\\Tag1', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' } + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService(registry, false, serviceContainer.object); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); + assert.equal(interpreters[0].displayName, 'DisplayName.Tag1', 'Incorrect display name'); + assert.equal(interpreters[0].path, path.join(environmentsPath, 'path1', 'one.exe'), 'Incorrect executable path'); + assert.equal(interpreters[0].version, 'Version.Tag1', 'Incorrect version'); + }); + test('Must default names for PythonCore and exe', async () => { + const registryKeys = [ + { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PythonCore'] }, + { key: '\\Software\\Python\\PythonCore', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PythonCore\\Tag1'] } + ]; + const registryValues = [ + { key: '\\Software\\Python\\PythonCore\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') } + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService(registry, false, serviceContainer.object); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Python Software Foundation', 'Incorrect company name'); + assert.equal(interpreters[0].displayName, undefined, 'Incorrect display name'); + assert.equal(interpreters[0].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[0].version, 'Tag1', 'Incorrect version'); + }); + test('Must ignore company \'PyLauncher\'', async () => { + const registryKeys = [ + { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PyLauncher'] }, + { key: '\\Software\\Python\\PythonCore', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PyLauncher\\Tag1'] } + ]; + const registryValues = [ + { key: '\\Software\\Python\\PyLauncher\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'c:/temp/Install Path Tag1' } + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService(registry, false, serviceContainer.object); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 0, 'Incorrect number of entries'); + }); + test('Must return a single entry and when registry contains only the InstallPath', async () => { + const registryKeys = [ + { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One'] }, + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1'] } + ]; + const registryValues = [ + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') } + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService(registry, false, serviceContainer.object); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Company One', 'Incorrect company name'); + assert.equal(interpreters[0].displayName, undefined, 'Incorrect display name'); + assert.equal(interpreters[0].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[0].version, 'Tag1', 'Incorrect version'); + }); + test('Must return multiple entries', async () => { + const registryKeys = [ + { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One', '\\Software\\Python\\Company Two', '\\Software\\Python\\Company Three'] }, + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1', '\\Software\\Python\\Company One\\Tag2'] }, + { key: '\\Software\\Python\\Company Two', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Two\\Tag A', '\\Software\\Python\\Company Two\\Tag B', '\\Software\\Python\\Company Two\\Tag C'] }, + { key: '\\Software\\Python\\Company Three', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Three\\Tag !'] }, + { key: '\\Software\\Python', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['A'] }, + { key: '\\Software\\Python\\Company A', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['Another Tag'] } + ]; + const registryValues = [ + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1', 'python.exe'), name: 'ExecutablePath' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2'), name: 'SysVersion' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' }, + + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2') }, + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2', 'python.exe'), name: 'ExecutablePath' }, + + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path3') }, + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag A', name: 'SysVersion' }, + + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag B', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company Two\\Tag C\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy') }, + + { key: '\\Software\\Python\\Company Three\\Tag !\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + + { key: '\\Software\\Python\\Company A\\Another Tag\\InstallPath', hive: RegistryHive.HKLM, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe') } + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService(registry, false, serviceContainer.object); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 4, 'Incorrect number of entries'); + assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); + assert.equal(interpreters[0].displayName, undefined, 'Incorrect display name'); + assert.equal(interpreters[0].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[0].version, 'Tag1', 'Incorrect version'); + + assert.equal(interpreters[1].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[1].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); + assert.equal(interpreters[1].displayName, undefined, 'Incorrect display name'); + assert.equal(interpreters[1].path, path.join(environmentsPath, 'path2', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[1].version, 'Tag2', 'Incorrect version'); + + assert.equal(interpreters[2].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[2].companyDisplayName, 'Company Two', 'Incorrect company name'); + assert.equal(interpreters[2].displayName, undefined, 'Incorrect display name'); + assert.equal(interpreters[2].path, path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[2].version, 'Tag B', 'Incorrect version'); + }); + test('Must return multiple entries excluding the invalid registry items and duplicate paths', async () => { + const registryKeys = [ + { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One', '\\Software\\Python\\Company Two', '\\Software\\Python\\Company Three', '\\Software\\Python\\Company Four', '\\Software\\Python\\Company Five', 'Missing Tag'] }, + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1', '\\Software\\Python\\Company One\\Tag2'] }, + { key: '\\Software\\Python\\Company Two', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Two\\Tag A', '\\Software\\Python\\Company Two\\Tag B', '\\Software\\Python\\Company Two\\Tag C'] }, + { key: '\\Software\\Python\\Company Three', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Three\\Tag !'] }, + { key: '\\Software\\Python\\Company Four', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Four\\Four !'] }, + { key: '\\Software\\Python\\Company Five', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Five\\Five !'] }, + { key: '\\Software\\Python', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['A'] }, + { key: '\\Software\\Python\\Company A', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['Another Tag'] } + ]; + const registryValues: { key: string; hive: RegistryHive; arch?: Architecture; value: string; name?: string }[] = [ + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), name: 'ExecutablePath' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag1', name: 'SysVersion' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' }, + + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy') }, + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe'), name: 'ExecutablePath' }, + + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') }, + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag A', name: 'SysVersion' }, + + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2') }, + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag B', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company Two\\Tag C\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + + // tslint:disable-next-line:no-any + { key: '\\Software\\Python\\Company Five\\Five !\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: undefined }, + + { key: '\\Software\\Python\\Company Three\\Tag !\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + + { key: '\\Software\\Python\\Company A\\Another Tag\\InstallPath', hive: RegistryHive.HKLM, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') } + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService(registry, false, serviceContainer.object); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 4, 'Incorrect number of entries'); + assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); + assert.equal(interpreters[0].displayName, undefined, 'Incorrect display name'); + assert.equal(interpreters[0].path, path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[0].version, 'Tag1', 'Incorrect version'); + + assert.equal(interpreters[1].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[1].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); + assert.equal(interpreters[1].displayName, undefined, 'Incorrect display name'); + assert.equal(interpreters[1].path, path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[1].version, 'Tag2', 'Incorrect version'); + + assert.equal(interpreters[2].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[2].companyDisplayName, 'Company Two', 'Incorrect company name'); + assert.equal(interpreters[2].displayName, undefined, 'Incorrect display name'); + assert.equal(interpreters[2].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[2].version, 'Tag A', 'Incorrect version'); + }); + test('Must return multiple entries excluding the invalid registry items and nonexistent paths', async () => { + const registryKeys = [ + { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One', '\\Software\\Python\\Company Two', '\\Software\\Python\\Company Three', '\\Software\\Python\\Company Four', '\\Software\\Python\\Company Five', 'Missing Tag'] }, + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1', '\\Software\\Python\\Company One\\Tag2'] }, + { key: '\\Software\\Python\\Company Two', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Two\\Tag A', '\\Software\\Python\\Company Two\\Tag B', '\\Software\\Python\\Company Two\\Tag C'] }, + { key: '\\Software\\Python\\Company Three', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Three\\Tag !'] }, + { key: '\\Software\\Python\\Company Four', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Four\\Four !'] }, + { key: '\\Software\\Python\\Company Five', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Five\\Five !'] }, + { key: '\\Software\\Python', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['A'] }, + { key: '\\Software\\Python\\Company A', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['Another Tag'] } + ]; + const registryValues: { key: string; hive: RegistryHive; arch?: Architecture; value: string; name?: string }[] = [ + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), name: 'ExecutablePath' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag1', name: 'SysVersion' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' }, + + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'scipy') }, + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'scipy', 'python.exe'), name: 'ExecutablePath' }, + + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path') }, + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag A', name: 'SysVersion' }, + + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2') }, + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag B', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company Two\\Tag C\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy') }, + + // tslint:disable-next-line:no-any + { key: '\\Software\\Python\\Company Five\\Five !\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: undefined }, + + { key: '\\Software\\Python\\Company Three\\Tag !\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy') }, + + { key: '\\Software\\Python\\Company A\\Another Tag\\InstallPath', hive: RegistryHive.HKLM, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy') } + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService(registry, false, serviceContainer.object); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 2, 'Incorrect number of entries'); + + assert.equal(interpreters[0].architecture, Architecture.x86, '1. Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', '1. Incorrect company name'); + assert.equal(interpreters[0].displayName, undefined, '1. Incorrect display name'); + assert.equal(interpreters[0].path, path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), '1. Incorrect path'); + assert.equal(interpreters[0].version, 'Tag1', '1. Incorrect version'); + + assert.equal(interpreters[1].architecture, Architecture.x86, '2. Incorrect arhictecture'); + assert.equal(interpreters[1].companyDisplayName, 'Company Two', '2. Incorrect company name'); + assert.equal(interpreters[1].displayName, undefined, '2. Incorrect display name'); + assert.equal(interpreters[1].path, path.join(environmentsPath, 'path2', 'python.exe'), '2. Incorrect path'); + assert.equal(interpreters[1].version, 'Tag B', '2. Incorrect version'); + }); +}); diff --git a/src/test/language/tokenizer.test.ts b/src/test/language/tokenizer.test.ts index e2a5b3f6defb..f90da3eeffd0 100644 --- a/src/test/language/tokenizer.test.ts +++ b/src/test/language/tokenizer.test.ts @@ -9,14 +9,14 @@ import { TokenType } from '../../client/language/types'; // tslint:disable-next-line:max-func-body-length suite('Language.Tokenizer', () => { - test('Empty', async () => { + test('Empty', () => { const t = new Tokenizer(); const tokens = t.tokenize(''); assert.equal(tokens instanceof TextRangeCollection, true); assert.equal(tokens.count, 0); assert.equal(tokens.length, 0); }); - test('Strings: unclosed', async () => { + test('Strings: unclosed', () => { const t = new Tokenizer(); const tokens = t.tokenize(' "string" """line1\n#line2"""\t\'un#closed'); assert.equal(tokens.count, 3); @@ -28,7 +28,7 @@ suite('Language.Tokenizer', () => { assert.equal(tokens.getItemAt(i).type, TokenType.String); } }); - test('Strings: block next to regular, double-quoted', async () => { + test('Strings: block next to regular, double-quoted', () => { const t = new Tokenizer(); const tokens = t.tokenize('"string""""s2"""'); assert.equal(tokens.count, 2); @@ -40,7 +40,7 @@ suite('Language.Tokenizer', () => { assert.equal(tokens.getItemAt(i).type, TokenType.String); } }); - test('Strings: block next to block, double-quoted', async () => { + test('Strings: block next to block, double-quoted', () => { const t = new Tokenizer(); const tokens = t.tokenize('""""""""'); assert.equal(tokens.count, 2); @@ -52,7 +52,7 @@ suite('Language.Tokenizer', () => { assert.equal(tokens.getItemAt(i).type, TokenType.String); } }); - test('Strings: unclosed sequence of quotes', async () => { + test('Strings: unclosed sequence of quotes', () => { const t = new Tokenizer(); const tokens = t.tokenize('"""""'); assert.equal(tokens.count, 1); @@ -64,7 +64,7 @@ suite('Language.Tokenizer', () => { assert.equal(tokens.getItemAt(i).type, TokenType.String); } }); - test('Strings: single quote escape', async () => { + test('Strings: single quote escape', () => { const t = new Tokenizer(); // tslint:disable-next-line:quotemark const tokens = t.tokenize("'\\'quoted\\''"); @@ -72,14 +72,89 @@ suite('Language.Tokenizer', () => { assert.equal(tokens.getItemAt(0).type, TokenType.String); assert.equal(tokens.getItemAt(0).length, 12); }); - test('Strings: double quote escape', async () => { + test('Strings: double quote escape', () => { const t = new Tokenizer(); const tokens = t.tokenize('"\\"quoted\\""'); assert.equal(tokens.count, 1); assert.equal(tokens.getItemAt(0).type, TokenType.String); assert.equal(tokens.getItemAt(0).length, 12); }); - test('Comments', async () => { + test('Strings: single quoted f-string ', () => { + const t = new Tokenizer(); + // tslint:disable-next-line:quotemark + const tokens = t.tokenize("a+f'quoted'"); + assert.equal(tokens.count, 3); + assert.equal(tokens.getItemAt(0).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(1).type, TokenType.Operator); + assert.equal(tokens.getItemAt(2).type, TokenType.String); + assert.equal(tokens.getItemAt(2).length, 9); + }); + test('Strings: double quoted f-string ', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('x(1,f"quoted")'); + assert.equal(tokens.count, 6); + assert.equal(tokens.getItemAt(0).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(1).type, TokenType.OpenBrace); + assert.equal(tokens.getItemAt(2).type, TokenType.Number); + assert.equal(tokens.getItemAt(3).type, TokenType.Comma); + assert.equal(tokens.getItemAt(4).type, TokenType.String); + assert.equal(tokens.getItemAt(4).length, 9); + assert.equal(tokens.getItemAt(5).type, TokenType.CloseBrace); + }); + test('Strings: single quoted multiline f-string ', () => { + const t = new Tokenizer(); + // tslint:disable-next-line:quotemark + const tokens = t.tokenize("f'''quoted'''"); + assert.equal(tokens.count, 1); + assert.equal(tokens.getItemAt(0).type, TokenType.String); + assert.equal(tokens.getItemAt(0).length, 13); + }); + test('Strings: double quoted multiline f-string ', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('f"""quoted """'); + assert.equal(tokens.count, 1); + assert.equal(tokens.getItemAt(0).type, TokenType.String); + assert.equal(tokens.getItemAt(0).length, 14); + }); + test('Strings: escape at the end of single quoted string ', () => { + const t = new Tokenizer(); + // tslint:disable-next-line:quotemark + const tokens = t.tokenize("'quoted\\'\nx"); + assert.equal(tokens.count, 2); + assert.equal(tokens.getItemAt(0).type, TokenType.String); + assert.equal(tokens.getItemAt(0).length, 9); + assert.equal(tokens.getItemAt(1).type, TokenType.Identifier); + }); + test('Strings: escape at the end of double quoted string ', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('"quoted\\"\nx'); + assert.equal(tokens.count, 2); + assert.equal(tokens.getItemAt(0).type, TokenType.String); + assert.equal(tokens.getItemAt(0).length, 9); + assert.equal(tokens.getItemAt(1).type, TokenType.Identifier); + }); + test('Strings: b/u/r-string', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('b"b" u"u" br"br" ur"ur"'); + assert.equal(tokens.count, 4); + assert.equal(tokens.getItemAt(0).type, TokenType.String); + assert.equal(tokens.getItemAt(0).length, 4); + assert.equal(tokens.getItemAt(1).type, TokenType.String); + assert.equal(tokens.getItemAt(1).length, 4); + assert.equal(tokens.getItemAt(2).type, TokenType.String); + assert.equal(tokens.getItemAt(2).length, 6); + assert.equal(tokens.getItemAt(3).type, TokenType.String); + assert.equal(tokens.getItemAt(3).length, 6); + }); + test('Strings: escape at the end of double quoted string ', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('"quoted\\"\nx'); + assert.equal(tokens.count, 2); + assert.equal(tokens.getItemAt(0).type, TokenType.String); + assert.equal(tokens.getItemAt(0).length, 9); + assert.equal(tokens.getItemAt(1).type, TokenType.Identifier); + }); + test('Comments', () => { const t = new Tokenizer(); const tokens = t.tokenize(' #co"""mment1\n\t\n#comm\'ent2 '); assert.equal(tokens.count, 2); @@ -91,7 +166,7 @@ suite('Language.Tokenizer', () => { assert.equal(tokens.getItemAt(i).type, TokenType.Comment); } }); - test('Period to operator token', async () => { + test('Period to operator token', () => { const t = new Tokenizer(); const tokens = t.tokenize('x.y'); assert.equal(tokens.count, 3); @@ -100,7 +175,7 @@ suite('Language.Tokenizer', () => { assert.equal(tokens.getItemAt(1).type, TokenType.Operator); assert.equal(tokens.getItemAt(2).type, TokenType.Identifier); }); - test('@ to operator token', async () => { + test('@ to operator token', () => { const t = new Tokenizer(); const tokens = t.tokenize('@x'); assert.equal(tokens.count, 2); @@ -108,17 +183,17 @@ suite('Language.Tokenizer', () => { assert.equal(tokens.getItemAt(0).type, TokenType.Operator); assert.equal(tokens.getItemAt(1).type, TokenType.Identifier); }); - test('Unknown token', async () => { + test('Unknown token', () => { const t = new Tokenizer(); - const tokens = t.tokenize('~$'); + const tokens = t.tokenize('`$'); assert.equal(tokens.count, 1); assert.equal(tokens.getItemAt(0).type, TokenType.Unknown); }); - test('Hex number', async () => { + test('Hex number', () => { const t = new Tokenizer(); const tokens = t.tokenize('1 0X2 0x3 0x'); - assert.equal(tokens.count, 4); + assert.equal(tokens.count, 5); assert.equal(tokens.getItemAt(0).type, TokenType.Number); assert.equal(tokens.getItemAt(0).length, 1); @@ -129,13 +204,16 @@ suite('Language.Tokenizer', () => { assert.equal(tokens.getItemAt(2).type, TokenType.Number); assert.equal(tokens.getItemAt(2).length, 3); - assert.equal(tokens.getItemAt(3).type, TokenType.Unknown); - assert.equal(tokens.getItemAt(3).length, 2); + assert.equal(tokens.getItemAt(3).type, TokenType.Number); + assert.equal(tokens.getItemAt(3).length, 1); + + assert.equal(tokens.getItemAt(4).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(4).length, 1); }); - test('Binary number', async () => { + test('Binary number', () => { const t = new Tokenizer(); const tokens = t.tokenize('1 0B1 0b010 0b3 0b'); - assert.equal(tokens.count, 6); + assert.equal(tokens.count, 7); assert.equal(tokens.getItemAt(0).type, TokenType.Number); assert.equal(tokens.getItemAt(0).length, 1); @@ -152,13 +230,16 @@ suite('Language.Tokenizer', () => { assert.equal(tokens.getItemAt(4).type, TokenType.Identifier); assert.equal(tokens.getItemAt(4).length, 2); - assert.equal(tokens.getItemAt(5).type, TokenType.Unknown); - assert.equal(tokens.getItemAt(5).length, 2); + assert.equal(tokens.getItemAt(5).type, TokenType.Number); + assert.equal(tokens.getItemAt(5).length, 1); + + assert.equal(tokens.getItemAt(6).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(6).length, 1); }); - test('Octal number', async () => { + test('Octal number', () => { const t = new Tokenizer(); - const tokens = t.tokenize('1 0o4 0o077 0o9 0oO'); - assert.equal(tokens.count, 6); + const tokens = t.tokenize('1 0o4 0o077 -0o200 0o9 0oO'); + assert.equal(tokens.count, 8); assert.equal(tokens.getItemAt(0).type, TokenType.Number); assert.equal(tokens.getItemAt(0).length, 1); @@ -170,29 +251,128 @@ suite('Language.Tokenizer', () => { assert.equal(tokens.getItemAt(2).length, 5); assert.equal(tokens.getItemAt(3).type, TokenType.Number); - assert.equal(tokens.getItemAt(3).length, 1); + assert.equal(tokens.getItemAt(3).length, 6); - assert.equal(tokens.getItemAt(4).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(4).type, TokenType.Number); + assert.equal(tokens.getItemAt(4).length, 1); + + assert.equal(tokens.getItemAt(5).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(5).length, 2); + + assert.equal(tokens.getItemAt(6).type, TokenType.Number); + assert.equal(tokens.getItemAt(6).length, 1); + + assert.equal(tokens.getItemAt(7).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(7).length, 2); + }); + test('Decimal number', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('-2147483647 ++2147483647'); + assert.equal(tokens.count, 3); + + assert.equal(tokens.getItemAt(0).type, TokenType.Number); + assert.equal(tokens.getItemAt(0).length, 11); + + assert.equal(tokens.getItemAt(1).type, TokenType.Operator); + assert.equal(tokens.getItemAt(1).length, 1); + + assert.equal(tokens.getItemAt(2).type, TokenType.Number); + assert.equal(tokens.getItemAt(2).length, 11); + }); + test('Decimal number operator', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('a[: -1]'); + assert.equal(tokens.count, 5); + + assert.equal(tokens.getItemAt(3).type, TokenType.Number); + assert.equal(tokens.getItemAt(3).length, 2); + }); + test('Floating point number', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('3.0 .2 ++.3e+12 --.4e1'); + assert.equal(tokens.count, 6); + + assert.equal(tokens.getItemAt(0).type, TokenType.Number); + assert.equal(tokens.getItemAt(0).length, 3); + + assert.equal(tokens.getItemAt(1).type, TokenType.Number); + assert.equal(tokens.getItemAt(1).length, 2); + + assert.equal(tokens.getItemAt(2).type, TokenType.Operator); + assert.equal(tokens.getItemAt(2).length, 1); + + assert.equal(tokens.getItemAt(3).type, TokenType.Number); + assert.equal(tokens.getItemAt(3).length, 7); + + assert.equal(tokens.getItemAt(4).type, TokenType.Operator); + assert.equal(tokens.getItemAt(4).length, 1); + + assert.equal(tokens.getItemAt(5).type, TokenType.Number); + assert.equal(tokens.getItemAt(5).length, 5); + }); + test('Floating point numbers with braces', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('(3.0) (.2) (+.3e+12, .4e1; 0)'); + assert.equal(tokens.count, 13); + + assert.equal(tokens.getItemAt(1).type, TokenType.Number); + assert.equal(tokens.getItemAt(1).length, 3); + + assert.equal(tokens.getItemAt(4).type, TokenType.Number); assert.equal(tokens.getItemAt(4).length, 2); - assert.equal(tokens.getItemAt(5).type, TokenType.Unknown); - assert.equal(tokens.getItemAt(5).length, 3); + assert.equal(tokens.getItemAt(7).type, TokenType.Number); + assert.equal(tokens.getItemAt(7).length, 7); + + assert.equal(tokens.getItemAt(9).type, TokenType.Number); + assert.equal(tokens.getItemAt(9).length, 4); + + assert.equal(tokens.getItemAt(11).type, TokenType.Number); + assert.equal(tokens.getItemAt(11).length, 1); + }); + test('Underscore numbers', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('+1_0_0_0 0_0 .5_00_3e-4 0xCAFE_F00D 10_000_000.0 0b_0011_1111_0100_1110'); + const lengths = [8, 3, 10, 11, 12, 22]; + assert.equal(tokens.count, 6); + + for (let i = 0; i < tokens.count; i += 1) { + assert.equal(tokens.getItemAt(i).type, TokenType.Number); + assert.equal(tokens.getItemAt(i).length, lengths[i]); + } + }); + test('Simple expression, leading minus', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('x == -y'); + assert.equal(tokens.count, 4); + + assert.equal(tokens.getItemAt(0).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(0).length, 1); + + assert.equal(tokens.getItemAt(1).type, TokenType.Operator); + assert.equal(tokens.getItemAt(1).length, 2); + + assert.equal(tokens.getItemAt(2).type, TokenType.Operator); + assert.equal(tokens.getItemAt(2).length, 1); + + assert.equal(tokens.getItemAt(3).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(3).length, 1); }); - test('Operators', async () => { + test('Operators', () => { const text = '< <> << <<= ' + - '== != > >> >>= ' + - '+ -' + + '== != > >> >>= >= <=' + + '+ - ~ %' + '* ** / /= //=' + - '*= += -= **= ' + - '& &= | |= ^ ^='; + '*= += -= ~= %= **= ' + + '& &= | |= ^ ^= ->'; const tokens = new Tokenizer().tokenize(text); const lengths = [ 1, 2, 2, 3, - 2, 2, 1, 2, 3, - 1, 1, + 2, 2, 1, 2, 3, 2, 2, + 1, 1, 1, 1, 1, 2, 1, 2, 3, - 2, 2, 2, 3, - 1, 2, 1, 2, 1, 2]; + 2, 2, 2, 2, 2, 3, + 1, 2, 1, 2, 1, 2, 2]; assert.equal(tokens.count, lengths.length); for (let i = 0; i < tokens.count; i += 1) { const t = tokens.getItemAt(i); diff --git a/src/test/linters/lint.args.test.ts b/src/test/linters/lint.args.test.ts new file mode 100644 index 000000000000..58421ab1f9bc --- /dev/null +++ b/src/test/linters/lint.args.test.ts @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length + +import { expect } from 'chai'; +import { Container } from 'inversify'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { CancellationTokenSource, OutputChannel, TextDocument, Uri, WorkspaceFolder } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import '../../client/common/extensions'; +import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; +import { IConfigurationService, IInstaller, ILintingSettings, ILogger, IOutputChannel, IPythonSettings } from '../../client/common/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { ServiceContainer } from '../../client/ioc/container'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { BaseLinter } from '../../client/linters/baseLinter'; +import { Flake8 } from '../../client/linters/flake8'; +import { LinterManager } from '../../client/linters/linterManager'; +import { MyPy } from '../../client/linters/mypy'; +import { Pep8 } from '../../client/linters/pep8'; +import { Prospector } from '../../client/linters/prospector'; +import { PyDocStyle } from '../../client/linters/pydocstyle'; +import { PyLama } from '../../client/linters/pylama'; +import { Pylint } from '../../client/linters/pylint'; +import { ILinterManager, ILintingEngine } from '../../client/linters/types'; +import { initialize } from '../initialize'; + +suite('Linting - Arguments', () => { + [undefined, path.join('users', 'dev_user')].forEach(workspaceUri => { + [Uri.file(path.join('users', 'dev_user', 'development path to', 'one.py')), Uri.file(path.join('users', 'dev_user', 'development', 'one.py'))].forEach(fileUri => { + suite(`File path ${fileUri.fsPath.indexOf(' ') > 0 ? 'with' : 'without'} spaces and ${workspaceUri ? 'without' : 'with'} a workspace`, () => { + let interpreterService: TypeMoq.IMock; + let engine: TypeMoq.IMock; + let configService: TypeMoq.IMock; + let docManager: TypeMoq.IMock; + let settings: TypeMoq.IMock; + let lm: ILinterManager; + let serviceContainer: ServiceContainer; + let document: TypeMoq.IMock; + let outputChannel: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + const cancellationToken = new CancellationTokenSource().token; + suiteSetup(initialize); + setup(async () => { + const cont = new Container(); + const serviceManager = new ServiceManager(cont); + + serviceContainer = new ServiceContainer(cont); + outputChannel = TypeMoq.Mock.ofType(); + + const fs = TypeMoq.Mock.ofType(); + fs.setup(x => x.fileExists(TypeMoq.It.isAny())).returns(() => new Promise((resolve, reject) => resolve(true))); + fs.setup(x => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => true); + serviceManager.addSingletonInstance(IFileSystem, fs.object); + + serviceManager.addSingletonInstance(IOutputChannel, outputChannel.object); + + interpreterService = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(IInterpreterService, interpreterService.object); + + engine = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(ILintingEngine, engine.object); + + docManager = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(IDocumentManager, docManager.object); + + const lintSettings = TypeMoq.Mock.ofType(); + lintSettings.setup(x => x.enabled).returns(() => true); + lintSettings.setup(x => x.lintOnSave).returns(() => true); + + settings = TypeMoq.Mock.ofType(); + settings.setup(x => x.linting).returns(() => lintSettings.object); + + configService = TypeMoq.Mock.ofType(); + configService.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + serviceManager.addSingletonInstance(IConfigurationService, configService.object); + + const workspaceFolder: WorkspaceFolder | undefined = workspaceUri ? { uri: Uri.file(workspaceUri), index: 0, name: '' } : undefined; + workspaceService = TypeMoq.Mock.ofType(); + workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder); + serviceManager.addSingletonInstance(IWorkspaceService, workspaceService.object); + + const logger = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(ILogger, logger.object); + + const installer = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(IInstaller, installer.object); + + const platformService = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(IPlatformService, platformService.object); + + lm = new LinterManager(serviceContainer); + serviceManager.addSingletonInstance(ILinterManager, lm); + document = TypeMoq.Mock.ofType(); + }); + + async function testLinter(linter: BaseLinter, expectedArgs: string[]) { + document.setup(d => d.uri).returns(() => fileUri); + + let invoked = false; + (linter as any).run = (args, doc, token) => { + expect(args).to.deep.equal(expectedArgs); + invoked = true; + return Promise.resolve([]); + }; + await linter.lint(document.object, cancellationToken); + expect(invoked).to.be.equal(true, 'method not invoked'); + } + test('Flake8', async () => { + const linter = new Flake8(outputChannel.object, serviceContainer); + const expectedArgs = ['--format=%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s', fileUri.fsPath]; + await testLinter(linter, expectedArgs); + }); + test('Pep8', async () => { + const linter = new Pep8(outputChannel.object, serviceContainer); + const expectedArgs = ['--format=%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s', fileUri.fsPath]; + await testLinter(linter, expectedArgs); + }); + test('Prospector', async () => { + const linter = new Prospector(outputChannel.object, serviceContainer); + const expectedPath = workspaceUri ? fileUri.fsPath.substring(workspaceUri.length + 2) : path.basename(fileUri.fsPath); + const expectedArgs = ['--absolute-paths', '--output-format=json', expectedPath]; + await testLinter(linter, expectedArgs); + }); + test('Pylama', async () => { + const linter = new PyLama(outputChannel.object, serviceContainer); + const expectedArgs = ['--format=parsable', fileUri.fsPath]; + await testLinter(linter, expectedArgs); + }); + test('MyPy', async () => { + const linter = new MyPy(outputChannel.object, serviceContainer); + const expectedArgs = [fileUri.fsPath]; + await testLinter(linter, expectedArgs); + }); + test('Pydocstyle', async () => { + const linter = new PyDocStyle(outputChannel.object, serviceContainer); + const expectedArgs = [fileUri.fsPath]; + await testLinter(linter, expectedArgs); + }); + test('Pylint', async () => { + const linter = new Pylint(outputChannel.object, serviceContainer); + document.setup(d => d.uri).returns(() => fileUri); + + let invoked = false; + (linter as any).run = (args, doc, token) => { + expect(args[args.length - 1]).to.equal(fileUri.fsPath); + invoked = true; + return Promise.resolve([]); + }; + await linter.lint(document.object, cancellationToken); + expect(invoked).to.be.equal(true, 'method not invoked'); + }); + }); + }); + }); +}); diff --git a/src/test/linters/lint.commands.test.ts b/src/test/linters/lint.commands.test.ts index d843c7128f4b..5cac8d6f997f 100644 --- a/src/test/linters/lint.commands.test.ts +++ b/src/test/linters/lint.commands.test.ts @@ -36,6 +36,7 @@ suite('Linting - Linter Selector', () => { const cont = new Container(); const serviceManager = new ServiceManager(cont); serviceContainer = new ServiceContainer(cont); + serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); appShell = TypeMoq.Mock.ofType(); serviceManager.addSingleton(IConfigurationService, ConfigurationService); @@ -74,7 +75,7 @@ suite('Linting - Linter Selector', () => { }); test('Run linter command', async () => { - commands.runLinting(); + await commands.runLinting(); engine.verify(p => p.lintOpenPythonFiles(), TypeMoq.Times.once()); }); diff --git a/src/test/linters/lint.manager.test.ts b/src/test/linters/lint.manager.test.ts index 68f1e20e39c3..06bbd289d4ba 100644 --- a/src/test/linters/lint.manager.test.ts +++ b/src/test/linters/lint.manager.test.ts @@ -8,6 +8,7 @@ import { EnumEx } from '../../client/common/enumUtils'; import { IConfigurationService, ILintingSettings, IPythonSettings, Product } from '../../client/common/types'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; +import { IServiceContainer } from '../../client/ioc/types'; import { LinterManager } from '../../client/linters/linterManager'; import { ILinterManager, LinterId } from '../../client/linters/types'; import { initialize } from '../initialize'; @@ -23,6 +24,7 @@ suite('Linting - Manager', () => { const cont = new Container(); const serviceManager = new ServiceManager(cont); const serviceContainer = new ServiceContainer(cont); + serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); serviceManager.addSingleton(IConfigurationService, ConfigurationService); configService = serviceManager.get(IConfigurationService); diff --git a/src/test/linters/lint.multiroot.test.ts b/src/test/linters/lint.multiroot.test.ts index 6ec8508c3371..df56a04324f4 100644 --- a/src/test/linters/lint.multiroot.test.ts +++ b/src/test/linters/lint.multiroot.test.ts @@ -2,15 +2,19 @@ import * as assert from 'assert'; import * as path from 'path'; import { CancellationTokenSource, ConfigurationTarget, OutputChannel, Uri, workspace } from 'vscode'; import { PythonSettings } from '../../client/common/configSettings'; -import { IConfigurationService, IOutputChannel, Product } from '../../client/common/types'; +import { CTagsProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; +import { ProductService } from '../../client/common/installer/productService'; +import { IProductPathService, IProductService } from '../../client/common/installer/types'; +import { IConfigurationService, IOutputChannel, Product, ProductType } from '../../client/common/types'; import { ILinter, ILinterManager } from '../../client/linters/types'; import { TEST_OUTPUT_CHANNEL } from '../../client/unittests/common/constants'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; +// tslint:disable:max-func-body-length no-invalid-this + const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); -// tslint:disable-next-line:max-func-body-length suite('Multiroot Linting', () => { const pylintSetting = 'linting.pylintEnabled'; const flake8Setting = 'linting.flake8Enabled'; @@ -18,7 +22,6 @@ suite('Multiroot Linting', () => { let ioc: UnitTestIocContainer; suiteSetup(function () { if (!IS_MULTI_ROOT_TEST) { - // tslint:disable-next-line:no-invalid-this this.skip(); } return initialize(); @@ -41,6 +44,13 @@ suite('Multiroot Linting', () => { ioc.registerLinterTypes(); ioc.registerVariableTypes(); ioc.registerPlatformTypes(); + ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); + ioc.serviceManager.addSingleton(IProductPathService, CTagsProductPathService, ProductType.WorkspaceSymbols); + ioc.serviceManager.addSingleton(IProductPathService, FormatterProductPathService, ProductType.Formatter); + ioc.serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); + ioc.serviceManager.addSingleton(IProductPathService, TestFrameworkProductPathService, ProductType.TestFramework); + ioc.serviceManager.addSingleton(IProductPathService, RefactoringLibraryProductPathService, ProductType.RefactoringLibrary); + } async function createLinter(product: Product, resource?: Uri): Promise { diff --git a/src/test/linters/lint.provider.test.ts b/src/test/linters/lint.provider.test.ts index 023ee86223be..ee893b73db8f 100644 --- a/src/test/linters/lint.provider.test.ts +++ b/src/test/linters/lint.provider.test.ts @@ -39,7 +39,7 @@ suite('Linting - Provider', () => { context = TypeMoq.Mock.ofType(); fs = TypeMoq.Mock.ofType(); - fs.setup(x => x.fileExistsAsync(TypeMoq.It.isAny())).returns(() => new Promise((resolve, reject) => resolve(true))); + fs.setup(x => x.fileExists(TypeMoq.It.isAny())).returns(() => new Promise((resolve, reject) => resolve(true))); fs.setup(x => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => true); serviceManager.addSingletonInstance(IFileSystem, fs.object); diff --git a/src/test/linters/lint.test.ts b/src/test/linters/lint.test.ts index 8e73a2878abf..737a2686729c 100644 --- a/src/test/linters/lint.test.ts +++ b/src/test/linters/lint.test.ts @@ -1,12 +1,14 @@ import * as assert from 'assert'; import * as fs from 'fs-extra'; import * as path from 'path'; -import { Uri } from 'vscode'; -import * as vscode from 'vscode'; +import { CancellationTokenSource, ConfigurationTarget, DiagnosticCollection, Uri, window, workspace } from 'vscode'; import { ICommandManager } from '../../client/common/application/types'; import { STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; import { Product } from '../../client/common/installer/productInstaller'; -import { IConfigurationService, IOutputChannel } from '../../client/common/types'; +import { CTagsProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; +import { ProductService } from '../../client/common/installer/productService'; +import { IProductPathService, IProductService } from '../../client/common/installer/types'; +import { IConfigurationService, IOutputChannel, ProductType } from '../../client/common/types'; import { LinterManager } from '../../client/linters/linterManager'; import { ILinterManager, ILintMessage, LintMessageSeverity } from '../../client/linters/types'; import { deleteFile, PythonSettingKeys, rootWorkspaceUri } from '../common'; @@ -93,7 +95,7 @@ const filteredPep88MessagesToBeReturned: ILintMessage[] = [ ]; // tslint:disable-next-line:max-func-body-length -suite('Linting', () => { +suite('Linting - General Tests', () => { let ioc: UnitTestIocContainer; let linterManager: ILinterManager; let configService: IConfigurationService; @@ -120,14 +122,19 @@ suite('Linting', () => { ioc.registerLinterTypes(); ioc.registerVariableTypes(); ioc.registerPlatformTypes(); - linterManager = new LinterManager(ioc.serviceContainer); configService = ioc.serviceContainer.get(IConfigurationService); + ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); + ioc.serviceManager.addSingleton(IProductPathService, CTagsProductPathService, ProductType.WorkspaceSymbols); + ioc.serviceManager.addSingleton(IProductPathService, FormatterProductPathService, ProductType.Formatter); + ioc.serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); + ioc.serviceManager.addSingleton(IProductPathService, TestFrameworkProductPathService, ProductType.TestFramework); + ioc.serviceManager.addSingleton(IProductPathService, RefactoringLibraryProductPathService, ProductType.RefactoringLibrary); } async function resetSettings() { // Don't run these updates in parallel, as they are updating the same file. - const target = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; + const target = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; await configService.updateSettingAsync('linting.enabled', true, rootWorkspaceUri, target); await configService.updateSettingAsync('linting.lintOnSave', false, rootWorkspaceUri, target); @@ -147,11 +154,11 @@ suite('Linting', () => { const output = ioc.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); await configService.updateSettingAsync(setting, enabled, rootWorkspaceUri, - IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace); + IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace); file = file ? file : fileToLint; - const document = await vscode.workspace.openTextDocument(file); - const cancelToken = new vscode.CancellationTokenSource(); + const document = await workspace.openTextDocument(file); + const cancelToken = new CancellationTokenSource(); await linterManager.setActiveLintersAsync([product]); await linterManager.enableLintingAsync(enabled); @@ -186,10 +193,9 @@ suite('Linting', () => { test('Disable Prospector and test linter', async () => { await testEnablingDisablingOfLinter(Product.prospector, false); }); - // test('Enable Prospector and test linter', async () => { - // Fails on Travis. Can be run locally though. - // await testEnablingDisablingOfLinter(Product.prospector, true); - // }); + test('Enable Prospector and test linter', async () => { + await testEnablingDisablingOfLinter(Product.prospector, true); + }); test('Disable Pydocstyle and test linter', async () => { await testEnablingDisablingOfLinter(Product.pydocstyle, false); }); @@ -200,8 +206,8 @@ suite('Linting', () => { // tslint:disable-next-line:no-any async function testLinterMessages(product: Product, pythonFile: string, messagesToBeReceived: ILintMessage[]): Promise { const outputChannel = ioc.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - const cancelToken = new vscode.CancellationTokenSource(); - const document = await vscode.workspace.openTextDocument(pythonFile); + const cancelToken = new CancellationTokenSource(); + const document = await workspace.openTextDocument(pythonFile); await linterManager.setActiveLintersAsync([product], document.uri); const linter = linterManager.createLinter(product, outputChannel, ioc.serviceContainer); @@ -257,15 +263,15 @@ suite('Linting', () => { this.timeout(40000); await closeActiveWindows(); - const document = await vscode.workspace.openTextDocument(path.join(pythoFilesPath, 'print.py')); - await vscode.window.showTextDocument(document); + const document = await workspace.openTextDocument(path.join(pythoFilesPath, 'print.py')); + await window.showTextDocument(document); await configService.updateSettingAsync('linting.enabled', true, workspaceUri); await configService.updateSettingAsync('linting.pylintUseMinimalCheckers', false, workspaceUri); await configService.updateSettingAsync('linting.pylintEnabled', true, workspaceUri); await configService.updateSettingAsync('linting.flake8Enabled', true, workspaceUri); const commands = ioc.serviceContainer.get(ICommandManager); - const collection = await commands.executeCommand('python.runLinting') as vscode.DiagnosticCollection; + const collection = await commands.executeCommand('python.runLinting') as DiagnosticCollection; assert.notEqual(collection, undefined, 'python.runLinting did not return valid diagnostics collection.'); const messages = collection!.get(document.uri); diff --git a/src/test/linters/lintengine.test.ts b/src/test/linters/lintengine.test.ts index 44520df46cdc..92bb25d17ba4 100644 --- a/src/test/linters/lintengine.test.ts +++ b/src/test/linters/lintengine.test.ts @@ -4,7 +4,7 @@ import * as TypeMoq from 'typemoq'; import { OutputChannel, TextDocument, Uri } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; -import { PythonLanguage, STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; +import { PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; import '../../client/common/extensions'; import { IFileSystem } from '../../client/common/platform/types'; import { IConfigurationService, ILintingSettings, IOutputChannel, IPythonSettings } from '../../client/common/types'; @@ -55,7 +55,7 @@ suite('Linting - LintingEngine', () => { }); test('Ensure document.uri is passed into isLintingEnabled', () => { - const doc = mockTextDocument('a.py', PythonLanguage.language, true); + const doc = mockTextDocument('a.py', PYTHON_LANGUAGE, true); try { lintingEngine.lintDocument(doc, 'auto').ignoreErrors(); } catch { @@ -63,7 +63,7 @@ suite('Linting - LintingEngine', () => { } }); test('Ensure document.uri is passed into createLinter', () => { - const doc = mockTextDocument('a.py', PythonLanguage.language, true); + const doc = mockTextDocument('a.py', PYTHON_LANGUAGE, true); try { lintingEngine.lintDocument(doc, 'auto').ignoreErrors(); } catch { @@ -72,7 +72,7 @@ suite('Linting - LintingEngine', () => { }); test('Verify files that match ignore pattern are not linted', async () => { - const doc = mockTextDocument('a1.py', PythonLanguage.language, true, ['a*.py']); + const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, true, ['a*.py']); await lintingEngine.lintDocument(doc, 'auto'); lintManager.verify(l => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); }); @@ -84,29 +84,29 @@ suite('Linting - LintingEngine', () => { }); test('Ensure files with git scheme are not linted', async () => { - const doc = mockTextDocument('a1.py', PythonLanguage.language, false, [], 'git'); + const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'git'); await lintingEngine.lintDocument(doc, 'auto'); lintManager.verify(l => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); }); test('Ensure files with showModifications scheme are not linted', async () => { - const doc = mockTextDocument('a1.py', PythonLanguage.language, false, [], 'showModifications'); + const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'showModifications'); await lintingEngine.lintDocument(doc, 'auto'); lintManager.verify(l => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); }); test('Ensure files with svn scheme are not linted', async () => { - const doc = mockTextDocument('a1.py', PythonLanguage.language, false, [], 'svn'); + const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'svn'); await lintingEngine.lintDocument(doc, 'auto'); lintManager.verify(l => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); }); test('Ensure non-existing files are not linted', async () => { - const doc = mockTextDocument('file.py', PythonLanguage.language, false, []); + const doc = mockTextDocument('file.py', PYTHON_LANGUAGE, false, []); await lintingEngine.lintDocument(doc, 'auto'); lintManager.verify(l => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); }); function mockTextDocument(fileName: string, language: string, exists: boolean, ignorePattern: string[] = [], scheme?: string): TextDocument { - fileSystem.setup(x => x.fileExistsAsync(TypeMoq.It.isAnyString())).returns(() => Promise.resolve(exists)); + fileSystem.setup(x => x.fileExists(TypeMoq.It.isAnyString())).returns(() => Promise.resolve(exists)); lintSettings.setup(l => l.ignorePatterns).returns(() => ignorePattern); settings.setup(x => x.linting).returns(() => lintSettings.object); diff --git a/src/test/linters/pylint.test.ts b/src/test/linters/pylint.test.ts index ce08b0ac5c95..66bc7fdb956d 100644 --- a/src/test/linters/pylint.test.ts +++ b/src/test/linters/pylint.test.ts @@ -6,7 +6,7 @@ import { Container } from 'inversify'; import * as os from 'os'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, OutputChannel, TextDocument, Uri, WorkspaceFolder } from 'vscode'; +import { CancellationTokenSource, DiagnosticSeverity, OutputChannel, TextDocument, Uri, WorkspaceFolder } from 'vscode'; import { IWorkspaceService } from '../../client/common/application/types'; import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; import { IPythonToolExecutionService } from '../../client/common/process/types'; @@ -19,7 +19,7 @@ import { ILinterManager } from '../../client/linters/types'; import { MockLintingSettings } from '../mockClasses'; // tslint:disable-next-line:max-func-body-length -suite('Linting - Pylintrc search', () => { +suite('Linting - Pylint', () => { const basePath = '/user/a/b/c/d'; const pylintrc = 'pylintrc'; const dotPylintrc = '.pylintrc'; @@ -63,11 +63,11 @@ suite('Linting - Pylintrc search', () => { }); test('pylintrc in the file folder', async () => { - fileSystem.setup(x => x.fileExistsAsync(path.join(basePath, pylintrc))).returns(() => Promise.resolve(true)); + fileSystem.setup(x => x.fileExists(path.join(basePath, pylintrc))).returns(() => Promise.resolve(true)); let result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); expect(result).to.be.equal(true, `'${pylintrc}' not detected in the file folder.`); - fileSystem.setup(x => x.fileExistsAsync(path.join(basePath, dotPylintrc))).returns(() => Promise.resolve(true)); + fileSystem.setup(x => x.fileExists(path.join(basePath, dotPylintrc))).returns(() => Promise.resolve(true)); result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); expect(result).to.be.equal(true, `'${dotPylintrc}' not detected in the file folder.`); }); @@ -77,10 +77,10 @@ suite('Linting - Pylintrc search', () => { const module3 = path.join('/user/a/b', '__init__.py'); const rc = path.join('/user/a/b/c', pylintrc); - fileSystem.setup(x => x.fileExistsAsync(module1)).returns(() => Promise.resolve(true)); - fileSystem.setup(x => x.fileExistsAsync(module2)).returns(() => Promise.resolve(true)); - fileSystem.setup(x => x.fileExistsAsync(module3)).returns(() => Promise.resolve(true)); - fileSystem.setup(x => x.fileExistsAsync(rc)).returns(() => Promise.resolve(true)); + fileSystem.setup(x => x.fileExists(module1)).returns(() => Promise.resolve(true)); + fileSystem.setup(x => x.fileExists(module2)).returns(() => Promise.resolve(true)); + fileSystem.setup(x => x.fileExists(module3)).returns(() => Promise.resolve(true)); + fileSystem.setup(x => x.fileExists(rc)).returns(() => Promise.resolve(true)); const result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); expect(result).to.be.equal(true, `'${pylintrc}' not detected in the module tree.`); @@ -92,10 +92,10 @@ suite('Linting - Pylintrc search', () => { const module3 = path.join('/user/a/b', '__init__.py'); const rc = path.join('/user/a/b/c', pylintrc); - fileSystem.setup(x => x.fileExistsAsync(module1)).returns(() => Promise.resolve(true)); - fileSystem.setup(x => x.fileExistsAsync(module2)).returns(() => Promise.resolve(true)); - fileSystem.setup(x => x.fileExistsAsync(module3)).returns(() => Promise.resolve(true)); - fileSystem.setup(x => x.fileExistsAsync(rc)).returns(() => Promise.resolve(true)); + fileSystem.setup(x => x.fileExists(module1)).returns(() => Promise.resolve(true)); + fileSystem.setup(x => x.fileExists(module2)).returns(() => Promise.resolve(true)); + fileSystem.setup(x => x.fileExists(module3)).returns(() => Promise.resolve(true)); + fileSystem.setup(x => x.fileExists(rc)).returns(() => Promise.resolve(true)); const result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); expect(result).to.be.equal(true, `'${dotPylintrc}' not detected in the module tree.`); @@ -103,7 +103,7 @@ suite('Linting - Pylintrc search', () => { test('.pylintrc up the ~ folder', async () => { const home = os.homedir(); const rc = path.join(home, dotPylintrc); - fileSystem.setup(x => x.fileExistsAsync(rc)).returns(() => Promise.resolve(true)); + fileSystem.setup(x => x.fileExists(rc)).returns(() => Promise.resolve(true)); const result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); expect(result).to.be.equal(true, `'${dotPylintrc}' not detected in the ~ folder.`); @@ -111,14 +111,14 @@ suite('Linting - Pylintrc search', () => { test('pylintrc up the ~/.config folder', async () => { const home = os.homedir(); const rc = path.join(home, '.config', pylintrc); - fileSystem.setup(x => x.fileExistsAsync(rc)).returns(() => Promise.resolve(true)); + fileSystem.setup(x => x.fileExists(rc)).returns(() => Promise.resolve(true)); const result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); expect(result).to.be.equal(true, `'${pylintrc}' not detected in the ~/.config folder.`); }); test('pylintrc in the /etc folder', async () => { const rc = path.join('/etc', pylintrc); - fileSystem.setup(x => x.fileExistsAsync(rc)).returns(() => Promise.resolve(true)); + fileSystem.setup(x => x.fileExists(rc)).returns(() => Promise.resolve(true)); const result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); expect(result).to.be.equal(true, `'${pylintrc}' not detected in the /etc folder.`); @@ -127,7 +127,7 @@ suite('Linting - Pylintrc search', () => { const root = '/user/a'; const midFolder = '/user/a/b'; fileSystem - .setup(x => x.fileExistsAsync(path.join(midFolder, pylintrc))) + .setup(x => x.fileExists(path.join(midFolder, pylintrc))) .returns(() => Promise.resolve(true)); const result = await Pylint.hasConfigrationFileInWorkspace(fileSystem.object, basePath, root); @@ -136,7 +136,7 @@ suite('Linting - Pylintrc search', () => { test('minArgs - pylintrc between the file and the workspace root', async () => { fileSystem - .setup(x => x.fileExistsAsync(path.join('/user/a/b', pylintrc))) + .setup(x => x.fileExists(path.join('/user/a/b', pylintrc))) .returns(() => Promise.resolve(true)); await testPylintArguments('/user/a/b/c', '/user/a', false); @@ -149,7 +149,7 @@ suite('Linting - Pylintrc search', () => { test('minArgs - pylintrc next to the file', async () => { const fileFolder = '/user/a/b/c'; fileSystem - .setup(x => x.fileExistsAsync(path.join(fileFolder, pylintrc))) + .setup(x => x.fileExists(path.join(fileFolder, pylintrc))) .returns(() => Promise.resolve(true)); await testPylintArguments(fileFolder, '/user/a', false); @@ -158,7 +158,7 @@ suite('Linting - Pylintrc search', () => { test('minArgs - pylintrc at the workspace root', async () => { const root = '/user/a'; fileSystem - .setup(x => x.fileExistsAsync(path.join(root, pylintrc))) + .setup(x => x.fileExists(path.join(root, pylintrc))) .returns(() => Promise.resolve(true)); await testPylintArguments('/user/a/b/c', root, false); @@ -199,4 +199,47 @@ suite('Linting - Pylintrc search', () => { expect(execInfo!.args.findIndex(x => x.indexOf('--disable=all') >= 0), 'Minimal args passed to pylint while pylintrc exists.').to.be.eq(expectedMinArgs ? 0 : -1); } + test('Negative column numbers should be treated 0', async () => { + const fileFolder = '/user/a/b/c'; + const outputChannel = TypeMoq.Mock.ofType(); + const pylinter = new Pylint(outputChannel.object, serviceContainer); + + const document = TypeMoq.Mock.ofType(); + document.setup(x => x.uri).returns(() => Uri.file(path.join(fileFolder, 'test.py'))); + + const wsf = TypeMoq.Mock.ofType(); + wsf.setup(x => x.uri).returns(() => Uri.file(fileFolder)); + + workspace.setup(x => x.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => wsf.object); + + const linterOutput = ['No config file found, using default configuration', + '************* Module test', + '1,1,convention,C0111:Missing module docstring', + '3,-1,error,E1305:Too many arguments for format string'].join(os.EOL); + execService + .setup(x => x.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: linterOutput, stderr: '' })); + + const lintSettings = new MockLintingSettings(); + lintSettings.pylintUseMinimalCheckers = false; + lintSettings.maxNumberOfProblems = 1000; + lintSettings.pylintPath = 'pyLint'; + lintSettings.pylintEnabled = true; + lintSettings.pylintCategorySeverity = { + convention: DiagnosticSeverity.Hint, + error: DiagnosticSeverity.Error, + fatal: DiagnosticSeverity.Error, + refactor: DiagnosticSeverity.Hint, + warning: DiagnosticSeverity.Warning + }; + + const settings = TypeMoq.Mock.ofType(); + settings.setup(x => x.linting).returns(() => lintSettings); + config.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + + const messages = await pylinter.lint(document.object, new CancellationTokenSource().token); + expect(messages).to.be.lengthOf(2); + expect(messages[0].column).to.be.equal(1); + expect(messages[1].column).to.be.equal(0); + }); }); diff --git a/src/test/mocks/proc.ts b/src/test/mocks/proc.ts index e910d0da4720..dcf1d5388859 100644 --- a/src/test/mocks/proc.ts +++ b/src/test/mocks/proc.ts @@ -1,5 +1,4 @@ import { EventEmitter } from 'events'; -import { inject, injectable } from 'inversify'; import 'rxjs/add/observable/of'; import { Observable } from 'rxjs/Observable'; import { ExecutionResult, IProcessService, ObservableExecutionResult, Output, SpawnOptions } from '../../client/common/process/types'; @@ -9,9 +8,8 @@ type ExecCallback = (result: ExecutionResult) => void; export const IOriginalProcessService = Symbol('IProcessService'); -@injectable() export class MockProcessService extends EventEmitter implements IProcessService { - constructor( @inject(IOriginalProcessService) private procService: IProcessService) { + constructor(private procService: IProcessService) { super(); } public onExecObservable(handler: (file: string, args: string[], options: SpawnOptions, callback: ExecObservableCallback) => void) { diff --git a/src/test/mocks/vsc/README.md b/src/test/mocks/vsc/README.md new file mode 100644 index 000000000000..39fbe1508bbd --- /dev/null +++ b/src/test/mocks/vsc/README.md @@ -0,0 +1,6 @@ +# This folder contains classes exposed by VS Code required in running the unit tests. +* These classes are only used when running unit tests that are not hosted by VS Code. +* So even if these classes were buggy, it doesn't matter, running the tests under VS Code host will ensure the right classes are available. +* The purpose of these classes are to avoid having to use VS Code as the hosting environment for the tests, making it faster to run the tests and not have to rely on VS Code host to run the tests. +* Everyting in here must either be within a namespace prefixed with `vscMock` or exported types must be prefixed with `vscMock`. +This is to prevent developers from accidentally importing them into their Code. Even if they did, the extension would fail to load and tests would fail. diff --git a/src/test/mocks/vsc/arrays.ts b/src/test/mocks/vsc/arrays.ts new file mode 100644 index 000000000000..bae8cc34b8b2 --- /dev/null +++ b/src/test/mocks/vsc/arrays.ts @@ -0,0 +1,403 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +// tslint:disable:all + +export namespace vscMockArrays { + /** + * Returns the last element of an array. + * @param array The array. + * @param n Which element from the end (default is zero). + */ + export function tail(array: T[], n: number = 0): T { + return array[array.length - (1 + n)]; + } + + export function equals(one: T[], other: T[], itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean { + if (one.length !== other.length) { + return false; + } + + for (let i = 0, len = one.length; i < len; i++) { + if (!itemEquals(one[i], other[i])) { + return false; + } + } + + return true; + } + + export function binarySearch(array: T[], key: T, comparator: (op1: T, op2: T) => number): number { + let low = 0, + high = array.length - 1; + + while (low <= high) { + let mid = ((low + high) / 2) | 0; + let comp = comparator(array[mid], key); + if (comp < 0) { + low = mid + 1; + } else if (comp > 0) { + high = mid - 1; + } else { + return mid; + } + } + return -(low + 1); + } + + /** + * Takes a sorted array and a function p. The array is sorted in such a way that all elements where p(x) is false + * are located before all elements where p(x) is true. + * @returns the least x for which p(x) is true or array.length if no element fullfills the given function. + */ + export function findFirst(array: T[], p: (x: T) => boolean): number { + let low = 0, high = array.length; + if (high === 0) { + return 0; // no children + } + while (low < high) { + let mid = Math.floor((low + high) / 2); + if (p(array[mid])) { + high = mid; + } else { + low = mid + 1; + } + } + return low; + } + + /** + * Like `Array#sort` but always stable. Usually runs a little slower `than Array#sort` + * so only use this when actually needing stable sort. + */ + export function mergeSort(data: T[], compare: (a: T, b: T) => number): T[] { + _divideAndMerge(data, compare); + return data; + } + + function _divideAndMerge(data: T[], compare: (a: T, b: T) => number): void { + if (data.length <= 1) { + // sorted + return; + } + const p = (data.length / 2) | 0; + const left = data.slice(0, p); + const right = data.slice(p); + + _divideAndMerge(left, compare); + _divideAndMerge(right, compare); + + let leftIdx = 0; + let rightIdx = 0; + let i = 0; + while (leftIdx < left.length && rightIdx < right.length) { + let ret = compare(left[leftIdx], right[rightIdx]); + if (ret <= 0) { + // smaller_equal -> take left to preserve order + data[i++] = left[leftIdx++]; + } else { + // greater -> take right + data[i++] = right[rightIdx++]; + } + } + while (leftIdx < left.length) { + data[i++] = left[leftIdx++]; + } + while (rightIdx < right.length) { + data[i++] = right[rightIdx++]; + } + } + + export function groupBy(data: T[], compare: (a: T, b: T) => number): T[][] { + const result: T[][] = []; + let currentGroup: T[]; + for (const element of mergeSort(data.slice(0), compare)) { + if (!currentGroup || compare(currentGroup[0], element) !== 0) { + currentGroup = [element]; + result.push(currentGroup); + } else { + currentGroup.push(element); + } + } + return result; + } + + type IMutableSplice = Array & any & { + deleteCount: number; + } + type ISplice = Array & any; + + /** + * Diffs two *sorted* arrays and computes the splices which apply the diff. + */ + export function sortedDiff(before: T[], after: T[], compare: (a: T, b: T) => number): ISplice[] { + const result: IMutableSplice[] = []; + + function pushSplice(start: number, deleteCount: number, toInsert: T[]): void { + if (deleteCount === 0 && toInsert.length === 0) { + return; + } + + const latest = result[result.length - 1]; + + if (latest && latest.start + latest.deleteCount === start) { + latest.deleteCount += deleteCount; + latest.toInsert.push(...toInsert); + } else { + result.push({ start, deleteCount, toInsert }); + } + } + + let beforeIdx = 0; + let afterIdx = 0; + + while (true) { + if (beforeIdx === before.length) { + pushSplice(beforeIdx, 0, after.slice(afterIdx)); + break; + } + if (afterIdx === after.length) { + pushSplice(beforeIdx, before.length - beforeIdx, []); + break; + } + + const beforeElement = before[beforeIdx]; + const afterElement = after[afterIdx]; + const n = compare(beforeElement, afterElement); + if (n === 0) { + // equal + beforeIdx += 1; + afterIdx += 1; + } else if (n < 0) { + // beforeElement is smaller -> before element removed + pushSplice(beforeIdx, 1, []); + beforeIdx += 1; + } else if (n > 0) { + // beforeElement is greater -> after element added + pushSplice(beforeIdx, 0, [afterElement]); + afterIdx += 1; + } + } + + return result; + } + + /** + * Takes two *sorted* arrays and computes their delta (removed, added elements). + * Finishes in `Math.min(before.length, after.length)` steps. + * @param before + * @param after + * @param compare + */ + export function delta(before: T[], after: T[], compare: (a: T, b: T) => number): { removed: T[], added: T[] } { + const splices = sortedDiff(before, after, compare); + const removed: T[] = []; + const added: T[] = []; + + for (const splice of splices) { + removed.push(...before.slice(splice.start, splice.start + splice.deleteCount)); + added.push(...splice.toInsert); + } + + return { removed, added }; + } + + /** + * Returns the top N elements from the array. + * + * Faster than sorting the entire array when the array is a lot larger than N. + * + * @param array The unsorted array. + * @param compare A sort function for the elements. + * @param n The number of elements to return. + * @return The first n elemnts from array when sorted with compare. + */ + export function top(array: T[], compare: (a: T, b: T) => number, n: number): T[] { + if (n === 0) { + return []; + } + const result = array.slice(0, n).sort(compare); + topStep(array, compare, result, n, array.length); + return result; + } + + function topStep(array: T[], compare: (a: T, b: T) => number, result: T[], i: number, m: number): void { + for (const n = result.length; i < m; i++) { + const element = array[i]; + if (compare(element, result[n - 1]) < 0) { + result.pop(); + const j = findFirst(result, e => compare(element, e) < 0); + result.splice(j, 0, element); + } + } + } + + /** + * @returns a new array with all undefined or null values removed. The original array is not modified at all. + */ + export function coalesce(array: T[]): T[] { + if (!array) { + return array; + } + + return array.filter(e => !!e); + } + + /** + * Moves the element in the array for the provided positions. + */ + export function move(array: any[], from: number, to: number): void { + array.splice(to, 0, array.splice(from, 1)[0]); + } + + /** + * @returns {{false}} if the provided object is an array + * and not empty. + */ + export function isFalsyOrEmpty(obj: any): boolean { + return !Array.isArray(obj) || (>obj).length === 0; + } + + /** + * Removes duplicates from the given array. The optional keyFn allows to specify + * how elements are checked for equalness by returning a unique string for each. + */ + export function distinct(array: T[], keyFn?: (t: T) => string): T[] { + if (!keyFn) { + return array.filter((element, position) => { + return array.indexOf(element) === position; + }); + } + + const seen: { [key: string]: boolean; } = Object.create(null); + return array.filter((elem) => { + const key = keyFn(elem); + if (seen[key]) { + return false; + } + + seen[key] = true; + + return true; + }); + } + + export function uniqueFilter(keyFn: (t: T) => string): (t: T) => boolean { + const seen: { [key: string]: boolean; } = Object.create(null); + + return element => { + const key = keyFn(element); + + if (seen[key]) { + return false; + } + + seen[key] = true; + return true; + }; + } + + export function firstIndex(array: T[], fn: (item: T) => boolean): number { + for (let i = 0; i < array.length; i++) { + const element = array[i]; + + if (fn(element)) { + return i; + } + } + + return -1; + } + + export function first(array: T[], fn: (item: T) => boolean, notFoundValue: T = null): T { + const index = firstIndex(array, fn); + return index < 0 ? notFoundValue : array[index]; + } + + export function commonPrefixLength(one: T[], other: T[], equals: (a: T, b: T) => boolean = (a, b) => a === b): number { + let result = 0; + + for (let i = 0, len = Math.min(one.length, other.length); i < len && equals(one[i], other[i]); i++) { + result++; + } + + return result; + } + + export function flatten(arr: T[][]): T[] { + return [].concat(...arr); + } + + export function range(to: number): number[]; + export function range(from: number, to: number): number[]; + export function range(arg: number, to?: number): number[] { + let from = typeof to === 'number' ? arg : 0; + + if (typeof to === 'number') { + from = arg; + } else { + from = 0; + to = arg; + } + + const result: number[] = []; + + if (from <= to) { + for (let i = from; i < to; i++) { + result.push(i); + } + } else { + for (let i = from; i > to; i--) { + result.push(i); + } + } + + return result; + } + + export function fill(num: number, valueFn: () => T, arr: T[] = []): T[] { + for (let i = 0; i < num; i++) { + arr[i] = valueFn(); + } + + return arr; + } + + export function index(array: T[], indexer: (t: T) => string): { [key: string]: T; }; + export function index(array: T[], indexer: (t: T) => string, merger?: (t: T, r: R) => R): { [key: string]: R; }; + export function index(array: T[], indexer: (t: T) => string, merger: (t: T, r: R) => R = t => t as any): { [key: string]: R; } { + return array.reduce((r, t) => { + const key = indexer(t); + r[key] = merger(t, r[key]); + return r; + }, Object.create(null)); + } + + /** + * Inserts an element into an array. Returns a function which, when + * called, will remove that element from the array. + */ + export function insert(array: T[], element: T): () => void { + array.push(element); + + return () => { + const index = array.indexOf(element); + if (index > -1) { + array.splice(index, 1); + } + }; + } + + /** + * Insert `insertArr` inside `target` at `insertIndex`. + * Please don't touch unless you understand https://jsperf.com/inserting-an-array-within-an-array + */ + export function arrayInsert(target: T[], insertIndex: number, insertArr: T[]): T[] { + const before = target.slice(0, insertIndex); + const after = target.slice(insertIndex); + return before.concat(insertArr, after); + } +} diff --git a/src/test/mocks/vsc/extHostedTypes.ts b/src/test/mocks/vsc/extHostedTypes.ts new file mode 100644 index 000000000000..0d5f6bc2f2ae --- /dev/null +++ b/src/test/mocks/vsc/extHostedTypes.ts @@ -0,0 +1,1922 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +// import * as crypto from 'crypto'; + +// tslint:disable:all + +import { relative } from 'path'; +import * as vscode from 'vscode'; +import { vscMockHtmlContent } from './htmlContent'; +import { vscMockStrings } from './strings'; +import { vscUri } from './uri'; + +export namespace vscMockExtHostedTypes { + + export interface IRelativePattern { + base: string; + pattern: string; + pathToRelative(from: string, to: string): string; + } + + // tslint:disable:all + const illegalArgument = (msg = 'Illegal Argument') => new Error(msg); + + export class Disposable { + + static from(...disposables: { dispose(): any }[]): Disposable { + return new Disposable(function () { + if (disposables) { + for (let disposable of disposables) { + if (disposable && typeof disposable.dispose === 'function') { + disposable.dispose(); + } + } + disposables = undefined; + } + }); + } + + private _callOnDispose: Function; + + constructor(callOnDispose: Function) { + this._callOnDispose = callOnDispose; + } + + dispose(): any { + if (typeof this._callOnDispose === 'function') { + this._callOnDispose(); + this._callOnDispose = undefined; + } + } + } + + export class Position { + + static Min(...positions: Position[]): Position { + let result = positions.pop(); + for (let p of positions) { + if (p.isBefore(result)) { + result = p; + } + } + return result; + } + + static Max(...positions: Position[]): Position { + let result = positions.pop(); + for (let p of positions) { + if (p.isAfter(result)) { + result = p; + } + } + return result; + } + + static isPosition(other: any): other is Position { + if (!other) { + return false; + } + if (other instanceof Position) { + return true; + } + let { line, character } = other; + if (typeof line === 'number' && typeof character === 'number') { + return true; + } + return false; + } + + private _line: number; + private _character: number; + + get line(): number { + return this._line; + } + + get character(): number { + return this._character; + } + + constructor(line: number, character: number) { + if (line < 0) { + throw illegalArgument('line must be non-negative'); + } + if (character < 0) { + throw illegalArgument('character must be non-negative'); + } + this._line = line; + this._character = character; + } + + isBefore(other: Position): boolean { + if (this._line < other._line) { + return true; + } + if (other._line < this._line) { + return false; + } + return this._character < other._character; + } + + isBeforeOrEqual(other: Position): boolean { + if (this._line < other._line) { + return true; + } + if (other._line < this._line) { + return false; + } + return this._character <= other._character; + } + + isAfter(other: Position): boolean { + return !this.isBeforeOrEqual(other); + } + + isAfterOrEqual(other: Position): boolean { + return !this.isBefore(other); + } + + isEqual(other: Position): boolean { + return this._line === other._line && this._character === other._character; + } + + compareTo(other: Position): number { + if (this._line < other._line) { + return -1; + } else if (this._line > other.line) { + return 1; + } else { + // equal line + if (this._character < other._character) { + return -1; + } else if (this._character > other._character) { + return 1; + } else { + // equal line and character + return 0; + } + } + } + + translate(change: { lineDelta?: number; characterDelta?: number; }): Position; + translate(lineDelta?: number, characterDelta?: number): Position; + translate(lineDeltaOrChange: number | { lineDelta?: number; characterDelta?: number; }, characterDelta: number = 0): Position { + + if (lineDeltaOrChange === null || characterDelta === null) { + throw illegalArgument(); + } + + let lineDelta: number; + if (typeof lineDeltaOrChange === 'undefined') { + lineDelta = 0; + } else if (typeof lineDeltaOrChange === 'number') { + lineDelta = lineDeltaOrChange; + } else { + lineDelta = typeof lineDeltaOrChange.lineDelta === 'number' ? lineDeltaOrChange.lineDelta : 0; + characterDelta = typeof lineDeltaOrChange.characterDelta === 'number' ? lineDeltaOrChange.characterDelta : 0; + } + + if (lineDelta === 0 && characterDelta === 0) { + return this; + } + return new Position(this.line + lineDelta, this.character + characterDelta); + } + + with(change: { line?: number; character?: number; }): Position; + with(line?: number, character?: number): Position; + with(lineOrChange: number | { line?: number; character?: number; }, character: number = this.character): Position { + + if (lineOrChange === null || character === null) { + throw illegalArgument(); + } + + let line: number; + if (typeof lineOrChange === 'undefined') { + line = this.line; + + } else if (typeof lineOrChange === 'number') { + line = lineOrChange; + + } else { + line = typeof lineOrChange.line === 'number' ? lineOrChange.line : this.line; + character = typeof lineOrChange.character === 'number' ? lineOrChange.character : this.character; + } + + if (line === this.line && character === this.character) { + return this; + } + return new Position(line, character); + } + + toJSON(): any { + return { line: this.line, character: this.character }; + } + } + + export class Range { + + static isRange(thing: any): thing is vscode.Range { + if (thing instanceof Range) { + return true; + } + if (!thing) { + return false; + } + return Position.isPosition((thing).start) + && Position.isPosition((thing.end)); + } + + protected _start: Position; + protected _end: Position; + + get start(): Position { + return this._start; + } + + get end(): Position { + return this._end; + } + + constructor(start: Position, end: Position); + constructor(startLine: number, startColumn: number, endLine: number, endColumn: number); + constructor(startLineOrStart: number | Position, startColumnOrEnd: number | Position, endLine?: number, endColumn?: number) { + let start: Position; + let end: Position; + + if (typeof startLineOrStart === 'number' && typeof startColumnOrEnd === 'number' && typeof endLine === 'number' && typeof endColumn === 'number') { + start = new Position(startLineOrStart, startColumnOrEnd); + end = new Position(endLine, endColumn); + } else if (startLineOrStart instanceof Position && startColumnOrEnd instanceof Position) { + start = startLineOrStart; + end = startColumnOrEnd; + } + + if (!start || !end) { + throw new Error('Invalid arguments'); + } + + if (start.isBefore(end)) { + this._start = start; + this._end = end; + } else { + this._start = end; + this._end = start; + } + } + + contains(positionOrRange: Position | Range): boolean { + if (positionOrRange instanceof Range) { + return this.contains(positionOrRange._start) + && this.contains(positionOrRange._end); + + } else if (positionOrRange instanceof Position) { + if (positionOrRange.isBefore(this._start)) { + return false; + } + if (this._end.isBefore(positionOrRange)) { + return false; + } + return true; + } + return false; + } + + isEqual(other: Range): boolean { + return this._start.isEqual(other._start) && this._end.isEqual(other._end); + } + + intersection(other: Range): Range { + let start = Position.Max(other.start, this._start); + let end = Position.Min(other.end, this._end); + if (start.isAfter(end)) { + // this happens when there is no overlap: + // |-----| + // |----| + return undefined; + } + return new Range(start, end); + } + + union(other: Range): Range { + if (this.contains(other)) { + return this; + } else if (other.contains(this)) { + return other; + } + let start = Position.Min(other.start, this._start); + let end = Position.Max(other.end, this.end); + return new Range(start, end); + } + + get isEmpty(): boolean { + return this._start.isEqual(this._end); + } + + get isSingleLine(): boolean { + return this._start.line === this._end.line; + } + + with(change: { start?: Position, end?: Position }): Range; + with(start?: Position, end?: Position): Range; + with(startOrChange: Position | { start?: Position, end?: Position }, end: Position = this.end): Range { + + if (startOrChange === null || end === null) { + throw illegalArgument(); + } + + let start: Position; + if (!startOrChange) { + start = this.start; + + } else if (Position.isPosition(startOrChange)) { + start = startOrChange; + + } else { + start = startOrChange.start || this.start; + end = startOrChange.end || this.end; + } + + if (start.isEqual(this._start) && end.isEqual(this.end)) { + return this; + } + return new Range(start, end); + } + + toJSON(): any { + return [this.start, this.end]; + } + } + + export class Selection extends Range { + + static isSelection(thing: any): thing is Selection { + if (thing instanceof Selection) { + return true; + } + if (!thing) { + return false; + } + return Range.isRange(thing) + && Position.isPosition((thing).anchor) + && Position.isPosition((thing).active) + && typeof (thing).isReversed === 'boolean'; + } + + private _anchor: Position; + + public get anchor(): Position { + return this._anchor; + } + + private _active: Position; + + public get active(): Position { + return this._active; + } + + constructor(anchor: Position, active: Position); + constructor(anchorLine: number, anchorColumn: number, activeLine: number, activeColumn: number); + constructor(anchorLineOrAnchor: number | Position, anchorColumnOrActive: number | Position, activeLine?: number, activeColumn?: number) { + let anchor: Position; + let active: Position; + + if (typeof anchorLineOrAnchor === 'number' && typeof anchorColumnOrActive === 'number' && typeof activeLine === 'number' && typeof activeColumn === 'number') { + anchor = new Position(anchorLineOrAnchor, anchorColumnOrActive); + active = new Position(activeLine, activeColumn); + } else if (anchorLineOrAnchor instanceof Position && anchorColumnOrActive instanceof Position) { + anchor = anchorLineOrAnchor; + active = anchorColumnOrActive; + } + + if (!anchor || !active) { + throw new Error('Invalid arguments'); + } + + super(anchor, active); + + this._anchor = anchor; + this._active = active; + } + + get isReversed(): boolean { + return this._anchor === this._end; + } + + toJSON() { + return { + start: this.start, + end: this.end, + active: this.active, + anchor: this.anchor + }; + } + } + + export enum EndOfLine { + LF = 1, + CRLF = 2 + } + + export class TextEdit { + + static isTextEdit(thing: any): thing is TextEdit { + if (thing instanceof TextEdit) { + return true; + } + if (!thing) { + return false; + } + return Range.isRange((thing)) + && typeof (thing).newText === 'string'; + } + + static replace(range: Range, newText: string): TextEdit { + return new TextEdit(range, newText); + } + + static insert(position: Position, newText: string): TextEdit { + return TextEdit.replace(new Range(position, position), newText); + } + + static delete(range: Range): TextEdit { + return TextEdit.replace(range, ''); + } + + static setEndOfLine(eol: EndOfLine): TextEdit { + let ret = new TextEdit(undefined, undefined); + ret.newEol = eol; + return ret; + } + + protected _range: Range; + protected _newText: string; + protected _newEol: EndOfLine; + + get range(): Range { + return this._range; + } + + set range(value: Range) { + if (value && !Range.isRange(value)) { + throw illegalArgument('range'); + } + this._range = value; + } + + get newText(): string { + return this._newText || ''; + } + + set newText(value: string) { + if (value && typeof value !== 'string') { + throw illegalArgument('newText'); + } + this._newText = value; + } + + get newEol(): EndOfLine { + return this._newEol; + } + + set newEol(value: EndOfLine) { + if (value && typeof value !== 'number') { + throw illegalArgument('newEol'); + } + this._newEol = value; + } + + constructor(range: Range, newText: string) { + this.range = range; + this.newText = newText; + } + + toJSON(): any { + return { + range: this.range, + newText: this.newText, + newEol: this._newEol + }; + } + } + + export class WorkspaceEdit implements vscode.WorkspaceEdit { + + private _seqPool: number = 0; + + private _resourceEdits: { seq: number, from: vscUri.URI, to: vscUri.URI }[] = []; + private _textEdits = new Map(); + + // createResource(uri: vscode.Uri): void { + // this.renameResource(undefined, uri); + // } + + // deleteResource(uri: vscode.Uri): void { + // this.renameResource(uri, undefined); + // } + + // renameResource(from: vscode.Uri, to: vscode.Uri): void { + // this._resourceEdits.push({ seq: this._seqPool++, from, to }); + // } + + // resourceEdits(): [vscode.Uri, vscode.Uri][] { + // return this._resourceEdits.map(({ from, to }) => (<[vscode.Uri, vscode.Uri]>[from, to])); + // } + + replace(uri: vscUri.URI, range: Range, newText: string): void { + let edit = new TextEdit(range, newText); + let array = this.get(uri); + if (array) { + array.push(edit); + } else { + array = [edit]; + } + this.set(uri, array); + } + + insert(resource: vscUri.URI, position: Position, newText: string): void { + this.replace(resource, new Range(position, position), newText); + } + + delete(resource: vscUri.URI, range: Range): void { + this.replace(resource, range, ''); + } + + has(uri: vscUri.URI): boolean { + return this._textEdits.has(uri.toString()); + } + + set(uri: vscUri.URI, edits: TextEdit[]): void { + let data = this._textEdits.get(uri.toString()); + if (!data) { + data = { seq: this._seqPool++, uri, edits: [] }; + this._textEdits.set(uri.toString(), data); + } + if (!edits) { + data.edits = undefined; + } else { + data.edits = edits.slice(0); + } + } + + get(uri: vscUri.URI): TextEdit[] { + if (!this._textEdits.has(uri.toString())) { + return undefined; + } + const { edits } = this._textEdits.get(uri.toString()); + return edits ? edits.slice() : undefined; + } + + entries(): [vscUri.URI, TextEdit[]][] { + const res: [vscUri.URI, TextEdit[]][] = []; + this._textEdits.forEach(value => res.push([value.uri, value.edits])); + return res.slice(); + } + + allEntries(): ([vscUri.URI, TextEdit[]] | [vscUri.URI, vscUri.URI])[] { + return this.entries(); + // // use the 'seq' the we have assigned when inserting + // // the operation and use that order in the resulting + // // array + // const res: ([vscUri.URI, TextEdit[]] | [vscUri.URI,vscUri.URI])[] = []; + // this._textEdits.forEach(value => { + // const { seq, uri, edits } = value; + // res[seq] = [uri, edits]; + // }); + // this._resourceEdits.forEach(value => { + // const { seq, from, to } = value; + // res[seq] = [from, to]; + // }); + // return res; + } + + get size(): number { + return this._textEdits.size + this._resourceEdits.length; + } + + toJSON(): any { + return this.entries(); + } + } + + export class SnippetString { + + static isSnippetString(thing: any): thing is SnippetString { + if (thing instanceof SnippetString) { + return true; + } + if (!thing) { + return false; + } + return typeof (thing).value === 'string'; + } + + private static _escape(value: string): string { + return value.replace(/\$|}|\\/g, '\\$&'); + } + + private _tabstop: number = 1; + + value: string; + + constructor(value?: string) { + this.value = value || ''; + } + + appendText(string: string): SnippetString { + this.value += SnippetString._escape(string); + return this; + } + + appendTabstop(number: number = this._tabstop++): SnippetString { + this.value += '$'; + this.value += number; + return this; + } + + appendPlaceholder(value: string | ((snippet: SnippetString) => any), number: number = this._tabstop++): SnippetString { + + if (typeof value === 'function') { + const nested = new SnippetString(); + nested._tabstop = this._tabstop; + value(nested); + this._tabstop = nested._tabstop; + value = nested.value; + } else { + value = SnippetString._escape(value); + } + + this.value += '${'; + this.value += number; + this.value += ':'; + this.value += value; + this.value += '}'; + + return this; + } + + appendVariable(name: string, defaultValue?: string | ((snippet: SnippetString) => any)): SnippetString { + + if (typeof defaultValue === 'function') { + const nested = new SnippetString(); + nested._tabstop = this._tabstop; + defaultValue(nested); + this._tabstop = nested._tabstop; + defaultValue = nested.value; + + } else if (typeof defaultValue === 'string') { + defaultValue = defaultValue.replace(/\$|}/g, '\\$&'); + } + + this.value += '${'; + this.value += name; + if (defaultValue) { + this.value += ':'; + this.value += defaultValue; + } + this.value += '}'; + + + return this; + } + } + + export enum DiagnosticTag { + Unnecessary = 1, + } + + export enum DiagnosticSeverity { + Hint = 3, + Information = 2, + Warning = 1, + Error = 0 + } + + export class Location { + + static isLocation(thing: any): thing is Location { + if (thing instanceof Location) { + return true; + } + if (!thing) { + return false; + } + return Range.isRange((thing).range) + && vscUri.URI.isUri((thing).uri); + } + + uri: vscUri.URI; + range: Range; + + constructor(uri: vscUri.URI, rangeOrPosition: Range | Position) { + this.uri = uri; + + if (!rangeOrPosition) { + //that's OK + } else if (rangeOrPosition instanceof Range) { + this.range = rangeOrPosition; + } else if (rangeOrPosition instanceof Position) { + this.range = new Range(rangeOrPosition, rangeOrPosition); + } else { + throw new Error('Illegal argument'); + } + } + + toJSON(): any { + return { + uri: this.uri, + range: this.range + }; + } + } + + export class DiagnosticRelatedInformation { + + static is(thing: any): thing is DiagnosticRelatedInformation { + if (!thing) { + return false; + } + return typeof (thing).message === 'string' + && (thing).location + && Range.isRange((thing).location.range) + && vscUri.URI.isUri((thing).location.uri); + } + + location: Location; + message: string; + + constructor(location: Location, message: string) { + this.location = location; + this.message = message; + } + } + + export class Diagnostic { + + range: Range; + message: string; + source: string; + code: string | number; + severity: DiagnosticSeverity; + relatedInformation: DiagnosticRelatedInformation[]; + customTags?: DiagnosticTag[]; + + constructor(range: Range, message: string, severity: DiagnosticSeverity = DiagnosticSeverity.Error) { + this.range = range; + this.message = message; + this.severity = severity; + } + + toJSON(): any { + return { + severity: DiagnosticSeverity[this.severity], + message: this.message, + range: this.range, + source: this.source, + code: this.code, + }; + } + } + + export class Hover { + + public contents: vscode.MarkdownString[] | vscode.MarkedString[]; + public range: Range; + + constructor( + contents: vscode.MarkdownString | vscode.MarkedString | vscode.MarkdownString[] | vscode.MarkedString[], + range?: Range + ) { + if (!contents) { + throw new Error('Illegal argument, contents must be defined'); + } + if (Array.isArray(contents)) { + this.contents = contents; + } else if (vscMockHtmlContent.isMarkdownString(contents)) { + this.contents = [contents]; + } else { + this.contents = [contents]; + } + this.range = range; + } + } + + export enum DocumentHighlightKind { + Text = 0, + Read = 1, + Write = 2 + } + + export class DocumentHighlight { + + range: Range; + kind: DocumentHighlightKind; + + constructor(range: Range, kind: DocumentHighlightKind = DocumentHighlightKind.Text) { + this.range = range; + this.kind = kind; + } + + toJSON(): any { + return { + range: this.range, + kind: DocumentHighlightKind[this.kind] + }; + } + } + + export enum SymbolKind { + File = 0, + Module = 1, + Namespace = 2, + Package = 3, + Class = 4, + Method = 5, + Property = 6, + Field = 7, + Constructor = 8, + Enum = 9, + Interface = 10, + Function = 11, + Variable = 12, + Constant = 13, + String = 14, + Number = 15, + Boolean = 16, + Array = 17, + Object = 18, + Key = 19, + Null = 20, + EnumMember = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25 + } + + export class SymbolInformation { + + name: string; + location: Location; + kind: SymbolKind; + containerName: string; + + constructor(name: string, kind: SymbolKind, containerName: string, location: Location); + constructor(name: string, kind: SymbolKind, range: Range, uri?: vscUri.URI, containerName?: string); + constructor(name: string, kind: SymbolKind, rangeOrContainer: string | Range, locationOrUri?: Location | vscUri.URI, containerName?: string) { + this.name = name; + this.kind = kind; + this.containerName = containerName; + + if (typeof rangeOrContainer === 'string') { + this.containerName = rangeOrContainer; + } + + if (locationOrUri instanceof Location) { + this.location = locationOrUri; + } else if (rangeOrContainer instanceof Range) { + this.location = new Location(locationOrUri, rangeOrContainer); + } + } + + toJSON(): any { + return { + name: this.name, + kind: SymbolKind[this.kind], + location: this.location, + containerName: this.containerName + }; + } + } + + export class SymbolInformation2 extends SymbolInformation { + definingRange: Range; + children: SymbolInformation2[]; + constructor(name: string, kind: SymbolKind, containerName: string, location: Location) { + super(name, kind, containerName, location); + + this.children = []; + this.definingRange = location.range; + } + + } + + export enum CodeActionTrigger { + Automatic = 1, + Manual = 2, + } + + export class CodeAction { + title: string; + + command?: vscode.Command; + + edit?: WorkspaceEdit; + + dianostics?: Diagnostic[]; + + kind?: CodeActionKind; + + constructor(title: string, kind?: CodeActionKind) { + this.title = title; + this.kind = kind; + } + } + + + export class CodeActionKind { + private static readonly sep = '.'; + + public static readonly Empty = new CodeActionKind(''); + public static readonly QuickFix = CodeActionKind.Empty.append('quickfix'); + public static readonly Refactor = CodeActionKind.Empty.append('refactor'); + public static readonly RefactorExtract = CodeActionKind.Refactor.append('extract'); + public static readonly RefactorInline = CodeActionKind.Refactor.append('inline'); + public static readonly RefactorRewrite = CodeActionKind.Refactor.append('rewrite'); + public static readonly Source = CodeActionKind.Empty.append('source'); + public static readonly SourceOrganizeImports = CodeActionKind.Source.append('organizeImports'); + + constructor( + public readonly value: string + ) { } + + public append(parts: string): CodeActionKind { + return new CodeActionKind(this.value ? this.value + CodeActionKind.sep + parts : parts); + } + + public contains(other: CodeActionKind): boolean { + return this.value === other.value || vscMockStrings.startsWith(other.value, this.value + CodeActionKind.sep); + } + } + + + export class CodeLens { + + range: Range; + + command: vscode.Command; + + constructor(range: Range, command?: vscode.Command) { + this.range = range; + this.command = command; + } + + get isResolved(): boolean { + return !!this.command; + } + } + + export class MarkdownString { + + value: string; + isTrusted?: boolean; + + constructor(value?: string) { + this.value = value || ''; + } + + appendText(value: string): MarkdownString { + // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash + this.value += value.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); + return this; + } + + appendMarkdown(value: string): MarkdownString { + this.value += value; + return this; + } + + appendCodeblock(code: string, language: string = ''): MarkdownString { + this.value += '\n```'; + this.value += language; + this.value += '\n'; + this.value += code; + this.value += '\n```\n'; + return this; + } + } + + export class ParameterInformation { + + label: string; + documentation?: string | MarkdownString; + + constructor(label: string, documentation?: string | MarkdownString) { + this.label = label; + this.documentation = documentation; + } + } + + export class SignatureInformation { + + label: string; + documentation?: string | MarkdownString; + parameters: ParameterInformation[]; + + constructor(label: string, documentation?: string | MarkdownString) { + this.label = label; + this.documentation = documentation; + this.parameters = []; + } + } + + export class SignatureHelp { + + signatures: SignatureInformation[]; + activeSignature: number; + activeParameter: number; + + constructor() { + this.signatures = []; + } + } + + export enum CompletionTriggerKind { + Invoke = 0, + TriggerCharacter = 1, + TriggerForIncompleteCompletions = 2 + } + + export interface CompletionContext { + triggerKind: CompletionTriggerKind; + triggerCharacter: string; + } + + export enum CompletionItemKind { + Text = 0, + Method = 1, + Function = 2, + Constructor = 3, + Field = 4, + Variable = 5, + Class = 6, + Interface = 7, + Module = 8, + Property = 9, + Unit = 10, + Value = 11, + Enum = 12, + Keyword = 13, + Snippet = 14, + Color = 15, + File = 16, + Reference = 17, + Folder = 18, + EnumMember = 19, + Constant = 20, + Struct = 21, + Event = 22, + Operator = 23, + TypeParameter = 24 + } + + export class CompletionItem { + + label: string; + kind: CompletionItemKind; + detail: string; + documentation: string | MarkdownString; + sortText: string; + filterText: string; + insertText: string | SnippetString; + range: Range; + textEdit: TextEdit; + additionalTextEdits: TextEdit[]; + command: vscode.Command; + + constructor(label: string, kind?: CompletionItemKind) { + this.label = label; + this.kind = kind; + } + + toJSON(): any { + return { + label: this.label, + kind: CompletionItemKind[this.kind], + detail: this.detail, + documentation: this.documentation, + sortText: this.sortText, + filterText: this.filterText, + insertText: this.insertText, + textEdit: this.textEdit + }; + } + } + + export class CompletionList { + + isIncomplete?: boolean; + + items: vscode.CompletionItem[]; + + constructor(items: vscode.CompletionItem[] = [], isIncomplete: boolean = false) { + this.items = items; + this.isIncomplete = isIncomplete; + } + } + + export enum ViewColumn { + Active = -1, + One = 1, + Two = 2, + Three = 3 + } + + export enum StatusBarAlignment { + Left = 1, + Right = 2 + } + + export enum TextEditorLineNumbersStyle { + Off = 0, + On = 1, + Relative = 2 + } + + export enum TextDocumentSaveReason { + Manual = 1, + AfterDelay = 2, + FocusOut = 3 + } + + export enum TextEditorRevealType { + Default = 0, + InCenter = 1, + InCenterIfOutsideViewport = 2, + AtTop = 3 + } + + export enum TextEditorSelectionChangeKind { + Keyboard = 1, + Mouse = 2, + Command = 3 + } + + /** + * These values match very carefully the values of `TrackedRangeStickiness` + */ + export enum DecorationRangeBehavior { + /** + * TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges + */ + OpenOpen = 0, + /** + * TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + */ + ClosedClosed = 1, + /** + * TrackedRangeStickiness.GrowsOnlyWhenTypingBefore + */ + OpenClosed = 2, + /** + * TrackedRangeStickiness.GrowsOnlyWhenTypingAfter + */ + ClosedOpen = 3 + } + + export namespace TextEditorSelectionChangeKind { + export function fromValue(s: string) { + switch (s) { + case 'keyboard': return TextEditorSelectionChangeKind.Keyboard; + case 'mouse': return TextEditorSelectionChangeKind.Mouse; + case 'api': return TextEditorSelectionChangeKind.Command; + } + return undefined; + } + } + + export class DocumentLink { + + range: Range; + + target: vscUri.URI; + + constructor(range: Range, target: vscUri.URI) { + if (target && !(target instanceof vscUri.URI)) { + throw illegalArgument('target'); + } + if (!Range.isRange(range) || range.isEmpty) { + throw illegalArgument('range'); + } + this.range = range; + this.target = target; + } + } + + export class Color { + readonly red: number; + readonly green: number; + readonly blue: number; + readonly alpha: number; + + constructor(red: number, green: number, blue: number, alpha: number) { + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; + } + } + + export type IColorFormat = string | { opaque: string, transparent: string }; + + export class ColorInformation { + range: Range; + + color: Color; + + constructor(range: Range, color: Color) { + if (color && !(color instanceof Color)) { + throw illegalArgument('color'); + } + if (!Range.isRange(range) || range.isEmpty) { + throw illegalArgument('range'); + } + this.range = range; + this.color = color; + } + } + + export class ColorPresentation { + label: string; + textEdit?: TextEdit; + additionalTextEdits?: TextEdit[]; + + constructor(label: string) { + if (!label || typeof label !== 'string') { + throw illegalArgument('label'); + } + this.label = label; + } + } + + export enum ColorFormat { + RGB = 0, + HEX = 1, + HSL = 2 + } + + export enum SourceControlInputBoxValidationType { + Error = 0, + Warning = 1, + Information = 2 + } + + export enum TaskRevealKind { + Always = 1, + + Silent = 2, + + Never = 3 + } + + export enum TaskPanelKind { + Shared = 1, + + Dedicated = 2, + + New = 3 + } + + export class TaskGroup implements vscode.TaskGroup { + + private _id: string; + + public static Clean: TaskGroup = new TaskGroup('clean', 'Clean'); + + public static Build: TaskGroup = new TaskGroup('build', 'Build'); + + public static Rebuild: TaskGroup = new TaskGroup('rebuild', 'Rebuild'); + + public static Test: TaskGroup = new TaskGroup('test', 'Test'); + + public static from(value: string) { + switch (value) { + case 'clean': + return TaskGroup.Clean; + case 'build': + return TaskGroup.Build; + case 'rebuild': + return TaskGroup.Rebuild; + case 'test': + return TaskGroup.Test; + default: + return undefined; + } + } + + constructor(id: string, _label: string) { + if (typeof id !== 'string') { + throw illegalArgument('name'); + } + if (typeof _label !== 'string') { + throw illegalArgument('name'); + } + this._id = id; + } + + get id(): string { + return this._id; + } + } + + export class ProcessExecution implements vscode.ProcessExecution { + + private _process: string; + private _args: string[]; + private _options: vscode.ProcessExecutionOptions; + + constructor(process: string, options?: vscode.ProcessExecutionOptions); + constructor(process: string, args: string[], options?: vscode.ProcessExecutionOptions); + constructor(process: string, varg1?: string[] | vscode.ProcessExecutionOptions, varg2?: vscode.ProcessExecutionOptions) { + if (typeof process !== 'string') { + throw illegalArgument('process'); + } + this._process = process; + if (varg1 !== void 0) { + if (Array.isArray(varg1)) { + this._args = varg1; + this._options = varg2; + } else { + this._options = varg1; + } + } + if (this._args === void 0) { + this._args = []; + } + } + + + get process(): string { + return this._process; + } + + set process(value: string) { + if (typeof value !== 'string') { + throw illegalArgument('process'); + } + this._process = value; + } + + get args(): string[] { + return this._args; + } + + set args(value: string[]) { + if (!Array.isArray(value)) { + value = []; + } + this._args = value; + } + + get options(): vscode.ProcessExecutionOptions { + return this._options; + } + + set options(value: vscode.ProcessExecutionOptions) { + this._options = value; + } + + public computeId(): string { + // const hash = crypto.createHash('md5'); + // hash.update('process'); + // if (this._process !== void 0) { + // hash.update(this._process); + // } + // if (this._args && this._args.length > 0) { + // for (let arg of this._args) { + // hash.update(arg); + // } + // } + // return hash.digest('hex'); + throw new Error('Not supported'); + } + } + + export class ShellExecution implements vscode.ShellExecution { + + private _commandLine: string; + private _command: string | vscode.ShellQuotedString; + private _args: (string | vscode.ShellQuotedString)[]; + private _options: vscode.ShellExecutionOptions; + + constructor(commandLine: string, options?: vscode.ShellExecutionOptions); + constructor(command: string | vscode.ShellQuotedString, args: (string | vscode.ShellQuotedString)[], options?: vscode.ShellExecutionOptions); + constructor(arg0: string | vscode.ShellQuotedString, arg1?: vscode.ShellExecutionOptions | (string | vscode.ShellQuotedString)[], arg2?: vscode.ShellExecutionOptions) { + if (Array.isArray(arg1)) { + if (!arg0) { + throw illegalArgument('command can\'t be undefined or null'); + } + if (typeof arg0 !== 'string' && typeof arg0.value !== 'string') { + throw illegalArgument('command'); + } + this._command = arg0; + this._args = arg1 as (string | vscode.ShellQuotedString)[]; + this._options = arg2; + } else { + if (typeof arg0 !== 'string') { + throw illegalArgument('commandLine'); + } + this._commandLine = arg0; + this._options = arg1; + } + } + + get commandLine(): string { + return this._commandLine; + } + + set commandLine(value: string) { + if (typeof value !== 'string') { + throw illegalArgument('commandLine'); + } + this._commandLine = value; + } + + get command(): string | vscode.ShellQuotedString { + return this._command; + } + + set command(value: string | vscode.ShellQuotedString) { + if (typeof value !== 'string' && typeof value.value !== 'string') { + throw illegalArgument('command'); + } + this._command = value; + } + + get args(): (string | vscode.ShellQuotedString)[] { + return this._args; + } + + set args(value: (string | vscode.ShellQuotedString)[]) { + this._args = value || []; + } + + get options(): vscode.ShellExecutionOptions { + return this._options; + } + + set options(value: vscode.ShellExecutionOptions) { + this._options = value; + } + + public computeId(): string { + // const hash = crypto.createHash('md5'); + // hash.update('shell'); + // if (this._commandLine !== void 0) { + // hash.update(this._commandLine); + // } + // if (this._command !== void 0) { + // hash.update(typeof this._command === 'string' ? this._command : this._command.value); + // } + // if (this._args && this._args.length > 0) { + // for (let arg of this._args) { + // hash.update(typeof arg === 'string' ? arg : arg.value); + // } + // } + // return hash.digest('hex'); + throw new Error('Not spported'); + } + } + + export enum ShellQuoting { + Escape = 1, + Strong = 2, + Weak = 3 + } + + export enum TaskScope { + Global = 1, + Workspace = 2 + } + + export class Task implements vscode.Task { + + private __id: string; + + private _definition: vscode.TaskDefinition; + private _scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder; + private _name: string; + private _execution: ProcessExecution | ShellExecution; + private _problemMatchers: string[]; + private _hasDefinedMatchers: boolean; + private _isBackground: boolean; + private _source: string; + private _group: TaskGroup; + private _presentationOptions: vscode.TaskPresentationOptions; + + constructor(definition: vscode.TaskDefinition, name: string, source: string, execution?: ProcessExecution | ShellExecution, problemMatchers?: string | string[]); + constructor(definition: vscode.TaskDefinition, scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder, name: string, source: string, execution?: ProcessExecution | ShellExecution, problemMatchers?: string | string[]); + constructor(definition: vscode.TaskDefinition, arg2: string | (vscode.TaskScope.Global | vscode.TaskScope.Workspace) | vscode.WorkspaceFolder, arg3: any, arg4?: any, arg5?: any, arg6?: any) { + this.definition = definition; + let problemMatchers: string | string[]; + if (typeof arg2 === 'string') { + this.name = arg2; + this.source = arg3; + this.execution = arg4; + problemMatchers = arg5; + } else if (arg2 === TaskScope.Global || arg2 === TaskScope.Workspace) { + this.target = arg2; + this.name = arg3; + this.source = arg4; + this.execution = arg5; + problemMatchers = arg6; + } else { + this.target = arg2; + this.name = arg3; + this.source = arg4; + this.execution = arg5; + problemMatchers = arg6; + } + if (typeof problemMatchers === 'string') { + this._problemMatchers = [problemMatchers]; + this._hasDefinedMatchers = true; + } else if (Array.isArray(problemMatchers)) { + this._problemMatchers = problemMatchers; + this._hasDefinedMatchers = true; + } else { + this._problemMatchers = []; + this._hasDefinedMatchers = false; + } + this._isBackground = false; + } + + get _id(): string { + return this.__id; + } + + set _id(value: string) { + this.__id = value; + } + + private clear(): void { + if (this.__id === void 0) { + return; + } + this.__id = undefined; + this._scope = undefined; + this._definition = undefined; + if (this._execution instanceof ProcessExecution) { + this._definition = { + type: 'process', + id: this._execution.computeId() + }; + } else if (this._execution instanceof ShellExecution) { + this._definition = { + type: 'shell', + id: this._execution.computeId() + }; + } + } + + get definition(): vscode.TaskDefinition { + return this._definition; + } + + set definition(value: vscode.TaskDefinition) { + if (value === void 0 || value === null) { + throw illegalArgument('Kind can\'t be undefined or null'); + } + this.clear(); + this._definition = value; + } + + get scope(): vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder { + return this._scope; + } + + set target(value: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder) { + this.clear(); + this._scope = value; + } + + get name(): string { + return this._name; + } + + set name(value: string) { + if (typeof value !== 'string') { + throw illegalArgument('name'); + } + this.clear(); + this._name = value; + } + + get execution(): ProcessExecution | ShellExecution { + return this._execution; + } + + set execution(value: ProcessExecution | ShellExecution) { + if (value === null) { + value = undefined; + } + this.clear(); + this._execution = value; + } + + get problemMatchers(): string[] { + return this._problemMatchers; + } + + set problemMatchers(value: string[]) { + if (!Array.isArray(value)) { + this._problemMatchers = []; + this._hasDefinedMatchers = false; + return; + } + this.clear(); + this._problemMatchers = value; + this._hasDefinedMatchers = true; + } + + get hasDefinedMatchers(): boolean { + return this._hasDefinedMatchers; + } + + get isBackground(): boolean { + return this._isBackground; + } + + set isBackground(value: boolean) { + if (value !== true && value !== false) { + value = false; + } + this.clear(); + this._isBackground = value; + } + + get source(): string { + return this._source; + } + + set source(value: string) { + if (typeof value !== 'string' || value.length === 0) { + throw illegalArgument('source must be a string of length > 0'); + } + this.clear(); + this._source = value; + } + + get group(): TaskGroup { + return this._group; + } + + set group(value: TaskGroup) { + if (value === void 0 || value === null) { + this._group = undefined; + return; + } + this.clear(); + this._group = value; + } + + get presentationOptions(): vscode.TaskPresentationOptions { + return this._presentationOptions; + } + + set presentationOptions(value: vscode.TaskPresentationOptions) { + if (value === null) { + value = undefined; + } + this.clear(); + this._presentationOptions = value; + } + } + + + export enum ProgressLocation { + SourceControl = 1, + Window = 10, + Notification = 15 + } + + export class TreeItem { + + label?: string; + resourceUri?: vscUri.URI; + iconPath?: string | vscUri.URI | { light: string | vscUri.URI; dark: string | vscUri.URI }; + command?: vscode.Command; + contextValue?: string; + tooltip?: string; + + constructor(label: string, collapsibleState?: vscode.TreeItemCollapsibleState) + constructor(resourceUri: vscUri.URI, collapsibleState?: vscode.TreeItemCollapsibleState) + constructor(arg1: string | vscUri.URI, public collapsibleState: vscode.TreeItemCollapsibleState = TreeItemCollapsibleState.None) { + if (arg1 instanceof vscUri.URI) { + this.resourceUri = arg1; + } else { + this.label = arg1; + } + } + + } + + export enum TreeItemCollapsibleState { + None = 0, + Collapsed = 1, + Expanded = 2 + } + + export class ThemeIcon { + static readonly File = new ThemeIcon('file'); + + static readonly Folder = new ThemeIcon('folder'); + + readonly id: string; + + private constructor(id: string) { + this.id = id; + } + } + + export class ThemeColor { + id: string; + constructor(id: string) { + this.id = id; + } + } + + export enum ConfigurationTarget { + Global = 1, + + Workspace = 2, + + WorkspaceFolder = 3 + } + + export class RelativePattern implements IRelativePattern { + base: string; + pattern: string; + + constructor(base: vscode.WorkspaceFolder | string, pattern: string) { + if (typeof base !== 'string') { + if (!base || !vscUri.URI.isUri(base.uri)) { + throw illegalArgument('base'); + } + } + + if (typeof pattern !== 'string') { + throw illegalArgument('pattern'); + } + + this.base = typeof base === 'string' ? base : base.uri.fsPath; + this.pattern = pattern; + } + + public pathToRelative(from: string, to: string): string { + return relative(from, to); + } + } + + export class Breakpoint { + + readonly enabled: boolean; + readonly condition?: string; + readonly hitCondition?: string; + readonly logMessage?: string; + + protected constructor(enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) { + this.enabled = typeof enabled === 'boolean' ? enabled : true; + if (typeof condition === 'string') { + this.condition = condition; + } + if (typeof hitCondition === 'string') { + this.hitCondition = hitCondition; + } + if (typeof logMessage === 'string') { + this.logMessage = logMessage; + } + } + } + + export class SourceBreakpoint extends Breakpoint { + readonly location: Location; + + constructor(location: Location, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) { + super(enabled, condition, hitCondition, logMessage); + if (location === null) { + throw illegalArgument('location'); + } + this.location = location; + } + } + + export class FunctionBreakpoint extends Breakpoint { + readonly functionName: string; + + constructor(functionName: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) { + super(enabled, condition, hitCondition, logMessage); + if (!functionName) { + throw illegalArgument('functionName'); + } + this.functionName = functionName; + } + } + + export class DebugAdapterExecutable { + readonly command: string; + readonly args: string[]; + + constructor(command: string, args?: string[]) { + this.command = command; + this.args = args; + } + } + + export enum LogLevel { + Trace = 1, + Debug = 2, + Info = 3, + Warning = 4, + Error = 5, + Critical = 6, + Off = 7 + } + + //#region file api + + export enum FileChangeType { + Changed = 1, + Created = 2, + Deleted = 3, + } + + export class FileSystemError extends Error { + + static FileExists(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryExists', FileSystemError.FileExists); + } + static FileNotFound(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryNotFound', FileSystemError.FileNotFound); + } + static FileNotADirectory(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryNotADirectory', FileSystemError.FileNotADirectory); + } + static FileIsADirectory(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryIsADirectory', FileSystemError.FileIsADirectory); + } + static NoPermissions(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'NoPermissions', FileSystemError.NoPermissions); + } + static Unavailable(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'Unavailable', FileSystemError.Unavailable); + } + + constructor(uriOrMessage?: string | vscUri.URI, code?: string, terminator?: Function) { + super(vscUri.URI.isUri(uriOrMessage) ? uriOrMessage.toString(true) : uriOrMessage); + this.name = code ? `${code} (FileSystemError)` : `FileSystemError`; + + // workaround when extending builtin objects and when compiling to ES5, see: + // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work + if (typeof (Object).setPrototypeOf === 'function') { + (Object).setPrototypeOf(this, FileSystemError.prototype); + } + + if (typeof Error.captureStackTrace === 'function' && typeof terminator === 'function') { + // nice stack traces + Error.captureStackTrace(this, terminator); + } + } + } + + //#endregion + + //#region folding api + + export class FoldingRange { + + start: number; + + end: number; + + kind?: FoldingRangeKind; + + constructor(start: number, end: number, kind?: FoldingRangeKind) { + this.start = start; + this.end = end; + this.kind = kind; + } + } + + export enum FoldingRangeKind { + Comment = 1, + Imports = 2, + Region = 3 + } + + //#endregion + + + export enum CommentThreadCollapsibleState { + /** + * Determines an item is collapsed + */ + Collapsed = 0, + /** + * Determines an item is expanded + */ + Expanded = 1 + } +} diff --git a/src/test/mocks/vsc/htmlContent.ts b/src/test/mocks/vsc/htmlContent.ts new file mode 100644 index 000000000000..5be4dd47fc92 --- /dev/null +++ b/src/test/mocks/vsc/htmlContent.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { vscMockArrays } from './arrays'; +// tslint:disable:all + +export namespace vscMockHtmlContent { + export interface IMarkdownString { + value: string; + isTrusted?: boolean; + } + + export class MarkdownString implements IMarkdownString { + + value: string; + isTrusted?: boolean; + + constructor(value: string = '') { + this.value = value; + } + + appendText(value: string): MarkdownString { + // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash + this.value += value.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); + return this; + } + + appendMarkdown(value: string): MarkdownString { + this.value += value; + return this; + } + + appendCodeblock(langId: string, code: string): MarkdownString { + this.value += '\n```'; + this.value += langId; + this.value += '\n'; + this.value += code; + this.value += '\n```\n'; + return this; + } + } + + export function isEmptyMarkdownString(oneOrMany: IMarkdownString | IMarkdownString[]): boolean { + if (isMarkdownString(oneOrMany)) { + return !oneOrMany.value; + } else if (Array.isArray(oneOrMany)) { + return oneOrMany.every(isEmptyMarkdownString); + } else { + return true; + } + } + + export function isMarkdownString(thing: any): thing is IMarkdownString { + if (thing instanceof MarkdownString) { + return true; + } else if (thing && typeof thing === 'object') { + return typeof (thing).value === 'string' + && (typeof (thing).isTrusted === 'boolean' || (thing).isTrusted === void 0); + } + return false; + } + + export function markedStringsEquals(a: IMarkdownString | IMarkdownString[], b: IMarkdownString | IMarkdownString[]): boolean { + if (!a && !b) { + return true; + } else if (!a || !b) { + return false; + } else if (Array.isArray(a) && Array.isArray(b)) { + return vscMockArrays.equals(a, b, markdownStringEqual); + } else if (isMarkdownString(a) && isMarkdownString(b)) { + return markdownStringEqual(a, b); + } else { + return false; + } + } + + function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean { + if (a === b) { + return true; + } else if (!a || !b) { + return false; + } else { + return a.value === b.value && a.isTrusted === b.isTrusted; + } + } + + export function removeMarkdownEscapes(text: string): string { + if (!text) { + return text; + } + return text.replace(/\\([\\`*_{}[\]()#+\-.!])/g, '$1'); + } +} diff --git a/src/test/mocks/vsc/index.ts b/src/test/mocks/vsc/index.ts new file mode 100644 index 000000000000..77eaad5a0791 --- /dev/null +++ b/src/test/mocks/vsc/index.ts @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-invalid-this no-require-imports no-var-requires no-any max-classes-per-file + +import { EventEmitter as NodeEventEmitter } from 'events'; +import * as vscode from 'vscode'; +// export * from './range'; +// export * from './position'; +// export * from './selection'; +export * from './extHostedTypes'; + +export namespace vscMock { + // This is one of the very few classes that we need in our unit tests. + // It is constructed in a number of places, and this is required for verification. + // Using mocked objects for verfications does not work in typemoq. + export class Uri implements vscode.Uri { + private constructor(public readonly scheme: string, public readonly authority: string, + public readonly path: string, public readonly query: string, + public readonly fragment: string, public readonly fsPath) { + + } + public static file(path: string): Uri { + return new Uri('file', '', path, '', '', path); + } + public static parse(value: string): Uri { + return new Uri('http', '', value, '', '', value); + } + public with(change: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): vscode.Uri { + throw new Error('Not implemented'); + } + public toString(skipEncoding?: boolean): string { + return this.fsPath; + } + public toJSON(): any { + return this.fsPath; + } + } + + export class Disposable { + constructor(private callOnDispose: Function) { + } + public dispose(): any { + if (this.callOnDispose) { + this.callOnDispose(); + } + } + } + + export class EventEmitter implements vscode.EventEmitter { + + public event: vscode.Event; + public emitter: NodeEventEmitter; + constructor() { + this.event = this.add; + this.emitter = new NodeEventEmitter(); + } + public fire(data?: T): void { + this.emitter.emit('evt', data); + } + public dispose(): void { + this.emitter.removeAllListeners(); + } + + protected add(listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]): Disposable { + this.emitter.addListener('evt', listener); + return { + dispose: () => { + this.emitter.removeListener('evt', listener); + } + } as any as Disposable; + } + } + + export class CancellationToken extends EventEmitter implements vscode.CancellationToken { + public isCancellationRequested: boolean; + public onCancellationRequested: vscode.Event; + constructor() { + super(); + this.onCancellationRequested = this.add; + } + public cancel() { + this.isCancellationRequested = true; + this.fire(); + } + } + + export class CancellationTokenSource { + public token: CancellationToken; + constructor() { + this.token = new CancellationToken(); + } + public cancel(): void { + this.token.cancel(); + } + public dispose(): void { + this.token.dispose(); + } + } + + export enum CompletionItemKind { + Text = 0, + Method = 1, + Function = 2, + Constructor = 3, + Field = 4, + Variable = 5, + Class = 6, + Interface = 7, + Module = 8, + Property = 9, + Unit = 10, + Value = 11, + Enum = 12, + Keyword = 13, + Snippet = 14, + Color = 15, + Reference = 17, + File = 16, + Folder = 18, + EnumMember = 19, + Constant = 20, + Struct = 21, + Event = 22, + Operator = 23, + TypeParameter = 24 + } + export enum SymbolKind { + File = 0, + Module = 1, + Namespace = 2, + Package = 3, + Class = 4, + Method = 5, + Property = 6, + Field = 7, + Constructor = 8, + Enum = 9, + Interface = 10, + Function = 11, + Variable = 12, + Constant = 13, + String = 14, + Number = 15, + Boolean = 16, + Array = 17, + Object = 18, + Key = 19, + Null = 20, + EnumMember = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25 + } +} diff --git a/src/test/mocks/vsc/position.ts b/src/test/mocks/vsc/position.ts new file mode 100644 index 000000000000..f901c5f7d9ce --- /dev/null +++ b/src/test/mocks/vsc/position.ts @@ -0,0 +1,157 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ +'use strict'; + +// tslint:disable:all +export namespace vscMockPosition { + /** + * A position in the editor. This interface is suitable for serialization. + */ + export interface IPosition { + /** + * line number (starts at 1) + */ + readonly lineNumber: number; + /** + * column (the first character in a line is between column 1 and column 2) + */ + readonly column: number; + } + + /** + * A position in the editor. + */ + export class Position { + /** + * line number (starts at 1) + */ + public readonly lineNumber: number; + /** + * column (the first character in a line is between column 1 and column 2) + */ + public readonly column: number; + + constructor(lineNumber: number, column: number) { + this.lineNumber = lineNumber; + this.column = column; + } + + /** + * Test if this position equals other position + */ + public equals(other: IPosition): boolean { + return Position.equals(this, other); + } + + /** + * Test if position `a` equals position `b` + */ + public static equals(a: IPosition, b: IPosition): boolean { + if (!a && !b) { + return true; + } + return ( + !!a && + !!b && + a.lineNumber === b.lineNumber && + a.column === b.column + ); + } + + /** + * Test if this position is before other position. + * If the two positions are equal, the result will be false. + */ + public isBefore(other: IPosition): boolean { + return Position.isBefore(this, other); + } + + /** + * Test if position `a` is before position `b`. + * If the two positions are equal, the result will be false. + */ + public static isBefore(a: IPosition, b: IPosition): boolean { + if (a.lineNumber < b.lineNumber) { + return true; + } + if (b.lineNumber < a.lineNumber) { + return false; + } + return a.column < b.column; + } + + /** + * Test if this position is before other position. + * If the two positions are equal, the result will be true. + */ + public isBeforeOrEqual(other: IPosition): boolean { + return Position.isBeforeOrEqual(this, other); + } + + /** + * Test if position `a` is before position `b`. + * If the two positions are equal, the result will be true. + */ + public static isBeforeOrEqual(a: IPosition, b: IPosition): boolean { + if (a.lineNumber < b.lineNumber) { + return true; + } + if (b.lineNumber < a.lineNumber) { + return false; + } + return a.column <= b.column; + } + + /** + * A function that compares positions, useful for sorting + */ + public static compare(a: IPosition, b: IPosition): number { + let aLineNumber = a.lineNumber | 0; + let bLineNumber = b.lineNumber | 0; + + if (aLineNumber === bLineNumber) { + let aColumn = a.column | 0; + let bColumn = b.column | 0; + return aColumn - bColumn; + } + + return aLineNumber - bLineNumber; + } + + /** + * Clone this position. + */ + public clone(): Position { + return new Position(this.lineNumber, this.column); + } + + /** + * Convert to a human-readable representation. + */ + public toString(): string { + return '(' + this.lineNumber + ',' + this.column + ')'; + } + + // --- + + /** + * Create a `Position` from an `IPosition`. + */ + public static lift(pos: IPosition): Position { + return new Position(pos.lineNumber, pos.column); + } + + /** + * Test if `obj` is an `IPosition`. + */ + public static isIPosition(obj: any): obj is IPosition { + return ( + obj + && (typeof obj.lineNumber === 'number') + && (typeof obj.column === 'number') + ); + } + } +} diff --git a/src/test/mocks/vsc/range.ts b/src/test/mocks/vsc/range.ts new file mode 100644 index 000000000000..16d40ff0bb12 --- /dev/null +++ b/src/test/mocks/vsc/range.ts @@ -0,0 +1,384 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; +// tslint:disable:all +import { vscMockPosition } from './position'; + +export namespace vscMockRange { + /** + * A range in the editor. This interface is suitable for serialization. + */ + export interface IRange { + /** + * Line number on which the range starts (starts at 1). + */ + readonly startLineNumber: number; + /** + * Column on which the range starts in line `startLineNumber` (starts at 1). + */ + readonly startColumn: number; + /** + * Line number on which the range ends. + */ + readonly endLineNumber: number; + /** + * Column on which the range ends in line `endLineNumber`. + */ + readonly endColumn: number; + } + + /** + * A range in the editor. (startLineNumber,startColumn) is <= (endLineNumber,endColumn) + */ + export class Range { + + /** + * Line number on which the range starts (starts at 1). + */ + public readonly startLineNumber: number; + /** + * Column on which the range starts in line `startLineNumber` (starts at 1). + */ + public readonly startColumn: number; + /** + * Line number on which the range ends. + */ + public readonly endLineNumber: number; + /** + * Column on which the range ends in line `endLineNumber`. + */ + public readonly endColumn: number; + + constructor(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number) { + if ((startLineNumber > endLineNumber) || (startLineNumber === endLineNumber && startColumn > endColumn)) { + this.startLineNumber = endLineNumber; + this.startColumn = endColumn; + this.endLineNumber = startLineNumber; + this.endColumn = startColumn; + } else { + this.startLineNumber = startLineNumber; + this.startColumn = startColumn; + this.endLineNumber = endLineNumber; + this.endColumn = endColumn; + } + } + + /** + * Test if this range is empty. + */ + public isEmpty(): boolean { + return Range.isEmpty(this); + } + + /** + * Test if `range` is empty. + */ + public static isEmpty(range: IRange): boolean { + return (range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn); + } + + /** + * Test if position is in this range. If the position is at the edges, will return true. + */ + public containsPosition(position: vscMockPosition.IPosition): boolean { + return Range.containsPosition(this, position); + } + + /** + * Test if `position` is in `range`. If the position is at the edges, will return true. + */ + public static containsPosition(range: IRange, position: vscMockPosition.IPosition): boolean { + if (position.lineNumber < range.startLineNumber || position.lineNumber > range.endLineNumber) { + return false; + } + if (position.lineNumber === range.startLineNumber && position.column < range.startColumn) { + return false; + } + if (position.lineNumber === range.endLineNumber && position.column > range.endColumn) { + return false; + } + return true; + } + + /** + * Test if range is in this range. If the range is equal to this range, will return true. + */ + public containsRange(range: IRange): boolean { + return Range.containsRange(this, range); + } + + /** + * Test if `otherRange` is in `range`. If the ranges are equal, will return true. + */ + public static containsRange(range: IRange, otherRange: IRange): boolean { + if (otherRange.startLineNumber < range.startLineNumber || otherRange.endLineNumber < range.startLineNumber) { + return false; + } + if (otherRange.startLineNumber > range.endLineNumber || otherRange.endLineNumber > range.endLineNumber) { + return false; + } + if (otherRange.startLineNumber === range.startLineNumber && otherRange.startColumn < range.startColumn) { + return false; + } + if (otherRange.endLineNumber === range.endLineNumber && otherRange.endColumn > range.endColumn) { + return false; + } + return true; + } + + /** + * A reunion of the two ranges. + * The smallest position will be used as the start point, and the largest one as the end point. + */ + public plusRange(range: IRange): Range { + return Range.plusRange(this, range); + } + + /** + * A reunion of the two ranges. + * The smallest position will be used as the start point, and the largest one as the end point. + */ + public static plusRange(a: IRange, b: IRange): Range { + var startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number; + if (b.startLineNumber < a.startLineNumber) { + startLineNumber = b.startLineNumber; + startColumn = b.startColumn; + } else if (b.startLineNumber === a.startLineNumber) { + startLineNumber = b.startLineNumber; + startColumn = Math.min(b.startColumn, a.startColumn); + } else { + startLineNumber = a.startLineNumber; + startColumn = a.startColumn; + } + + if (b.endLineNumber > a.endLineNumber) { + endLineNumber = b.endLineNumber; + endColumn = b.endColumn; + } else if (b.endLineNumber === a.endLineNumber) { + endLineNumber = b.endLineNumber; + endColumn = Math.max(b.endColumn, a.endColumn); + } else { + endLineNumber = a.endLineNumber; + endColumn = a.endColumn; + } + + return new Range(startLineNumber, startColumn, endLineNumber, endColumn); + } + + /** + * A intersection of the two ranges. + */ + public intersectRanges(range: IRange): Range { + return Range.intersectRanges(this, range); + } + + /** + * A intersection of the two ranges. + */ + public static intersectRanges(a: IRange, b: IRange): Range { + var resultStartLineNumber = a.startLineNumber, + resultStartColumn = a.startColumn, + resultEndLineNumber = a.endLineNumber, + resultEndColumn = a.endColumn, + otherStartLineNumber = b.startLineNumber, + otherStartColumn = b.startColumn, + otherEndLineNumber = b.endLineNumber, + otherEndColumn = b.endColumn; + + if (resultStartLineNumber < otherStartLineNumber) { + resultStartLineNumber = otherStartLineNumber; + resultStartColumn = otherStartColumn; + } else if (resultStartLineNumber === otherStartLineNumber) { + resultStartColumn = Math.max(resultStartColumn, otherStartColumn); + } + + if (resultEndLineNumber > otherEndLineNumber) { + resultEndLineNumber = otherEndLineNumber; + resultEndColumn = otherEndColumn; + } else if (resultEndLineNumber === otherEndLineNumber) { + resultEndColumn = Math.min(resultEndColumn, otherEndColumn); + } + + // Check if selection is now empty + if (resultStartLineNumber > resultEndLineNumber) { + return null; + } + if (resultStartLineNumber === resultEndLineNumber && resultStartColumn > resultEndColumn) { + return null; + } + return new Range(resultStartLineNumber, resultStartColumn, resultEndLineNumber, resultEndColumn); + } + + /** + * Test if this range equals other. + */ + public equalsRange(other: IRange): boolean { + return Range.equalsRange(this, other); + } + + /** + * Test if range `a` equals `b`. + */ + public static equalsRange(a: IRange, b: IRange): boolean { + return ( + !!a && + !!b && + a.startLineNumber === b.startLineNumber && + a.startColumn === b.startColumn && + a.endLineNumber === b.endLineNumber && + a.endColumn === b.endColumn + ); + } + + /** + * Return the end position (which will be after or equal to the start position) + */ + public getEndPosition(): vscMockPosition.Position { + return new vscMockPosition.Position(this.endLineNumber, this.endColumn); + } + + /** + * Return the start position (which will be before or equal to the end position) + */ + public getStartPosition(): vscMockPosition.Position { + return new vscMockPosition.Position(this.startLineNumber, this.startColumn); + } + + /** + * Transform to a user presentable string representation. + */ + public toString(): string { + return '[' + this.startLineNumber + ',' + this.startColumn + ' -> ' + this.endLineNumber + ',' + this.endColumn + ']'; + } + + /** + * Create a new range using this range's start position, and using endLineNumber and endColumn as the end position. + */ + public setEndPosition(endLineNumber: number, endColumn: number): Range { + return new Range(this.startLineNumber, this.startColumn, endLineNumber, endColumn); + } + + /** + * Create a new range using this range's end position, and using startLineNumber and startColumn as the start position. + */ + public setStartPosition(startLineNumber: number, startColumn: number): Range { + return new Range(startLineNumber, startColumn, this.endLineNumber, this.endColumn); + } + + /** + * Create a new empty range using this range's start position. + */ + public collapseToStart(): Range { + return Range.collapseToStart(this); + } + + /** + * Create a new empty range using this range's start position. + */ + public static collapseToStart(range: IRange): Range { + return new Range(range.startLineNumber, range.startColumn, range.startLineNumber, range.startColumn); + } + + // --- + + public static fromPositions(start: vscMockPosition.IPosition, end: vscMockPosition.IPosition = start): Range { + return new Range(start.lineNumber, start.column, end.lineNumber, end.column); + } + + /** + * Create a `Range` from an `IRange`. + */ + public static lift(range: IRange): Range { + if (!range) { + return null; + } + return new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); + } + + /** + * Test if `obj` is an `IRange`. + */ + public static isIRange(obj: any): obj is IRange { + return ( + obj + && (typeof obj.startLineNumber === 'number') + && (typeof obj.startColumn === 'number') + && (typeof obj.endLineNumber === 'number') + && (typeof obj.endColumn === 'number') + ); + } + + /** + * Test if the two ranges are touching in any way. + */ + public static areIntersectingOrTouching(a: IRange, b: IRange): boolean { + // Check if `a` is before `b` + if (a.endLineNumber < b.startLineNumber || (a.endLineNumber === b.startLineNumber && a.endColumn < b.startColumn)) { + return false; + } + + // Check if `b` is before `a` + if (b.endLineNumber < a.startLineNumber || (b.endLineNumber === a.startLineNumber && b.endColumn < a.startColumn)) { + return false; + } + + // These ranges must intersect + return true; + } + + /** + * A function that compares ranges, useful for sorting ranges + * It will first compare ranges on the startPosition and then on the endPosition + */ + public static compareRangesUsingStarts(a: IRange, b: IRange): number { + let aStartLineNumber = a.startLineNumber | 0; + let bStartLineNumber = b.startLineNumber | 0; + + if (aStartLineNumber === bStartLineNumber) { + let aStartColumn = a.startColumn | 0; + let bStartColumn = b.startColumn | 0; + + if (aStartColumn === bStartColumn) { + let aEndLineNumber = a.endLineNumber | 0; + let bEndLineNumber = b.endLineNumber | 0; + + if (aEndLineNumber === bEndLineNumber) { + let aEndColumn = a.endColumn | 0; + let bEndColumn = b.endColumn | 0; + return aEndColumn - bEndColumn; + } + return aEndLineNumber - bEndLineNumber; + } + return aStartColumn - bStartColumn; + } + return aStartLineNumber - bStartLineNumber; + } + + /** + * A function that compares ranges, useful for sorting ranges + * It will first compare ranges on the endPosition and then on the startPosition + */ + public static compareRangesUsingEnds(a: IRange, b: IRange): number { + if (a.endLineNumber === b.endLineNumber) { + if (a.endColumn === b.endColumn) { + if (a.startLineNumber === b.startLineNumber) { + return a.startColumn - b.startColumn; + } + return a.startLineNumber - b.startLineNumber; + } + return a.endColumn - b.endColumn; + } + return a.endLineNumber - b.endLineNumber; + } + + /** + * Test if the range spans multiple lines. + */ + public static spansMultipleLines(range: IRange): boolean { + return range.endLineNumber > range.startLineNumber; + } + } +} diff --git a/src/test/mocks/vsc/selection.ts b/src/test/mocks/vsc/selection.ts new file mode 100644 index 000000000000..5c750fed8847 --- /dev/null +++ b/src/test/mocks/vsc/selection.ts @@ -0,0 +1,211 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +// tslint:disable:all +import { vscMockPosition } from './position'; +import { vscMockRange } from './range'; +export namespace vscMockSelection { + /** + * A selection in the editor. + * The selection is a range that has an orientation. + */ + export interface ISelection { + /** + * The line number on which the selection has started. + */ + readonly selectionStartLineNumber: number; + /** + * The column on `selectionStartLineNumber` where the selection has started. + */ + readonly selectionStartColumn: number; + /** + * The line number on which the selection has ended. + */ + readonly positionLineNumber: number; + /** + * The column on `positionLineNumber` where the selection has ended. + */ + readonly positionColumn: number; + } + + /** + * The direction of a selection. + */ + export enum SelectionDirection { + /** + * The selection starts above where it ends. + */ + LTR, + /** + * The selection starts below where it ends. + */ + RTL + } + + /** + * A selection in the editor. + * The selection is a range that has an orientation. + */ + export class Selection extends vscMockRange.Range { + /** + * The line number on which the selection has started. + */ + public readonly selectionStartLineNumber: number; + /** + * The column on `selectionStartLineNumber` where the selection has started. + */ + public readonly selectionStartColumn: number; + /** + * The line number on which the selection has ended. + */ + public readonly positionLineNumber: number; + /** + * The column on `positionLineNumber` where the selection has ended. + */ + public readonly positionColumn: number; + + constructor(selectionStartLineNumber: number, selectionStartColumn: number, positionLineNumber: number, positionColumn: number) { + super(selectionStartLineNumber, selectionStartColumn, positionLineNumber, positionColumn); + this.selectionStartLineNumber = selectionStartLineNumber; + this.selectionStartColumn = selectionStartColumn; + this.positionLineNumber = positionLineNumber; + this.positionColumn = positionColumn; + } + + /** + * Clone this selection. + */ + public clone(): Selection { + return new Selection(this.selectionStartLineNumber, this.selectionStartColumn, this.positionLineNumber, this.positionColumn); + } + + /** + * Transform to a human-readable representation. + */ + public toString(): string { + return '[' + this.selectionStartLineNumber + ',' + this.selectionStartColumn + ' -> ' + this.positionLineNumber + ',' + this.positionColumn + ']'; + } + + /** + * Test if equals other selection. + */ + public equalsSelection(other: ISelection): boolean { + return ( + Selection.selectionsEqual(this, other) + ); + } + + /** + * Test if the two selections are equal. + */ + public static selectionsEqual(a: ISelection, b: ISelection): boolean { + return ( + a.selectionStartLineNumber === b.selectionStartLineNumber && + a.selectionStartColumn === b.selectionStartColumn && + a.positionLineNumber === b.positionLineNumber && + a.positionColumn === b.positionColumn + ); + } + + /** + * Get directions (LTR or RTL). + */ + public getDirection(): SelectionDirection { + if (this.selectionStartLineNumber === this.startLineNumber && this.selectionStartColumn === this.startColumn) { + return SelectionDirection.LTR; + } + return SelectionDirection.RTL; + } + + /** + * Create a new selection with a different `positionLineNumber` and `positionColumn`. + */ + public setEndPosition(endLineNumber: number, endColumn: number): Selection { + if (this.getDirection() === SelectionDirection.LTR) { + return new Selection(this.startLineNumber, this.startColumn, endLineNumber, endColumn); + } + return new Selection(endLineNumber, endColumn, this.startLineNumber, this.startColumn); + } + + /** + * Get the position at `positionLineNumber` and `positionColumn`. + */ + public getPosition(): vscMockPosition.Position { + return new vscMockPosition.Position(this.positionLineNumber, this.positionColumn); + } + + /** + * Create a new selection with a different `selectionStartLineNumber` and `selectionStartColumn`. + */ + public setStartPosition(startLineNumber: number, startColumn: number): Selection { + if (this.getDirection() === SelectionDirection.LTR) { + return new Selection(startLineNumber, startColumn, this.endLineNumber, this.endColumn); + } + return new Selection(this.endLineNumber, this.endColumn, startLineNumber, startColumn); + } + + // ---- + + /** + * Create a `Selection` from one or two positions + */ + public static fromPositions(start: vscMockPosition.IPosition, end: vscMockPosition.IPosition = start): Selection { + return new Selection(start.lineNumber, start.column, end.lineNumber, end.column); + } + + /** + * Create a `Selection` from an `ISelection`. + */ + public static liftSelection(sel: ISelection): Selection { + return new Selection(sel.selectionStartLineNumber, sel.selectionStartColumn, sel.positionLineNumber, sel.positionColumn); + } + + /** + * `a` equals `b`. + */ + public static selectionsArrEqual(a: ISelection[], b: ISelection[]): boolean { + if (a && !b || !a && b) { + return false; + } + if (!a && !b) { + return true; + } + if (a.length !== b.length) { + return false; + } + for (var i = 0, len = a.length; i < len; i++) { + if (!this.selectionsEqual(a[i], b[i])) { + return false; + } + } + return true; + } + + /** + * Test if `obj` is an `ISelection`. + */ + public static isISelection(obj: any): obj is ISelection { + return ( + obj + && (typeof obj.selectionStartLineNumber === 'number') + && (typeof obj.selectionStartColumn === 'number') + && (typeof obj.positionLineNumber === 'number') + && (typeof obj.positionColumn === 'number') + ); + } + + /** + * Create with a direction. + */ + public static createWithDirection(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, direction: SelectionDirection): Selection { + + if (direction === SelectionDirection.LTR) { + return new Selection(startLineNumber, startColumn, endLineNumber, endColumn); + } + + return new Selection(endLineNumber, endColumn, startLineNumber, startColumn); + } + } +} diff --git a/src/test/mocks/vsc/strings.ts b/src/test/mocks/vsc/strings.ts new file mode 100644 index 000000000000..a1feac2d2668 --- /dev/null +++ b/src/test/mocks/vsc/strings.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +// tslint:disable:all + +export namespace vscMockStrings { + /** + * Determines if haystack starts with needle. + */ + export function startsWith(haystack: string, needle: string): boolean { + if (haystack.length < needle.length) { + return false; + } + + for (let i = 0; i < needle.length; i++) { + if (haystack[i] !== needle[i]) { + return false; + } + } + + return true; + } + + /** + * Determines if haystack ends with needle. + */ + export function endsWith(haystack: string, needle: string): boolean { + let diff = haystack.length - needle.length; + if (diff > 0) { + return haystack.indexOf(needle, diff) === diff; + } else if (diff === 0) { + return haystack === needle; + } else { + return false; + } + } +} diff --git a/src/test/mocks/vsc/telemetryReporter.ts b/src/test/mocks/vsc/telemetryReporter.ts new file mode 100644 index 000000000000..9b5ef94178cf --- /dev/null +++ b/src/test/mocks/vsc/telemetryReporter.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:all +import * as telemetry from 'vscode-extension-telemetry'; +export class vscMockTelemetryReporter implements telemetry.default { + constructor() { + // + } + + public sendTelemetryEvent(): void { + // + } +} diff --git a/src/test/mocks/vsc/uri.ts b/src/test/mocks/vsc/uri.ts new file mode 100644 index 000000000000..e91531924823 --- /dev/null +++ b/src/test/mocks/vsc/uri.ts @@ -0,0 +1,474 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +export namespace vscUri { + const platform = { + isWindows: /^win/.test(process.platform) + }; + + // tslint:disable:all + + function _encode(ch: string): string { + return '%' + ch.charCodeAt(0).toString(16).toUpperCase(); + } + + // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent + function encodeURIComponent2(str: string): string { + return encodeURIComponent(str).replace(/[!'()*]/g, _encode); + } + + function encodeNoop(str: string): string { + return str.replace(/[#?]/, _encode); + } + + + const _schemePattern = /^\w[\w\d+.-]*$/; + const _singleSlashStart = /^\//; + const _doubleSlashStart = /^\/\//; + + function _validateUri(ret: URI): void { + // scheme, https://tools.ietf.org/html/rfc3986#section-3.1 + // ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) + if (ret.scheme && !_schemePattern.test(ret.scheme)) { + throw new Error('[UriError]: Scheme contains illegal characters.'); + } + + // path, http://tools.ietf.org/html/rfc3986#section-3.3 + // If a URI contains an authority component, then the path component + // must either be empty or begin with a slash ("/") character. If a URI + // does not contain an authority component, then the path cannot begin + // with two slash characters ("//"). + if (ret.path) { + if (ret.authority) { + if (!_singleSlashStart.test(ret.path)) { + throw new Error('[UriError]: If a URI contains an authority component, then the path component must either be empty or begin with a slash ("/") character'); + } + } else { + if (_doubleSlashStart.test(ret.path)) { + throw new Error('[UriError]: If a URI does not contain an authority component, then the path cannot begin with two slash characters ("//")'); + } + } + } + } + + const _empty = ''; + const _slash = '/'; + const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; + const _driveLetterPath = /^\/[a-zA-Z]:/; + const _upperCaseDrive = /^(\/)?([A-Z]:)/; + const _driveLetter = /^[a-zA-Z]:/; + + /** + * Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986. + * This class is a simple parser which creates the basic component paths + * (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation + * and encoding. + * + * foo://example.com:8042/over/there?name=ferret#nose + * \_/ \______________/\_________/ \_________/ \__/ + * | | | | | + * scheme authority path query fragment + * | _____________________|__ + * / \ / \ + * urn:example:animal:ferret:nose + * + * + */ + export class URI implements UriComponents { + + static isUri(thing: any): thing is URI { + if (thing instanceof URI) { + return true; + } + if (!thing) { + return false; + } + return typeof (thing).authority === 'string' + && typeof (thing).fragment === 'string' + && typeof (thing).path === 'string' + && typeof (thing).query === 'string' + && typeof (thing).scheme === 'string'; + } + + /** + * scheme is the 'http' part of 'http://www.msft.com/some/path?query#fragment'. + * The part before the first colon. + */ + readonly scheme: string; + + /** + * authority is the 'www.msft.com' part of 'http://www.msft.com/some/path?query#fragment'. + * The part between the first double slashes and the next slash. + */ + readonly authority: string; + + /** + * path is the '/some/path' part of 'http://www.msft.com/some/path?query#fragment'. + */ + readonly path: string; + + /** + * query is the 'query' part of 'http://www.msft.com/some/path?query#fragment'. + */ + readonly query: string; + + /** + * fragment is the 'fragment' part of 'http://www.msft.com/some/path?query#fragment'. + */ + readonly fragment: string; + + /** + * @internal + */ + protected constructor(scheme: string, authority: string, path: string, query: string, fragment: string); + + /** + * @internal + */ + protected constructor(components: UriComponents); + + /** + * @internal + */ + protected constructor(schemeOrData: string | UriComponents, authority?: string, path?: string, query?: string, fragment?: string) { + + if (typeof schemeOrData === 'object') { + this.scheme = schemeOrData.scheme || _empty; + this.authority = schemeOrData.authority || _empty; + this.path = schemeOrData.path || _empty; + this.query = schemeOrData.query || _empty; + this.fragment = schemeOrData.fragment || _empty; + // no validation because it's this URI + // that creates uri components. + // _validateUri(this); + } else { + this.scheme = schemeOrData || _empty; + this.authority = authority || _empty; + this.path = path || _empty; + this.query = query || _empty; + this.fragment = fragment || _empty; + _validateUri(this); + } + } + + // ---- filesystem path ----------------------- + + /** + * Returns a string representing the corresponding file system path of this URI. + * Will handle UNC paths and normalize windows drive letters to lower-case. Also + * uses the platform specific path separator. Will *not* validate the path for + * invalid characters and semantics. Will *not* look at the scheme of this URI. + */ + get fsPath(): string { + return _makeFsPath(this); + } + + // ---- modify to new ------------------------- + + public with(change: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): URI { + + if (!change) { + return this; + } + + let { scheme, authority, path, query, fragment } = change; + if (scheme === void 0) { + scheme = this.scheme; + } else if (scheme === null) { + scheme = _empty; + } + if (authority === void 0) { + authority = this.authority; + } else if (authority === null) { + authority = _empty; + } + if (path === void 0) { + path = this.path; + } else if (path === null) { + path = _empty; + } + if (query === void 0) { + query = this.query; + } else if (query === null) { + query = _empty; + } + if (fragment === void 0) { + fragment = this.fragment; + } else if (fragment === null) { + fragment = _empty; + } + + if (scheme === this.scheme + && authority === this.authority + && path === this.path + && query === this.query + && fragment === this.fragment) { + + return this; + } + + return new _URI(scheme, authority, path, query, fragment); + } + + // ---- parse & validate ------------------------ + + public static parse(value: string): URI { + const match = _regexp.exec(value); + if (!match) { + return new _URI(_empty, _empty, _empty, _empty, _empty); + } + return new _URI( + match[2] || _empty, + decodeURIComponent(match[4] || _empty), + decodeURIComponent(match[5] || _empty), + decodeURIComponent(match[7] || _empty), + decodeURIComponent(match[9] || _empty), + ); + } + + public static file(path: string): URI { + + let authority = _empty; + + // normalize to fwd-slashes on windows, + // on other systems bwd-slashes are valid + // filename character, eg /f\oo/ba\r.txt + if (platform.isWindows) { + path = path.replace(/\\/g, _slash); + } + + // check for authority as used in UNC shares + // or use the path as given + if (path[0] === _slash && path[1] === _slash) { + let idx = path.indexOf(_slash, 2); + if (idx === -1) { + authority = path.substring(2); + path = _slash; + } else { + authority = path.substring(2, idx); + path = path.substring(idx) || _slash; + } + } + + // Ensure that path starts with a slash + // or that it is at least a slash + if (_driveLetter.test(path)) { + path = _slash + path; + + } else if (path[0] !== _slash) { + // tricky -> makes invalid paths + // but otherwise we have to stop + // allowing relative paths... + path = _slash + path; + } + + return new _URI('file', authority, path, _empty, _empty); + } + + public static from(components: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): URI { + return new _URI( + components.scheme, + components.authority, + components.path, + components.query, + components.fragment, + ); + } + + // ---- printing/externalize --------------------------- + + /** + * + * @param skipEncoding Do not encode the result, default is `false` + */ + public toString(skipEncoding: boolean = false): string { + return _asFormatted(this, skipEncoding); + } + + public toJSON(): object { + const res = { + $mid: 1, + fsPath: this.fsPath, + external: this.toString(), + }; + + if (this.path) { + res.path = this.path; + } + + if (this.scheme) { + res.scheme = this.scheme; + } + + if (this.authority) { + res.authority = this.authority; + } + + if (this.query) { + res.query = this.query; + } + + if (this.fragment) { + res.fragment = this.fragment; + } + + return res; + } + + static revive(data: UriComponents | any): URI { + if (!data) { + return data; + } else if (data instanceof URI) { + return data; + } else { + let result = new _URI(data); + result._fsPath = (data).fsPath; + result._formatted = (data).external; + return result; + } + } + } + + export interface UriComponents { + scheme: string; + authority: string; + path: string; + query: string; + fragment: string; + } + + interface UriState extends UriComponents { + $mid: number; + fsPath: string; + external: string; + } + + + // tslint:disable-next-line:class-name + class _URI extends URI { + + _formatted: string = null; + _fsPath: string = null; + + get fsPath(): string { + if (!this._fsPath) { + this._fsPath = _makeFsPath(this); + } + return this._fsPath; + } + + public toString(skipEncoding: boolean = false): string { + if (!skipEncoding) { + if (!this._formatted) { + this._formatted = _asFormatted(this, false); + } + return this._formatted; + } else { + // we don't cache that + return _asFormatted(this, true); + } + } + } + + + /** + * Compute `fsPath` for the given uri + * @param uri + */ + function _makeFsPath(uri: URI): string { + + let value: string; + if (uri.authority && uri.path && uri.scheme === 'file') { + // unc path: file://shares/c$/far/boo + value = `//${uri.authority}${uri.path}`; + } else if (_driveLetterPath.test(uri.path)) { + // windows drive letter: file:///c:/far/boo + value = uri.path[1].toLowerCase() + uri.path.substr(2); + } else { + // other path + value = uri.path; + } + if (platform.isWindows) { + value = value.replace(/\//g, '\\'); + } + return value; + } + + /** + * Create the external version of a uri + */ + function _asFormatted(uri: URI, skipEncoding: boolean): string { + + const encoder = !skipEncoding + ? encodeURIComponent2 + : encodeNoop; + + const parts: string[] = []; + + let { scheme, authority, path, query, fragment } = uri; + if (scheme) { + parts.push(scheme, ':'); + } + if (authority || scheme === 'file') { + parts.push('//'); + } + if (authority) { + let idx = authority.indexOf('@'); + if (idx !== -1) { + const userinfo = authority.substr(0, idx); + authority = authority.substr(idx + 1); + idx = userinfo.indexOf(':'); + if (idx === -1) { + parts.push(encoder(userinfo)); + } else { + parts.push(encoder(userinfo.substr(0, idx)), ':', encoder(userinfo.substr(idx + 1))); + } + parts.push('@'); + } + authority = authority.toLowerCase(); + idx = authority.indexOf(':'); + if (idx === -1) { + parts.push(encoder(authority)); + } else { + parts.push(encoder(authority.substr(0, idx)), authority.substr(idx)); + } + } + if (path) { + // lower-case windows drive letters in /C:/fff or C:/fff + const m = _upperCaseDrive.exec(path); + if (m) { + if (m[1]) { + path = '/' + m[2].toLowerCase() + path.substr(3); // "/c:".length === 3 + } else { + path = m[2].toLowerCase() + path.substr(2); // // "c:".length === 2 + } + } + + // encode every segement but not slashes + // make sure that # and ? are always encoded + // when occurring in paths - otherwise the result + // cannot be parsed back again + let lastIdx = 0; + while (true) { + let idx = path.indexOf(_slash, lastIdx); + if (idx === -1) { + parts.push(encoder(path.substring(lastIdx))); + break; + } + parts.push(encoder(path.substring(lastIdx, idx)), _slash); + lastIdx = idx + 1; + } + } + if (query) { + parts.push('?', encoder(query)); + } + if (fragment) { + parts.push('#', encoder(fragment)); + } + + return parts.join(_empty); + } +} diff --git a/src/test/performance/load.perf.test.ts b/src/test/performance/load.perf.test.ts new file mode 100644 index 000000000000..f3b4f87ffa1c --- /dev/null +++ b/src/test/performance/load.perf.test.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-invalid-this no-console + +import { expect } from 'chai'; +import * as fs from 'fs-extra'; +import { EOL } from 'os'; +import * as path from 'path'; +import { commands, extensions } from 'vscode'; +import { StopWatch } from '../../client/common/stopWatch'; + +const AllowedIncreaseInActivationDelayInMS = 500; + +suite('Activation Times', () => { + if (process.env.ACTIVATION_TIMES_LOG_FILE_PATH) { + const logFile = process.env.ACTIVATION_TIMES_LOG_FILE_PATH; + const sampleCounter = fs.existsSync(logFile) ? fs.readFileSync(logFile, { encoding: 'utf8' }).toString().split(/\r?\n/g).length : 1; + if (sampleCounter > 5) { + return; + } + test(`Capture Extension Activation Times (Version: ${process.env.ACTIVATION_TIMES_EXT_VERSION}, sample: ${sampleCounter})`, async () => { + const pythonExtension = extensions.getExtension('ms-python.python'); + if (!pythonExtension) { + throw new Error('Python Extension not found'); + } + const stopWatch = new StopWatch(); + await pythonExtension!.activate(); + const elapsedTime = stopWatch.elapsedTime; + if (elapsedTime > 10) { + await fs.ensureDir(path.dirname(logFile)); + await fs.appendFile(logFile, `${elapsedTime}${EOL}`, { encoding: 'utf8' }); + console.log(`Loaded in ${elapsedTime}ms`); + } + commands.executeCommand('workbench.action.reloadWindow'); + }); + } + + if (process.env.ACTIVATION_TIMES_DEV_LOG_FILE_PATHS && + process.env.ACTIVATION_TIMES_RELEASE_LOG_FILE_PATHS && + process.env.ACTIVATION_TIMES_DEV_LANGUAGE_SERVER_LOG_FILE_PATHS) { + + test('Test activation times of Dev vs Release Extension', async () => { + function getActivationTimes(files: string[]) { + const activationTimes: number[] = []; + for (const file of files) { + fs.readFileSync(file, { encoding: 'utf8' }).toString() + .split(/\r?\n/g) + .map(line => line.trim()) + .filter(line => line.length > 0) + .map(line => parseInt(line, 10)) + .forEach(item => activationTimes.push(item)); + } + return activationTimes; + } + const devActivationTimes = getActivationTimes(JSON.parse(process.env.ACTIVATION_TIMES_DEV_LOG_FILE_PATHS!)); + const releaseActivationTimes = getActivationTimes(JSON.parse(process.env.ACTIVATION_TIMES_RELEASE_LOG_FILE_PATHS!)); + const languageServerActivationTimes = getActivationTimes(JSON.parse(process.env.ACTIVATION_TIMES_DEV_LANGUAGE_SERVER_LOG_FILE_PATHS!)); + const devActivationAvgTime = devActivationTimes.reduce((sum, item) => sum + item, 0) / devActivationTimes.length; + const releaseActivationAvgTime = releaseActivationTimes.reduce((sum, item) => sum + item, 0) / releaseActivationTimes.length; + const languageServerActivationAvgTime = languageServerActivationTimes.reduce((sum, item) => sum + item, 0) / languageServerActivationTimes.length; + + console.log(`Dev version loaded in ${devActivationAvgTime}ms`); + console.log(`Release version loaded in ${releaseActivationAvgTime}ms`); + console.log(`Language Server loaded in ${languageServerActivationAvgTime}ms`); + + expect(devActivationAvgTime - releaseActivationAvgTime).to.be.lessThan(AllowedIncreaseInActivationDelayInMS, 'Activation times have increased above allowed threshold.'); + }); + } +}); diff --git a/src/test/performance/sample.py b/src/test/performance/sample.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/performance/settings.json b/src/test/performance/settings.json new file mode 100644 index 000000000000..809ebf6ab2f4 --- /dev/null +++ b/src/test/performance/settings.json @@ -0,0 +1 @@ +{ "python.jediEnabled": true } diff --git a/src/test/performanceTest.ts b/src/test/performanceTest.ts new file mode 100644 index 000000000000..c9e5a1d13538 --- /dev/null +++ b/src/test/performanceTest.ts @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +/* +Comparing performance metrics is not easy (the metrics can and always get skewed). +One approach is to run the tests multile times and gather multiple sample data. +For Extension activation times, we load both extensions x times, and re-load the window y times in each x load. +I.e. capture averages by giving the extensions sufficient time to warm up. +This block of code merely launches the tests by using either the dev or release version of the extension, +and spawning the tests (mimic user starting tests from command line), this way we can run tests multiple times. +*/ + +// tslint:disable:no-console no-require-imports no-var-requires + +import { spawn } from 'child_process'; +import * as download from 'download'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as request from 'request'; +import { EXTENSION_ROOT_DIR } from '../client/common/constants'; + +const NamedRegexp = require('named-js-regexp'); +const StreamZip = require('node-stream-zip'); +const del = require('del'); + +const tmpFolder = path.join(EXTENSION_ROOT_DIR, 'tmp'); +const publishedExtensionPath = path.join(tmpFolder, 'ext', 'testReleaseExtensionsFolder'); +const logFilesPath = path.join(tmpFolder, 'test', 'logs'); + +enum Version { + Dev, Release +} + +class TestRunner { + public async start() { + await del([path.join(tmpFolder, '**')]); + await this.extractLatestExtension(publishedExtensionPath); + + const timesToLoadEachVersion = 2; + const devLogFiles: string[] = []; + const releaseLogFiles: string[] = []; + const languageServerLogFiles: string[] = []; + + for (let i = 0; i < timesToLoadEachVersion; i += 1) { + await this.enableLanguageServer(false); + + const devLogFile = path.join(logFilesPath, `dev_loadtimes${i}.txt`); + console.log(`Start Performance Tests: Counter ${i}, for Dev version with Jedi`); + await this.capturePerfTimes(Version.Dev, devLogFile); + devLogFiles.push(devLogFile); + + const releaseLogFile = path.join(logFilesPath, `release_loadtimes${i}.txt`); + console.log(`Start Performance Tests: Counter ${i}, for Release version with Jedi`); + await this.capturePerfTimes(Version.Release, releaseLogFile); + releaseLogFiles.push(releaseLogFile); + + // Language server. + await this.enableLanguageServer(true); + const languageServerLogFile = path.join(logFilesPath, `languageServer_loadtimes${i}.txt`); + console.log(`Start Performance Tests: Counter ${i}, for Release version with Language Server`); + await this.capturePerfTimes(Version.Release, languageServerLogFile); + languageServerLogFiles.push(languageServerLogFile); + } + + console.log('Compare Performance Results'); + await this.runPerfTest(devLogFiles, releaseLogFiles, languageServerLogFiles); + } + private async enableLanguageServer(enable: boolean) { + const settings = `{ "python.jediEnabled": ${!enable} }`; + await fs.writeFile(path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'performance', 'settings.json'), settings); + } + + private async capturePerfTimes(version: Version, logFile: string) { + const releaseVersion = await this.getReleaseVersion(); + const devVersion = await this.getDevVersion(); + await fs.ensureDir(path.dirname(logFile)); + const env: { [key: string]: {} } = { + ACTIVATION_TIMES_LOG_FILE_PATH: logFile, + ACTIVATION_TIMES_EXT_VERSION: version === Version.Release ? releaseVersion : devVersion, + CODE_EXTENSIONS_PATH: version === Version.Release ? publishedExtensionPath : EXTENSION_ROOT_DIR + }; + + await this.launchTest(env); + } + private async runPerfTest(devLogFiles: string[], releaseLogFiles: string[], languageServerLogFiles: string[]) { + const env: { [key: string]: {} } = { + ACTIVATION_TIMES_DEV_LOG_FILE_PATHS: JSON.stringify(devLogFiles), + ACTIVATION_TIMES_RELEASE_LOG_FILE_PATHS: JSON.stringify(releaseLogFiles), + ACTIVATION_TIMES_DEV_LANGUAGE_SERVER_LOG_FILE_PATHS: JSON.stringify(languageServerLogFiles) + }; + + await this.launchTest(env); + } + + private async launchTest(customEnvVars: { [key: string]: {} }) { + await new Promise((resolve, reject) => { + const env: { [key: string]: {} } = { + TEST_FILES_SUFFIX: 'perf.test', + CODE_TESTS_WORKSPACE: path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'performance'), + ...process.env, + ...customEnvVars + }; + + const proc = spawn('node', [path.join(__dirname, 'standardTest.js')], { cwd: EXTENSION_ROOT_DIR, env }); + proc.stdout.pipe(process.stdout); + proc.stderr.pipe(process.stderr); + proc.on('error', reject); + proc.on('close', code => { + if (code === 0) { + resolve(); + } else { + reject(`Failed with code ${code}.`); + } + }); + }); + } + + private async extractLatestExtension(targetDir: string): Promise { + const extensionFile = await this.downloadExtension(); + await this.unzip(extensionFile, targetDir); + } + + private async getReleaseVersion(): Promise { + const url = 'https://marketplace.visualstudio.com/items?itemName=ms-python.python'; + const content = await new Promise((resolve, reject) => { + request(url, (error, response, body) => { + if (error) { + return reject(error); + } + if (response.statusCode === 200) { + return resolve(body); + } + reject(`Status code of ${response.statusCode} received.`); + }); + }); + const re = NamedRegexp('"version"\S?:\S?"(:\\d{4}\\.\\d{1,2}\\.\\d{1,2})"', 'g'); + const matches = re.exec(content); + return matches.groups().version; + } + + private async getDevVersion(): Promise { + // tslint:disable-next-line:non-literal-require + return require(path.join(EXTENSION_ROOT_DIR, 'package.json')).version; + } + + private async unzip(zipFile: string, targetFolder: string): Promise { + await fs.ensureDir(targetFolder); + return new Promise((resolve, reject) => { + const zip = new StreamZip({ + file: zipFile, + storeEntries: true + }); + zip.on('ready', async () => { + zip.extract('extension', targetFolder, err => { + if (err) { + reject(err); + } else { + resolve(); + } + zip.close(); + }); + }); + }); + } + + private async downloadExtension(): Promise { + const version = await this.getReleaseVersion(); + const url = `https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/python/${version}/vspackage`; + const destination = path.join(__dirname, `extension${version}.zip`); + if (await fs.pathExists(destination)) { + return destination; + } + + await download(url, path.dirname(destination), { filename: path.basename(destination) }); + return destination; + } +} + +new TestRunner().start().catch(ex => console.error('Error in running Performance Tests', ex)); diff --git a/src/test/providers/codeActionsProvider.test.ts b/src/test/providers/codeActionsProvider.test.ts new file mode 100644 index 000000000000..8147063d649e --- /dev/null +++ b/src/test/providers/codeActionsProvider.test.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { CancellationToken, CodeActionContext, CodeActionKind, Range, TextDocument } from 'vscode'; +import { PythonCodeActionProvider } from '../../client/providers/codeActionsProvider'; + +suite('CodeAction Provider', () => { + let codeActionsProvider: PythonCodeActionProvider; + let document: TypeMoq.IMock; + let range: TypeMoq.IMock; + let context: TypeMoq.IMock; + let token: TypeMoq.IMock; + + setup(() => { + codeActionsProvider = new PythonCodeActionProvider(); + document = TypeMoq.Mock.ofType(); + range = TypeMoq.Mock.ofType(); + context = TypeMoq.Mock.ofType(); + token = TypeMoq.Mock.ofType(); + }); + + test('Ensure it always returns a source.organizeImports CodeAction', async () => { + const codeActions = await codeActionsProvider.provideCodeActions( + document.object, + range.object, + context.object, + token.object + ); + + if (!codeActions) { + throw Error(`codeActionsProvider.provideCodeActions did not return an array (it returned ${codeActions})`); + } + + const organizeImportsCodeAction = codeActions.filter( + codeAction => codeAction.kind === CodeActionKind.SourceOrganizeImports + ); + expect(organizeImportsCodeAction).to.have.length(1); + expect(organizeImportsCodeAction[0].kind).to.eq(CodeActionKind.SourceOrganizeImports); + }); +}); diff --git a/src/test/providers/completionSource.unit.test.ts b/src/test/providers/completionSource.unit.test.ts new file mode 100644 index 000000000000..a6ddf77bf934 --- /dev/null +++ b/src/test/providers/completionSource.unit.test.ts @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import * as TypeMoq from 'typemoq'; +import { CancellationTokenSource, CompletionItemKind, Position, SymbolKind, TextDocument, TextLine } from 'vscode'; +import { IAutoCompleteSettings, IConfigurationService, IPythonSettings } from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { JediFactory } from '../../client/languageServices/jediProxyFactory'; +import { CompletionSource } from '../../client/providers/completionSource'; +import { IItemInfoSource } from '../../client/providers/itemInfoSource'; +import { IAutoCompleteItem, ICompletionResult, JediProxyHandler } from '../../client/providers/jediProxy'; + +suite('Completion Provider', () => { + let completionSource: CompletionSource; + let jediHandler: TypeMoq.IMock>; + let autoCompleteSettings: TypeMoq.IMock; + let itemInfoSource: TypeMoq.IMock; + setup(() => { + const jediFactory = TypeMoq.Mock.ofType(JediFactory); + jediHandler = TypeMoq.Mock.ofType>(); + const serviceContainer = TypeMoq.Mock.ofType(); + const configService = TypeMoq.Mock.ofType(); + const pythonSettings = TypeMoq.Mock.ofType(); + autoCompleteSettings = TypeMoq.Mock.ofType(); + autoCompleteSettings = TypeMoq.Mock.ofType(); + + jediFactory.setup(j => j.getJediProxyHandler(TypeMoq.It.isAny())) + .returns(() => jediHandler.object); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) + .returns(() => configService.object); + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + pythonSettings.setup(p => p.autoComplete).returns(() => autoCompleteSettings.object); + itemInfoSource = TypeMoq.Mock.ofType(); + completionSource = new CompletionSource(jediFactory.object, serviceContainer.object, itemInfoSource.object); + }); + + async function testDocumentation(source: string, addBrackets: boolean) { + const doc = TypeMoq.Mock.ofType(); + const position = new Position(1, 1); + const token = new CancellationTokenSource().token; + const lineText = TypeMoq.Mock.ofType(); + const completionResult = TypeMoq.Mock.ofType(); + + const autoCompleteItems: IAutoCompleteItem[] = [{ + description: 'description', kind: SymbolKind.Function, + raw_docstring: 'raw docstring', + rawType: CompletionItemKind.Function, + rightLabel: 'right label', + text: 'some text', type: CompletionItemKind.Function + }]; + + autoCompleteSettings.setup(a => a.addBrackets).returns(() => addBrackets); + doc.setup(d => d.fileName).returns(() => ''); + doc.setup(d => d.getText(TypeMoq.It.isAny())).returns(() => source); + doc.setup(d => d.lineAt(TypeMoq.It.isAny())).returns(() => lineText.object); + doc.setup(d => d.offsetAt(TypeMoq.It.isAny())).returns(() => 0); + lineText.setup(l => l.text).returns(() => source); + completionResult.setup(c => c.requestId).returns(() => 1); + completionResult.setup(c => c.items).returns(() => autoCompleteItems); + completionResult.setup((c: any) => c.then).returns(() => undefined); + jediHandler.setup(j => j.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { + return Promise.resolve(completionResult.object); + }); + + const expectedSource = `${source}${autoCompleteItems[0].text}`; + itemInfoSource.setup(i => i.getItemInfoFromText(TypeMoq.It.isAny(), TypeMoq.It.isAny(), + TypeMoq.It.isAny(), expectedSource, TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + + const [item] = await completionSource.getVsCodeCompletionItems(doc.object, position, token); + await completionSource.getDocumentation(item, token); + itemInfoSource.verifyAll(); + } + + test('Ensure docs are provided when \'addBrackets\' setting is false', async () => { + const source = 'if True:\n print("Hello")\n'; + await testDocumentation(source, false); + }); + test('Ensure docs are provided when \'addBrackets\' setting is true', async () => { + const source = 'if True:\n print("Hello")\n'; + await testDocumentation(source, true); + }); + +}); diff --git a/src/test/providers/foldingProvider.test.ts b/src/test/providers/foldingProvider.test.ts new file mode 100644 index 000000000000..9de189afb47f --- /dev/null +++ b/src/test/providers/foldingProvider.test.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import { CancellationTokenSource, FoldingRange, FoldingRangeKind, workspace } from 'vscode'; +import { DocStringFoldingProvider } from '../../client/providers/docStringFoldingProvider'; + +type FileFoldingRanges = { file: string; ranges: FoldingRange[] }; +const pythonFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'folding'); + +// tslint:disable-next-line:max-func-body-length +suite('Provider - Folding Provider', () => { + const docStringFileAndExpectedFoldingRanges: FileFoldingRanges[] = [ + { + file: path.join(pythonFilesPath, 'attach_server.py'), ranges: [ + new FoldingRange(0, 14), new FoldingRange(44, 73, FoldingRangeKind.Comment), + new FoldingRange(95, 143), new FoldingRange(149, 150, FoldingRangeKind.Comment), + new FoldingRange(305, 313), new FoldingRange(320, 322) + ] + }, + { + file: path.join(pythonFilesPath, 'visualstudio_ipython_repl.py'), ranges: [ + new FoldingRange(0, 14), new FoldingRange(78, 79, FoldingRangeKind.Comment), + new FoldingRange(81, 82, FoldingRangeKind.Comment), new FoldingRange(92, 93, FoldingRangeKind.Comment), + new FoldingRange(108, 109, FoldingRangeKind.Comment), new FoldingRange(139, 140, FoldingRangeKind.Comment), + new FoldingRange(169, 170, FoldingRangeKind.Comment), new FoldingRange(275, 277, FoldingRangeKind.Comment), + new FoldingRange(319, 320, FoldingRangeKind.Comment) + ] + }, + { + file: path.join(pythonFilesPath, 'visualstudio_py_debugger.py'), ranges: [ + new FoldingRange(0, 15, FoldingRangeKind.Comment), new FoldingRange(22, 25, FoldingRangeKind.Comment), + new FoldingRange(47, 48, FoldingRangeKind.Comment), new FoldingRange(69, 70, FoldingRangeKind.Comment), + new FoldingRange(96, 97, FoldingRangeKind.Comment), new FoldingRange(105, 106, FoldingRangeKind.Comment), + new FoldingRange(141, 142, FoldingRangeKind.Comment), new FoldingRange(149, 162, FoldingRangeKind.Comment), + new FoldingRange(165, 166, FoldingRangeKind.Comment), new FoldingRange(207, 208, FoldingRangeKind.Comment), + new FoldingRange(235, 237, FoldingRangeKind.Comment), new FoldingRange(240, 241, FoldingRangeKind.Comment), + new FoldingRange(300, 301, FoldingRangeKind.Comment), new FoldingRange(334, 335, FoldingRangeKind.Comment), + new FoldingRange(346, 348, FoldingRangeKind.Comment), new FoldingRange(499, 500, FoldingRangeKind.Comment), + new FoldingRange(558, 559, FoldingRangeKind.Comment), new FoldingRange(602, 604, FoldingRangeKind.Comment), + new FoldingRange(608, 609, FoldingRangeKind.Comment), new FoldingRange(612, 614, FoldingRangeKind.Comment), + new FoldingRange(637, 638, FoldingRangeKind.Comment) + ] + }, + { + file: path.join(pythonFilesPath, 'visualstudio_py_repl.py'), ranges: [] + } + ]; + + docStringFileAndExpectedFoldingRanges.forEach(item => { + test(`Test Docstring folding regions '${path.basename(item.file)}'`, async () => { + const document = await workspace.openTextDocument(item.file); + const provider = new DocStringFoldingProvider(); + const ranges = await provider.provideFoldingRanges(document, {}, new CancellationTokenSource().token); + expect(ranges).to.be.lengthOf(item.ranges.length); + ranges!.forEach(range => { + const index = item.ranges + .findIndex(searchItem => searchItem.start === range.start && + searchItem.end === range.end); + expect(index).to.be.greaterThan(-1, `${range.start}, ${range.end} not found`); + }); + }); + }); +}); diff --git a/src/test/providers/pythonSignatureProvider.unit.test.ts b/src/test/providers/pythonSignatureProvider.unit.test.ts new file mode 100644 index 000000000000..490eff19e9f9 --- /dev/null +++ b/src/test/providers/pythonSignatureProvider.unit.test.ts @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { assert, expect, use } from 'chai'; +import * as chaipromise from 'chai-as-promised'; +import * as TypeMoq from 'typemoq'; +import { CancellationToken, Position, SignatureHelp, + TextDocument, TextLine, Uri } from 'vscode'; +import { JediFactory } from '../../client/languageServices/jediProxyFactory'; +import { IArgumentsResult, JediProxyHandler } from '../../client/providers/jediProxy'; +import { isPositionInsideStringOrComment } from '../../client/providers/providerUtilities'; +import { PythonSignatureProvider } from '../../client/providers/signatureProvider'; + +use(chaipromise); + +suite('Signature Provider unit tests', () => { + let pySignatureProvider: PythonSignatureProvider; + let jediHandler: TypeMoq.IMock>; + let argResultItems: IArgumentsResult; + setup(() => { + const jediFactory = TypeMoq.Mock.ofType(JediFactory); + jediHandler = TypeMoq.Mock.ofType>(); + jediFactory.setup(j => j.getJediProxyHandler(TypeMoq.It.isAny())) + .returns(() => jediHandler.object); + pySignatureProvider = new PythonSignatureProvider(jediFactory.object); + argResultItems = { + definitions: [ + { + description: 'The result', + docstring: 'Some docstring goes here.', + name: 'print', + paramindex: 0, + params: [ + { + description: 'Some parameter', + docstring: 'gimme docs', + name: 'param', + value: 'blah' + } + ] + } + ], + requestId: 1 + }; + }); + + function testSignatureReturns(source: string, pos: number): Thenable { + const doc = TypeMoq.Mock.ofType(); + const position = new Position(0, pos); + const lineText = TypeMoq.Mock.ofType(); + const argsResult = TypeMoq.Mock.ofType(); + const cancelToken = TypeMoq.Mock.ofType(); + cancelToken.setup(ct => ct.isCancellationRequested).returns(() => false); + + doc.setup(d => d.fileName).returns(() => ''); + doc.setup(d => d.getText(TypeMoq.It.isAny())).returns(() => source); + doc.setup(d => d.lineAt(TypeMoq.It.isAny())).returns(() => lineText.object); + doc.setup(d => d.offsetAt(TypeMoq.It.isAny())).returns(() => pos - 1); // pos is 1-based + const docUri = TypeMoq.Mock.ofType(); + docUri.setup(u => u.scheme).returns(() => 'http'); + doc.setup(d => d.uri).returns(() => docUri.object); + lineText.setup(l => l.text).returns(() => source); + argsResult.setup(c => c.requestId).returns(() => 1); + argsResult.setup(c => c.definitions).returns(() => argResultItems[0].definitions); + jediHandler.setup(j => j.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { + return Promise.resolve(argResultItems); + }); + + return pySignatureProvider.provideSignatureHelp(doc.object, position, cancelToken.object); + } + + function testIsInsideStringOrComment(sourceLine: string, sourcePos: number) : boolean { + const textLine: TypeMoq.IMock = TypeMoq.Mock.ofType(); + textLine.setup(t => t.text).returns(() => sourceLine); + const doc: TypeMoq.IMock = TypeMoq.Mock.ofType(); + const pos: Position = new Position(1, sourcePos); + + doc.setup(d => d.fileName).returns(() => ''); + doc.setup(d => d.getText(TypeMoq.It.isAny())).returns(() => sourceLine); + doc.setup(d => d.lineAt(TypeMoq.It.isAny())).returns(() => textLine.object); + doc.setup(d => d.offsetAt(TypeMoq.It.isAny())).returns(() => sourcePos); + + return isPositionInsideStringOrComment(doc.object, pos); + } + + test('Ensure no signature is given within a string.', async () => { + const source = ' print(\'Python is awesome,\')\n'; + const sigHelp: SignatureHelp = await testSignatureReturns(source, 27); + expect(sigHelp).to.not.be.equal(undefined, 'Expected to get a blank signature item back - did the pattern change here?'); + expect(sigHelp.signatures.length).to.equal(0, 'Signature provided for symbols within a string?'); + }); + test('Ensure no signature is given within a line comment.', async () => { + const source = '# print(\'Python is awesome,\')\n'; + const sigHelp: SignatureHelp = await testSignatureReturns(source, 28); + expect(sigHelp).to.not.be.equal(undefined, 'Expected to get a blank signature item back - did the pattern change here?'); + expect(sigHelp.signatures.length).to.equal(0, 'Signature provided for symbols within a full-line comment?'); + }); + test('Ensure no signature is given within a comment tailing a command.', async () => { + const source = ' print(\'Python\') # print(\'is awesome,\')\n'; + const sigHelp: SignatureHelp = await testSignatureReturns(source, 38); + expect(sigHelp).to.not.be.equal(undefined, 'Expected to get a blank signature item back - did the pattern change here?'); + expect(sigHelp.signatures.length).to.equal(0, 'Signature provided for symbols within a trailing comment?'); + }); + test('Ensure signature is given for built-in print command.', async () => { + const source = ' print(\'Python\',)\n'; + let sigHelp: SignatureHelp; + try { + sigHelp = await testSignatureReturns(source, 18); + expect(sigHelp).to.not.equal(undefined, 'Expected to get a blank signature item back - did the pattern change here?'); + expect(sigHelp.signatures.length).to.not.equal(0, 'Expected dummy argresult back from testing our print signature.'); + expect(sigHelp.activeParameter).to.be.equal(0, 'Parameter for print should be the first member of the test argresult\'s params object.'); + expect(sigHelp.activeSignature).to.be.equal(0, 'The signature for print should be the first member of the test argresult.'); + expect(sigHelp.signatures[sigHelp.activeSignature].label).to.be.equal('print(param)', `Expected arg result calls for specific returned signature of \'print(param)\' but we got ${sigHelp.signatures[sigHelp.activeSignature].label}`); + } catch (error) { + assert(false, `Caught exception ${error}`); + } + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected.', () => { + const sourceLine: string = ' print(\'Hello world!\')\n'; + const sourcePos: number = sourceLine.length - 1; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.not.be.equal(true, [ + `Position set to the end of ${sourceLine} but `, + 'is reported as being within a string or comment.'].join('')); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected at end of source.', () => { + const sourceLine: string = ' print(\'Hello world!\')\n'; + const sourcePos: number = 0; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.not.be.equal(true, [ + `Position set to the end of ${sourceLine} but `, + 'is reported as being within a string or comment.'].join('')); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected at beginning of source.', () => { + const sourceLine: string = ' print(\'Hello world!\')\n'; + const sourcePos: number = 0; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.not.be.equal(true, [ + `Position set to the beginning of ${sourceLine} but `, + 'is reported as being within a string or comment.'].join('')); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected within a string.', () => { + const sourceLine: string = ' print(\'Hello world!\')\n'; + const sourcePos: number = 16; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal(true, [ + `Position set within the string in ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.'].join('')); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected immediately before a string.', () => { + const sourceLine: string = ' print(\'Hello world!\')\n'; + const sourcePos: number = 8; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal(false, [ + `Position set to just before the string in ${sourceLine} (position ${sourcePos}) but `, + 'is reported as being within a string or comment.'].join('')); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected immediately in a string.', () => { + const sourceLine: string = ' print(\'Hello world!\')\n'; + const sourcePos: number = 9; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal(true, [ + `Position set to the start of the string in ${sourceLine} (position ${sourcePos}) but `, + 'is reported as being within a string or comment.'].join('')); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected within a comment.', () => { + const sourceLine: string = '# print(\'Hello world!\')\n'; + const sourcePos: number = 16; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal(true, [ + `Position set within a full line comment ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.'].join('')); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected within a trailing comment.', () => { + const sourceLine: string = ' print(\'Hello world!\') # some comment...\n'; + const sourcePos: number = 34; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal(true, [ + `Position set within a trailing line comment ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.'].join('')); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected at the very end of a trailing comment.', () => { + const sourceLine: string = ' print(\'Hello world!\') # some comment...\n'; + const sourcePos: number = sourceLine.length - 1; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal(true, [ + `Position set within a trailing line comment ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.'].join('')); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected within a multiline string.', () => { + const sourceLine: string = ' stringVal = \'\'\'This is a multiline\nstring that you can use\nto test this stuff out with\neveryday!\'\'\'\n'; + const sourcePos: number = 48; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal(true, [ + `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.'].join('')); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected at the very last quote on a multiline string.', () => { + const sourceLine: string = ' stringVal = \'\'\'This is a multiline\nstring that you can use\nto test this stuff out with\neveryday!\'\'\'\n'; + const sourcePos: number = sourceLine.length - 2; // just at the last ' + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal(true, [ + `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.'].join('')); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected within a multiline string (double-quoted).', () => { + const sourceLine: string = ' stringVal = """This is a multiline\nstring that you can use\nto test this stuff out with\neveryday!"""\n'; + const sourcePos: number = 48; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal(true, [ + `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.'].join('')); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected at the very last quote on a multiline string (double-quoted).', () => { + const sourceLine: string = ' stringVal = """This is a multiline\nstring that you can use\nto test this stuff out with\neveryday!"""\n'; + const sourcePos: number = sourceLine.length - 2; // just at the last ' + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal(true, [ + `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.'].join('')); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected during construction of a multiline string (double-quoted).', () => { + const sourceLine: string = ' stringVal = """This is a multiline\nstring that you can use\nto test this stuff'; + const sourcePos: number = sourceLine.length - 1; // just at the last position in the string before it's termination + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal(true, [ + `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.'].join('')); + }); +}); diff --git a/src/test/providers/repl.test.ts b/src/test/providers/repl.unit.test.ts similarity index 100% rename from src/test/providers/repl.test.ts rename to src/test/providers/repl.unit.test.ts diff --git a/src/test/providers/shebangCodeLenseProvider.test.ts b/src/test/providers/shebangCodeLenseProvider.test.ts index bc89c1761d5e..b1e4a15bd8ae 100644 --- a/src/test/providers/shebangCodeLenseProvider.test.ts +++ b/src/test/providers/shebangCodeLenseProvider.test.ts @@ -1,8 +1,7 @@ import * as assert from 'assert'; import * as child_process from 'child_process'; import * as path from 'path'; -import * as vscode from 'vscode'; -import { CancellationTokenSource } from 'vscode'; +import { CancellationTokenSource, TextDocument, workspace } from 'vscode'; import { IS_WINDOWS, PythonSettings } from '../../client/common/configSettings'; import { ShebangCodeLensProvider } from '../../client/interpreter/display/shebangCodeLensProvider'; import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; @@ -91,7 +90,7 @@ suite('Shebang detection', () => { }); async function openFile(fileName: string) { - return vscode.workspace.openTextDocument(fileName); + return workspace.openTextDocument(fileName); } async function getFullyQualifiedPathToInterpreter(pythonPath: string) { return new Promise(resolve => { @@ -101,8 +100,8 @@ suite('Shebang detection', () => { }).catch(() => undefined); } - async function setupCodeLens(document: vscode.TextDocument) { + async function setupCodeLens(document: TextDocument) { const codeLensProvider = new ShebangCodeLensProvider(ioc.serviceContainer); - return await codeLensProvider.provideCodeLenses(document, new CancellationTokenSource().token); + return codeLensProvider.provideCodeLenses(document, new CancellationTokenSource().token); } }); diff --git a/src/test/providers/symbolProvider.unit.test.ts b/src/test/providers/symbolProvider.unit.test.ts new file mode 100644 index 000000000000..8ff4d46bccd6 --- /dev/null +++ b/src/test/providers/symbolProvider.unit.test.ts @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any no-require-imports no-var-requires + +import { expect, use } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { CancellationToken, CancellationTokenSource, CompletionItemKind, DocumentSymbolProvider, SymbolKind, TextDocument, Uri } from 'vscode'; +import { IFileSystem } from '../../client/common/platform/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { JediFactory } from '../../client/languageServices/jediProxyFactory'; +import { IDefinition, ISymbolResult, JediProxyHandler } from '../../client/providers/jediProxy'; +import { PythonSymbolProvider } from '../../client/providers/symbolProvider'; + +const assertArrays = require('chai-arrays'); +use(assertArrays); + +suite('Symbol Provider', () => { + let serviceContainer: TypeMoq.IMock; + let jediHandler: TypeMoq.IMock>; + let jediFactory: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + let provider: DocumentSymbolProvider; + let uri: Uri; + let doc: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + jediFactory = TypeMoq.Mock.ofType(JediFactory); + jediHandler = TypeMoq.Mock.ofType>(); + + fileSystem = TypeMoq.Mock.ofType(); + doc = TypeMoq.Mock.ofType(); + jediFactory.setup(j => j.getJediProxyHandler(TypeMoq.It.isAny())) + .returns(() => jediHandler.object); + + serviceContainer.setup(c => c.get(IFileSystem)).returns(() => fileSystem.object); + }); + + async function testDocumentation(requestId: number, fileName: string, expectedSize: number, token?: CancellationToken, isUntitled = false) { + fileSystem.setup(fs => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => true); + token = token ? token : new CancellationTokenSource().token; + const symbolResult = TypeMoq.Mock.ofType(); + + const definitions: IDefinition[] = [ + { + container: '', fileName: fileName, kind: SymbolKind.Array, + range: { endColumn: 0, endLine: 0, startColumn: 0, startLine: 0 }, + rawType: '', text: '', type: CompletionItemKind.Class + } + ]; + + uri = Uri.file(fileName); + doc.setup(d => d.uri).returns(() => uri); + doc.setup(d => d.fileName).returns(() => fileName); + doc.setup(d => d.isUntitled).returns(() => isUntitled); + doc.setup(d => d.getText(TypeMoq.It.isAny())).returns(() => ''); + symbolResult.setup(c => c.requestId).returns(() => requestId); + symbolResult.setup(c => c.definitions).returns(() => definitions); + symbolResult.setup((c: any) => c.then).returns(() => undefined); + jediHandler.setup(j => j.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(symbolResult.object)); + + const items = await provider.provideDocumentSymbols(doc.object, token); + expect(items).to.be.array(); + expect(items).to.be.ofSize(expectedSize); + } + + test('Ensure symbols are returned', async () => { + provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0); + await testDocumentation(1, __filename, 1); + }); + test('Ensure symbols are returned (for untitled documents)', async () => { + provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0); + await testDocumentation(1, __filename, 1, undefined, true); + }); + test('Ensure symbols are returned with a debounce of 100ms', async () => { + provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0); + await testDocumentation(1, __filename, 1); + }); + test('Ensure symbols are returned with a debounce of 100ms (for untitled documents)', async () => { + provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0); + await testDocumentation(1, __filename, 1, undefined, true); + }); + test('Ensure symbols are not returned when cancelled', async () => { + provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0); + const tokenSource = new CancellationTokenSource(); + tokenSource.cancel(); + await testDocumentation(1, __filename, 0, tokenSource.token); + }); + test('Ensure symbols are not returned when cancelled (for untitled documents)', async () => { + provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0); + const tokenSource = new CancellationTokenSource(); + tokenSource.cancel(); + await testDocumentation(1, __filename, 0, tokenSource.token, true); + }); + test('Ensure symbols are returned only for the last request', async () => { + provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 100); + await Promise.all([ + testDocumentation(1, __filename, 0), + testDocumentation(2, __filename, 0), + testDocumentation(3, __filename, 1) + ]); + }); + test('Ensure symbols are returned for all the requests when the doc is untitled', async () => { + provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 100); + await Promise.all([ + testDocumentation(1, __filename, 1, undefined, true), + testDocumentation(2, __filename, 1, undefined, true), + testDocumentation(3, __filename, 1, undefined, true) + ]); + }); + test('Ensure symbols are returned for multiple documents', async () => { + provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0); + await Promise.all([ + testDocumentation(1, 'file1', 1), + testDocumentation(2, 'file2', 1) + ]); + }); + test('Ensure symbols are returned for multiple untitled documents ', async () => { + provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0); + await Promise.all([ + testDocumentation(1, 'file1', 1, undefined, true), + testDocumentation(2, 'file2', 1, undefined, true) + ]); + }); + test('Ensure symbols are returned for multiple documents with a debounce of 100ms', async () => { + provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 100); + await Promise.all([ + testDocumentation(1, 'file1', 1), + testDocumentation(2, 'file2', 1) + ]); + }); + test('Ensure symbols are returned for multiple untitled documents with a debounce of 100ms', async () => { + provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 100); + await Promise.all([ + testDocumentation(1, 'file1', 1, undefined, true), + testDocumentation(2, 'file2', 1, undefined, true) + ]); + }); + test('Ensure IFileSystem.arePathsSame is used', async () => { + doc.setup(d => d.getText()) + .returns(() => '') + .verifiable(TypeMoq.Times.once()); + doc.setup(d => d.isDirty) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + doc.setup(d => d.fileName) + .returns(() => __filename); + + const symbols = TypeMoq.Mock.ofType(); + symbols.setup((s: any) => s.then).returns(() => undefined); + const definitions: IDefinition[] = []; + for (let counter = 0; counter < 3; counter += 1) { + const def = TypeMoq.Mock.ofType(); + def.setup(d => d.fileName).returns(() => counter.toString()); + definitions.push(def.object); + + fileSystem.setup(fs => fs.arePathsSame(TypeMoq.It.isValue(counter.toString()), TypeMoq.It.isValue(__filename))) + .returns(() => false) + .verifiable(TypeMoq.Times.exactly(1)); + } + symbols.setup(s => s.definitions) + .returns(() => definitions) + .verifiable(TypeMoq.Times.atLeastOnce()); + + jediHandler.setup(j => j.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(symbols.object)) + .verifiable(TypeMoq.Times.once()); + + provider = new PythonSymbolProvider(serviceContainer.object, jediFactory.object, 0); + await provider.provideDocumentSymbols(doc.object, new CancellationTokenSource().token); + + doc.verifyAll(); + symbols.verifyAll(); + fileSystem.verifyAll(); + jediHandler.verifyAll(); + }); +}); diff --git a/src/test/providers/terminal.test.ts b/src/test/providers/terminal.unit.test.ts similarity index 96% rename from src/test/providers/terminal.test.ts rename to src/test/providers/terminal.unit.test.ts index 7b9097120d7e..6573ef9d231a 100644 --- a/src/test/providers/terminal.test.ts +++ b/src/test/providers/terminal.unit.test.ts @@ -68,7 +68,7 @@ suite('Terminal Provider', () => { terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(undefined), TypeMoq.It.isValue('Python'))).returns(() => terminalService.object); commandHandler!.call(terminalProvider); - terminalService.verify(t => t.show(), TypeMoq.Times.once()); + terminalService.verify(t => t.show(false), TypeMoq.Times.once()); }); test('Ensure terminal creation does not use uri of the active documents which is untitled', () => { @@ -94,7 +94,7 @@ suite('Terminal Provider', () => { terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(undefined), TypeMoq.It.isValue('Python'))).returns(() => terminalService.object); commandHandler!.call(terminalProvider); - terminalService.verify(t => t.show(), TypeMoq.Times.once()); + terminalService.verify(t => t.show(false), TypeMoq.Times.once()); }); test('Ensure terminal creation uses uri of active document', () => { @@ -122,7 +122,7 @@ suite('Terminal Provider', () => { terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(documentUri), TypeMoq.It.isValue('Python'))).returns(() => terminalService.object); commandHandler!.call(terminalProvider); - terminalService.verify(t => t.show(), TypeMoq.Times.once()); + terminalService.verify(t => t.show(false), TypeMoq.Times.once()); }); test('Ensure terminal creation uses uri of active workspace', () => { @@ -147,6 +147,6 @@ suite('Terminal Provider', () => { terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(workspaceUri), TypeMoq.It.isValue('Python'))).returns(() => terminalService.object); commandHandler!.call(terminalProvider); - terminalService.verify(t => t.show(), TypeMoq.Times.once()); + terminalService.verify(t => t.show(false), TypeMoq.Times.once()); }); }); diff --git a/src/test/pythonFiles/autocomp/four.py b/src/test/pythonFiles/autocomp/four.py index f67f78af0856..470338f71157 100644 --- a/src/test/pythonFiles/autocomp/four.py +++ b/src/test/pythonFiles/autocomp/four.py @@ -1,4 +1,4 @@ -# -*- coding:utf-8 -*- +# -*- coding: utf-8 -*- # pylint: disable=E0401, W0512 import os diff --git a/src/test/pythonFiles/debugging/logMessage.py b/src/test/pythonFiles/debugging/logMessage.py new file mode 100644 index 000000000000..ec04cd68c76e --- /dev/null +++ b/src/test/pythonFiles/debugging/logMessage.py @@ -0,0 +1,4 @@ +import time +a = 1 +b = 2 +c = a + b diff --git a/src/test/pythonFiles/debugging/loopyTest.py b/src/test/pythonFiles/debugging/loopyTest.py new file mode 100644 index 000000000000..03c95d371918 --- /dev/null +++ b/src/test/pythonFiles/debugging/loopyTest.py @@ -0,0 +1,2 @@ +for i in range(10): + print(i) diff --git a/src/test/pythonFiles/debugging/multiThread.py b/src/test/pythonFiles/debugging/multiThread.py index 707f2568a2da..588971ffb502 100644 --- a/src/test/pythonFiles/debugging/multiThread.py +++ b/src/test/pythonFiles/debugging/multiThread.py @@ -4,7 +4,7 @@ def bar(): time.sleep(2) - print("abcdef") + print('bar') def foo(x): while True: diff --git a/src/test/pythonFiles/debugging/printSysArgv.py b/src/test/pythonFiles/debugging/printSysArgv.py new file mode 100644 index 000000000000..d14add5dd7f9 --- /dev/null +++ b/src/test/pythonFiles/debugging/printSysArgv.py @@ -0,0 +1,4 @@ +import sys +import time +sys.stdout.write(','.join(sys.argv[1:])) +sys.stdout.flush() diff --git a/src/test/pythonFiles/debugging/sample2WithoutSleep.py b/src/test/pythonFiles/debugging/sample2WithoutSleep.py new file mode 100644 index 000000000000..88b11216bc70 --- /dev/null +++ b/src/test/pythonFiles/debugging/sample2WithoutSleep.py @@ -0,0 +1,13 @@ +import time +# time.sleep(3) +a = 1 +b = 2 +print(a + b) + +def do_something(name): + print("inside") + print(name) + +do_something("Do that") + +print("hello world") diff --git a/src/test/pythonFiles/debugging/sampleWithAssertEx.py b/src/test/pythonFiles/debugging/sampleWithAssertEx.py new file mode 100644 index 000000000000..2cfffa40db4b --- /dev/null +++ b/src/test/pythonFiles/debugging/sampleWithAssertEx.py @@ -0,0 +1 @@ +assert False diff --git a/src/test/pythonFiles/debugging/sampleWithSleep.py b/src/test/pythonFiles/debugging/sampleWithSleep.py new file mode 100644 index 000000000000..7a84f4f0da0c --- /dev/null +++ b/src/test/pythonFiles/debugging/sampleWithSleep.py @@ -0,0 +1,8 @@ +import time +import os +print(os.getpid()) +time.sleep(1) +for i in 10000: + time.sleep(0.1) + print(i) +print('end') diff --git a/src/test/pythonFiles/debugging/stdErrOutput.py b/src/test/pythonFiles/debugging/stdErrOutput.py index 1e55c2ffd4ba..ef576d80d8a8 100644 --- a/src/test/pythonFiles/debugging/stdErrOutput.py +++ b/src/test/pythonFiles/debugging/stdErrOutput.py @@ -1,3 +1,4 @@ import sys +import time sys.stderr.write('error output') sys.stderr.flush() diff --git a/src/test/pythonFiles/debugging/stdOutOutput.py b/src/test/pythonFiles/debugging/stdOutOutput.py index 9d1994322597..e750f3c1fcbe 100644 --- a/src/test/pythonFiles/debugging/stdOutOutput.py +++ b/src/test/pythonFiles/debugging/stdOutOutput.py @@ -1,3 +1,4 @@ import sys +import time sys.stdout.write('normal output') sys.stdout.flush() diff --git a/src/test/pythonFiles/definition/decorators.py b/src/test/pythonFiles/definition/decorators.py deleted file mode 100644 index 6d772479b55d..000000000000 --- a/src/test/pythonFiles/definition/decorators.py +++ /dev/null @@ -1,28 +0,0 @@ -def identity(ob): - return ob - -@identity -def myfunc(): - print "my function" - -myfunc() - -# https://github.com/DonJayamanne/pythonVSCode/issues/1046 -from fabric.api import sudo -# currently go to definition of sudo will go to some decorator function -# works, if fabric package is not installed -sudo() - -from numba import jit - -# https://github.com/DonJayamanne/pythonVSCode/issues/478 -@jit() -def calculate_cash_flows(remaining_loan_term, remaining_io_term, - settle_balance, settle_date, payment_day, - ir_fixed, ir_accrual_day_count_basis, - amortizing_debt_service): - print("") - -# currently go to definition of sudo will go to some decorator function -# works, if fabric package is not installed -calculate_cash_flows(1,2,3,4,5,6,7,8) diff --git a/src/test/pythonFiles/definition/four.py b/src/test/pythonFiles/definition/four.py index f67f78af0856..470338f71157 100644 --- a/src/test/pythonFiles/definition/four.py +++ b/src/test/pythonFiles/definition/four.py @@ -1,4 +1,4 @@ -# -*- coding:utf-8 -*- +# -*- coding: utf-8 -*- # pylint: disable=E0401, W0512 import os diff --git a/src/test/pythonFiles/definition/navigation/__init__.py b/src/test/pythonFiles/definition/navigation/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/definition/navigation/definitions.py b/src/test/pythonFiles/definition/navigation/definitions.py new file mode 100644 index 000000000000..a8379a49f960 --- /dev/null +++ b/src/test/pythonFiles/definition/navigation/definitions.py @@ -0,0 +1,31 @@ +from contextlib import contextmanager + +def my_decorator(fn): + """ + This is my decorator. + """ + def wrapper(*args, **kwargs): + """ + This is the wrapper. + """ + return 42 + return wrapper + +@my_decorator +def thing(arg): + """ + Thing which is decorated. + """ + pass + +@contextmanager +def my_context_manager(): + """ + This is my context manager. + """ + print("before") + yield + print("after") + +with my_context_manager(): + thing(19) diff --git a/src/test/pythonFiles/definition/navigation/usages.py b/src/test/pythonFiles/definition/navigation/usages.py new file mode 100644 index 000000000000..deb6d78edc15 --- /dev/null +++ b/src/test/pythonFiles/definition/navigation/usages.py @@ -0,0 +1,16 @@ +import definitions +from .definitions import my_context_manager, my_decorator, thing + +@definitions.my_decorator +def one(): + pass + +@my_decorator +def two(): + pass + +with definitions.my_context_manager(): + definitions.thing(19) + +with my_context_manager(): + thing(19) diff --git a/src/test/pythonFiles/exclusions/Lib/fileLib.py b/src/test/pythonFiles/exclusions/Lib/fileLib.py new file mode 100644 index 000000000000..50000adeda40 --- /dev/null +++ b/src/test/pythonFiles/exclusions/Lib/fileLib.py @@ -0,0 +1 @@ + a \ No newline at end of file diff --git a/src/test/pythonFiles/exclusions/Lib/site-packages/sitePackages.py b/src/test/pythonFiles/exclusions/Lib/site-packages/sitePackages.py new file mode 100644 index 000000000000..dad1af98c7f5 --- /dev/null +++ b/src/test/pythonFiles/exclusions/Lib/site-packages/sitePackages.py @@ -0,0 +1 @@ + b \ No newline at end of file diff --git a/src/test/pythonFiles/exclusions/dir1/dir1file.py b/src/test/pythonFiles/exclusions/dir1/dir1file.py new file mode 100644 index 000000000000..fe453b3fcc6a --- /dev/null +++ b/src/test/pythonFiles/exclusions/dir1/dir1file.py @@ -0,0 +1 @@ + for \ No newline at end of file diff --git a/src/test/pythonFiles/exclusions/dir1/dir2/dir2file.py b/src/test/pythonFiles/exclusions/dir1/dir2/dir2file.py new file mode 100644 index 000000000000..fe453b3fcc6a --- /dev/null +++ b/src/test/pythonFiles/exclusions/dir1/dir2/dir2file.py @@ -0,0 +1 @@ + for \ No newline at end of file diff --git a/src/test/pythonFiles/exclusions/one.py b/src/test/pythonFiles/exclusions/one.py new file mode 100644 index 000000000000..8c68a1c1fee2 --- /dev/null +++ b/src/test/pythonFiles/exclusions/one.py @@ -0,0 +1 @@ + if \ No newline at end of file diff --git a/src/test/pythonFiles/folding/attach_server.py b/src/test/pythonFiles/folding/attach_server.py new file mode 100644 index 000000000000..c67dc9f106a6 --- /dev/null +++ b/src/test/pythonFiles/folding/attach_server.py @@ -0,0 +1,330 @@ +# Python Tools for Visual Studio +# Copyright(c) Microsoft Corporation +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# +# See the Apache Version 2.0 License for specific language governing +# permissions and limitations under the License. + +__author__ = "Microsoft Corporation " +__version__ = "3.0.0.0" + +__all__ = ['enable_attach', 'wait_for_attach', 'break_into_debugger', 'settrace', 'is_attached', 'AttachAlreadyEnabledError'] + +import atexit +import getpass +import os +import os.path +import platform +import socket +import struct +import sys +import threading +try: + import thread +except ImportError: + import _thread as thread +try: + import ssl +except ImportError: + ssl = None + +import ptvsd.visualstudio_py_debugger as vspd +import ptvsd.visualstudio_py_repl as vspr +from ptvsd.visualstudio_py_util import to_bytes, read_bytes, read_int, read_string, write_bytes, write_int, write_string + + +# The server (i.e. the Python app) waits on a TCP port provided. Whenever anything connects to that port, +# it immediately sends the octet sequence 'PTVSDBG', followed by version number represented as int64, +# and then waits for the client to respond with the same exact byte sequence. After signatures are thereby +# exchanged and found to match, the client is expected to provide a string secret (in the usual debugger +# string format, None/ACII/Unicode prefix + length + data), which can be an empty string to designate the +# lack of a specified secret. +# +# If the secret does not match the one expected by the server, it responds with 'RJCT', and then closes +# the connection. Otherwise, the server responds with 'ACPT', and awaits a 4-octet command. The following +# commands are recognized: +# +# 'INFO' +# Report information about the process. The server responds with the following information, in order: +# - Process ID (int64) +# - Executable name (string) +# - User name (string) +# - Implementation name (string) +# and then immediately closes connection. Note, all string fields can be empty or null strings. +# +# 'ATCH' +# Attach debugger to the process. If successful, the server responds with 'ACPT', followed by process ID +# (int64), and then the Python language version that the server is running represented by three int64s - +# major, minor, micro; From there on the socket is assumed to be using the normal PTVS debugging protocol. +# If attaching was not successful (which can happen if some other debugger is already attached), the server +# responds with 'RJCT' and closes the connection. +# +# 'REPL' +# Attach REPL to the process. If successful, the server responds with 'ACPT', and from there on the socket +# is assumed to be using the normal PTVS REPL protocol. If not successful (which can happen if there is +# no debugger attached), the server responds with 'RJCT' and closes the connection. + +PTVS_VER = '2.2' +DEFAULT_PORT = 5678 +PTVSDBG_VER = 6 # must be kept in sync with DebuggerProtocolVersion in PythonRemoteProcess.cs +PTVSDBG = to_bytes('PTVSDBG') +ACPT = to_bytes('ACPT') +RJCT = to_bytes('RJCT') +INFO = to_bytes('INFO') +ATCH = to_bytes('ATCH') +REPL = to_bytes('REPL') + +_attach_enabled = False +_attached = threading.Event() +vspd.DONT_DEBUG.append(os.path.normcase(__file__)) + + +class AttachAlreadyEnabledError(Exception): + """`ptvsd.enable_attach` has already been called in this process.""" + + +def enable_attach(secret, address = ('0.0.0.0', DEFAULT_PORT), certfile = None, keyfile = None, redirect_output = True): + """Enables Python Tools for Visual Studio to attach to this process remotely + to debug Python code. + + Parameters + ---------- + secret : str + Used to validate the clients - only those clients providing the valid + secret will be allowed to connect to this server. On client side, the + secret is prepended to the Qualifier string, separated from the + hostname by ``'@'``, e.g.: ``'secret@myhost.cloudapp.net:5678'``. If + secret is ``None``, there's no validation, and any client can connect + freely. + address : (str, int), optional + Specifies the interface and port on which the debugging server should + listen for TCP connections. It is in the same format as used for + regular sockets of the `socket.AF_INET` family, i.e. a tuple of + ``(hostname, port)``. On client side, the server is identified by the + Qualifier string in the usual ``'hostname:port'`` format, e.g.: + ``'myhost.cloudapp.net:5678'``. Default is ``('0.0.0.0', 5678)``. + certfile : str, optional + Used to enable SSL. If not specified, or if set to ``None``, the + connection between this program and the debugger will be unsecure, + and can be intercepted on the wire. If specified, the meaning of this + parameter is the same as for `ssl.wrap_socket`. + keyfile : str, optional + Used together with `certfile` when SSL is enabled. Its meaning is the + same as for ``ssl.wrap_socket``. + redirect_output : bool, optional + Specifies whether any output (on both `stdout` and `stderr`) produced + by this program should be sent to the debugger. Default is ``True``. + + Notes + ----- + This function returns immediately after setting up the debugging server, + and does not block program execution. If you need to block until debugger + is attached, call `ptvsd.wait_for_attach`. The debugger can be detached + and re-attached multiple times after `enable_attach` is called. + + This function can only be called once during the lifetime of the process. + On a second call, `AttachAlreadyEnabledError` is raised. In circumstances + where the caller does not control how many times the function will be + called (e.g. when a script with a single call is run more than once by + a hosting app or framework), the call should be wrapped in ``try..except``. + + Only the thread on which this function is called, and any threads that are + created after it returns, will be visible in the debugger once it is + attached. Any threads that are already running before this function is + called will not be visible. + """ + + if not ssl and (certfile or keyfile): + raise ValueError('could not import the ssl module - SSL is not supported on this version of Python') + + if sys.platform == 'cli': + # Check that IronPython was launched with -X:Frames and -X:Tracing, since we can't register our trace + # func on the thread that calls enable_attach otherwise + import clr + x_tracing = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Tracing + x_frames = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Frames + if not x_tracing or not x_frames: + raise RuntimeError('IronPython must be started with -X:Tracing and -X:Frames options to support PTVS remote debugging.') + + global _attach_enabled + if _attach_enabled: + raise AttachAlreadyEnabledError('ptvsd.enable_attach() has already been called in this process.') + _attach_enabled = True + + atexit.register(vspd.detach_process_and_notify_debugger) + + server = socket.socket(proto=socket.IPPROTO_TCP) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind(address) + server.listen(1) + def server_thread_func(): + while True: + client = None + raw_client = None + try: + client, addr = server.accept() + if certfile: + client = ssl.wrap_socket(client, server_side = True, ssl_version = ssl.PROTOCOL_TLSv1, certfile = certfile, keyfile = keyfile) + write_bytes(client, PTVSDBG) + write_int(client, PTVSDBG_VER) + + response = read_bytes(client, 7) + if response != PTVSDBG: + continue + dbg_ver = read_int(client) + if dbg_ver != PTVSDBG_VER: + continue + + client_secret = read_string(client) + if secret is None or secret == client_secret: + write_bytes(client, ACPT) + else: + write_bytes(client, RJCT) + continue + + response = read_bytes(client, 4) + + if response == INFO: + try: + pid = os.getpid() + except AttributeError: + pid = 0 + write_int(client, pid) + + exe = sys.executable or '' + write_string(client, exe) + + try: + username = getpass.getuser() + except AttributeError: + username = '' + write_string(client, username) + + try: + impl = platform.python_implementation() + except AttributeError: + try: + impl = sys.implementation.name + except AttributeError: + impl = 'Python' + + major, minor, micro, release_level, serial = sys.version_info + + os_and_arch = platform.system() + if os_and_arch == "": + os_and_arch = sys.platform + try: + if sys.maxsize > 2**32: + os_and_arch += ' 64-bit' + else: + os_and_arch += ' 32-bit' + except AttributeError: + pass + + version = '%s %s.%s.%s (%s)' % (impl, major, minor, micro, os_and_arch) + write_string(client, version) + + # Don't just drop the connection - let the debugger close it after it finishes reading. + client.recv(1) + + elif response == ATCH: + debug_options = vspd.parse_debug_options(read_string(client)) + if redirect_output: + debug_options.add('RedirectOutput') + + if vspd.DETACHED: + write_bytes(client, ACPT) + try: + pid = os.getpid() + except AttributeError: + pid = 0 + write_int(client, pid) + + major, minor, micro, release_level, serial = sys.version_info + write_int(client, major) + write_int(client, minor) + write_int(client, micro) + + vspd.attach_process_from_socket(client, debug_options, report = True) + vspd.mark_all_threads_for_break(vspd.STEPPING_ATTACH_BREAK) + _attached.set() + client = None + else: + write_bytes(client, RJCT) + + elif response == REPL: + if not vspd.DETACHED: + write_bytes(client, ACPT) + vspd.connect_repl_using_socket(client) + client = None + else: + write_bytes(client, RJCT) + + except (socket.error, OSError): + pass + finally: + if client is not None: + client.close() + + server_thread = threading.Thread(target = server_thread_func) + server_thread.setDaemon(True) + server_thread.start() + + frames = [] + f = sys._getframe() + while True: + f = f.f_back + if f is None: + break + frames.append(f) + frames.reverse() + cur_thread = vspd.new_thread() + for f in frames: + cur_thread.push_frame(f) + def replace_trace_func(): + for f in frames: + f.f_trace = cur_thread.trace_func + replace_trace_func() + sys.settrace(cur_thread.trace_func) + vspd.intercept_threads(for_attach = True) + + +# Alias for convenience of users of pydevd +settrace = enable_attach + + +def wait_for_attach(timeout = None): + """If a PTVS remote debugger is attached, returns immediately. Otherwise, + blocks until a remote debugger attaches to this process, or until the + optional timeout occurs. + + Parameters + ---------- + timeout : float, optional + The timeout for the operation in seconds (or fractions thereof). + """ + if vspd.DETACHED: + _attached.clear() + _attached.wait(timeout) + + +def break_into_debugger(): + """If a PTVS remote debugger is attached, pauses execution of all threads, + and breaks into the debugger with current thread as active. + """ + if not vspd.DETACHED: + vspd.SEND_BREAK_COMPLETE = thread.get_ident() + vspd.mark_all_threads_for_break() + +def is_attached(): + """Returns ``True`` if debugger is attached, ``False`` otherwise.""" + return not vspd.DETACHED diff --git a/src/test/pythonFiles/folding/empty.py b/src/test/pythonFiles/folding/empty.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/folding/miscSamples.py b/src/test/pythonFiles/folding/miscSamples.py new file mode 100644 index 000000000000..01495fb0ee9c --- /dev/null +++ b/src/test/pythonFiles/folding/miscSamples.py @@ -0,0 +1,40 @@ + +def one(): + """comment""" + pass + +def two(): + value = """a doc string with single and double quotes "This is how it's done" """ + pass + +def three(): + """a doc string with single and double quotes "This is how it's done" + Another line + """ + pass + +def four(): + '''a doc string with single and double quotes "This is how it's done" ''' + pass + +def five(): + '''a doc string with single and double quotes "This is how it's done" + Another line + ''' + pass + +def six(): + """ s1 """ """ s2 """ + pass + +def seven(): + value = """ s1 """ """ s2 """ + pass + +def eight(): + ''' s1 ''' ''' s2 ''' + pass + +def nine(): + value = ''' s1 ''' ''' s2 ''' + pass diff --git a/src/test/pythonFiles/folding/noComments.py b/src/test/pythonFiles/folding/noComments.py new file mode 100644 index 000000000000..ca4d3f4140a6 --- /dev/null +++ b/src/test/pythonFiles/folding/noComments.py @@ -0,0 +1,278 @@ +__author__ = "Microsoft Corporation " +__version__ = "3.0.0.0" + +__all__ = ['enable_attach', 'wait_for_attach', 'break_into_debugger', 'settrace', 'is_attached', 'AttachAlreadyEnabledError'] + +import atexit +import getpass +import os +import os.path +import platform +import socket +import struct +import sys +import threading +try: + import thread +except ImportError: + import _thread as thread +try: + import ssl +except ImportError: + ssl = None + +import ptvsd.visualstudio_py_debugger as vspd +import ptvsd.visualstudio_py_repl as vspr +from ptvsd.visualstudio_py_util import to_bytes, read_bytes, read_int, read_string, write_bytes, write_int, write_string + +PTVS_VER = '2.2' +DEFAULT_PORT = 5678 +PTVSDBG_VER = 6 +PTVSDBG = to_bytes('PTVSDBG') +ACPT = to_bytes('ACPT') +RJCT = to_bytes('RJCT') +INFO = to_bytes('INFO') +ATCH = to_bytes('ATCH') +REPL = to_bytes('REPL') + +_attach_enabled = False +_attached = threading.Event() +vspd.DONT_DEBUG.append(os.path.normcase(__file__)) + + +class AttachAlreadyEnabledError(Exception): + """`ptvsd.enable_attach` has already been called in this process.""" + + +def enable_attach(secret, address = ('0.0.0.0', DEFAULT_PORT), certfile = None, keyfile = None, redirect_output = True): + """Enables Python Tools for Visual Studio to attach to this process remotely + to debug Python code. + + Parameters + ---------- + secret : str + Used to validate the clients - only those clients providing the valid + secret will be allowed to connect to this server. On client side, the + secret is prepended to the Qualifier string, separated from the + hostname by ``'@'``, e.g.: ``'secret@myhost.cloudapp.net:5678'``. If + secret is ``None``, there's no validation, and any client can connect + freely. + address : (str, int), optional + Specifies the interface and port on which the debugging server should + listen for TCP connections. It is in the same format as used for + regular sockets of the `socket.AF_INET` family, i.e. a tuple of + ``(hostname, port)``. On client side, the server is identified by the + Qualifier string in the usual ``'hostname:port'`` format, e.g.: + ``'myhost.cloudapp.net:5678'``. Default is ``('0.0.0.0', 5678)``. + certfile : str, optional + Used to enable SSL. If not specified, or if set to ``None``, the + connection between this program and the debugger will be unsecure, + and can be intercepted on the wire. If specified, the meaning of this + parameter is the same as for `ssl.wrap_socket`. + keyfile : str, optional + Used together with `certfile` when SSL is enabled. Its meaning is the + same as for ``ssl.wrap_socket``. + redirect_output : bool, optional + Specifies whether any output (on both `stdout` and `stderr`) produced + by this program should be sent to the debugger. Default is ``True``. + + Notes + ----- + This function returns immediately after setting up the debugging server, + and does not block program execution. If you need to block until debugger + is attached, call `ptvsd.wait_for_attach`. The debugger can be detached + and re-attached multiple times after `enable_attach` is called. + + This function can only be called once during the lifetime of the process. + On a second call, `AttachAlreadyEnabledError` is raised. In circumstances + where the caller does not control how many times the function will be + called (e.g. when a script with a single call is run more than once by + a hosting app or framework), the call should be wrapped in ``try..except``. + + Only the thread on which this function is called, and any threads that are + created after it returns, will be visible in the debugger once it is + attached. Any threads that are already running before this function is + called will not be visible. + """ + + if not ssl and (certfile or keyfile): + raise ValueError('could not import the ssl module - SSL is not supported on this version of Python') + + if sys.platform == 'cli': + import clr + x_tracing = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Tracing + x_frames = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Frames + if not x_tracing or not x_frames: + raise RuntimeError('IronPython must be started with -X:Tracing and -X:Frames options to support PTVS remote debugging.') + + global _attach_enabled + if _attach_enabled: + raise AttachAlreadyEnabledError('ptvsd.enable_attach() has already been called in this process.') + _attach_enabled = True + + atexit.register(vspd.detach_process_and_notify_debugger) + + server = socket.socket(proto=socket.IPPROTO_TCP) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind(address) + server.listen(1) + def server_thread_func(): + while True: + client = None + raw_client = None + try: + client, addr = server.accept() + if certfile: + client = ssl.wrap_socket(client, server_side = True, ssl_version = ssl.PROTOCOL_TLSv1, certfile = certfile, keyfile = keyfile) + write_bytes(client, PTVSDBG) + write_int(client, PTVSDBG_VER) + + response = read_bytes(client, 7) + if response != PTVSDBG: + continue + dbg_ver = read_int(client) + if dbg_ver != PTVSDBG_VER: + continue + + client_secret = read_string(client) + if secret is None or secret == client_secret: + write_bytes(client, ACPT) + else: + write_bytes(client, RJCT) + continue + + response = read_bytes(client, 4) + + if response == INFO: + try: + pid = os.getpid() + except AttributeError: + pid = 0 + write_int(client, pid) + + exe = sys.executable or '' + write_string(client, exe) + + try: + username = getpass.getuser() + except AttributeError: + username = '' + write_string(client, username) + + try: + impl = platform.python_implementation() + except AttributeError: + try: + impl = sys.implementation.name + except AttributeError: + impl = 'Python' + + major, minor, micro, release_level, serial = sys.version_info + + os_and_arch = platform.system() + if os_and_arch == "": + os_and_arch = sys.platform + try: + if sys.maxsize > 2**32: + os_and_arch += ' 64-bit' + else: + os_and_arch += ' 32-bit' + except AttributeError: + pass + + version = '%s %s.%s.%s (%s)' % (impl, major, minor, micro, os_and_arch) + write_string(client, version) + + client.recv(1) + + elif response == ATCH: + debug_options = vspd.parse_debug_options(read_string(client)) + if redirect_output: + debug_options.add('RedirectOutput') + + if vspd.DETACHED: + write_bytes(client, ACPT) + try: + pid = os.getpid() + except AttributeError: + pid = 0 + write_int(client, pid) + + major, minor, micro, release_level, serial = sys.version_info + write_int(client, major) + write_int(client, minor) + write_int(client, micro) + + vspd.attach_process_from_socket(client, debug_options, report = True) + vspd.mark_all_threads_for_break(vspd.STEPPING_ATTACH_BREAK) + _attached.set() + client = None + else: + write_bytes(client, RJCT) + + elif response == REPL: + if not vspd.DETACHED: + write_bytes(client, ACPT) + vspd.connect_repl_using_socket(client) + client = None + else: + write_bytes(client, RJCT) + + except (socket.error, OSError): + pass + finally: + if client is not None: + client.close() + + server_thread = threading.Thread(target = server_thread_func) + server_thread.setDaemon(True) + server_thread.start() + + frames = [] + f = sys._getframe() + while True: + f = f.f_back + if f is None: + break + frames.append(f) + frames.reverse() + cur_thread = vspd.new_thread() + for f in frames: + cur_thread.push_frame(f) + def replace_trace_func(): + for f in frames: + f.f_trace = cur_thread.trace_func + replace_trace_func() + sys.settrace(cur_thread.trace_func) + vspd.intercept_threads(for_attach = True) + + +settrace = enable_attach + + +def wait_for_attach(timeout = None): + """If a PTVS remote debugger is attached, returns immediately. Otherwise, + blocks until a remote debugger attaches to this process, or until the + optional timeout occurs. + + Parameters + ---------- + timeout : float, optional + The timeout for the operation in seconds (or fractions thereof). + """ + if vspd.DETACHED: + _attached.clear() + _attached.wait(timeout) + + +def break_into_debugger(): + """If a PTVS remote debugger is attached, pauses execution of all threads, + and breaks into the debugger with current thread as active. + """ + if not vspd.DETACHED: + vspd.SEND_BREAK_COMPLETE = thread.get_ident() + vspd.mark_all_threads_for_break() + +def is_attached(): + """Returns ``True`` if debugger is attached, ``False`` otherwise.""" + return not vspd.DETACHED diff --git a/src/test/pythonFiles/folding/noDocStrings.py b/src/test/pythonFiles/folding/noDocStrings.py new file mode 100644 index 000000000000..9fd4b4874a57 --- /dev/null +++ b/src/test/pythonFiles/folding/noDocStrings.py @@ -0,0 +1,266 @@ +# Python Tools for Visual Studio +# Copyright(c) Microsoft Corporation +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# +# See the Apache Version 2.0 License for specific language governing +# permissions and limitations under the License. + +__author__ = "Microsoft Corporation " +__version__ = "3.0.0.0" + +__all__ = ['enable_attach', 'wait_for_attach', 'break_into_debugger', 'settrace', 'is_attached', 'AttachAlreadyEnabledError'] + +import atexit +import getpass +import os +import os.path +import platform +import socket +import struct +import sys +import threading +try: + import thread +except ImportError: + import _thread as thread +try: + import ssl +except ImportError: + ssl = None + +import ptvsd.visualstudio_py_debugger as vspd +import ptvsd.visualstudio_py_repl as vspr +from ptvsd.visualstudio_py_util import to_bytes, read_bytes, read_int, read_string, write_bytes, write_int, write_string + + +# The server (i.e. the Python app) waits on a TCP port provided. Whenever anything connects to that port, +# it immediately sends the octet sequence 'PTVSDBG', followed by version number represented as int64, +# and then waits for the client to respond with the same exact byte sequence. After signatures are thereby +# exchanged and found to match, the client is expected to provide a string secret (in the usual debugger +# string format, None/ACII/Unicode prefix + length + data), which can be an empty string to designate the +# lack of a specified secret. +# +# If the secret does not match the one expected by the server, it responds with 'RJCT', and then closes +# the connection. Otherwise, the server responds with 'ACPT', and awaits a 4-octet command. The following +# commands are recognized: +# +# 'INFO' +# Report information about the process. The server responds with the following information, in order: +# - Process ID (int64) +# - Executable name (string) +# - User name (string) +# - Implementation name (string) +# and then immediately closes connection. Note, all string fields can be empty or null strings. +# +# 'ATCH' +# Attach debugger to the process. If successful, the server responds with 'ACPT', followed by process ID +# (int64), and then the Python language version that the server is running represented by three int64s - +# major, minor, micro; From there on the socket is assumed to be using the normal PTVS debugging protocol. +# If attaching was not successful (which can happen if some other debugger is already attached), the server +# responds with 'RJCT' and closes the connection. +# +# 'REPL' +# Attach REPL to the process. If successful, the server responds with 'ACPT', and from there on the socket +# is assumed to be using the normal PTVS REPL protocol. If not successful (which can happen if there is +# no debugger attached), the server responds with 'RJCT' and closes the connection. + +PTVS_VER = '2.2' +DEFAULT_PORT = 5678 +PTVSDBG_VER = 6 # must be kept in sync with DebuggerProtocolVersion in PythonRemoteProcess.cs +PTVSDBG = to_bytes('PTVSDBG') +ACPT = to_bytes('ACPT') +RJCT = to_bytes('RJCT') +INFO = to_bytes('INFO') +ATCH = to_bytes('ATCH') +REPL = to_bytes('REPL') + +_attach_enabled = False +_attached = threading.Event() +vspd.DONT_DEBUG.append(os.path.normcase(__file__)) + + +class AttachAlreadyEnabledError(Exception): + + +def enable_attach(secret, address = ('0.0.0.0', DEFAULT_PORT), certfile = None, keyfile = None, redirect_output = True): + if not ssl and (certfile or keyfile): + raise ValueError('could not import the ssl module - SSL is not supported on this version of Python') + + if sys.platform == 'cli': + # Check that IronPython was launched with -X:Frames and -X:Tracing, since we can't register our trace + # func on the thread that calls enable_attach otherwise + import clr + x_tracing = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Tracing + x_frames = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Frames + if not x_tracing or not x_frames: + raise RuntimeError('IronPython must be started with -X:Tracing and -X:Frames options to support PTVS remote debugging.') + + global _attach_enabled + if _attach_enabled: + raise AttachAlreadyEnabledError('ptvsd.enable_attach() has already been called in this process.') + _attach_enabled = True + + atexit.register(vspd.detach_process_and_notify_debugger) + + server = socket.socket(proto=socket.IPPROTO_TCP) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind(address) + server.listen(1) + def server_thread_func(): + while True: + client = None + raw_client = None + try: + client, addr = server.accept() + if certfile: + client = ssl.wrap_socket(client, server_side = True, ssl_version = ssl.PROTOCOL_TLSv1, certfile = certfile, keyfile = keyfile) + write_bytes(client, PTVSDBG) + write_int(client, PTVSDBG_VER) + + response = read_bytes(client, 7) + if response != PTVSDBG: + continue + dbg_ver = read_int(client) + if dbg_ver != PTVSDBG_VER: + continue + + client_secret = read_string(client) + if secret is None or secret == client_secret: + write_bytes(client, ACPT) + else: + write_bytes(client, RJCT) + continue + + response = read_bytes(client, 4) + + if response == INFO: + try: + pid = os.getpid() + except AttributeError: + pid = 0 + write_int(client, pid) + + exe = sys.executable or '' + write_string(client, exe) + + try: + username = getpass.getuser() + except AttributeError: + username = '' + write_string(client, username) + + try: + impl = platform.python_implementation() + except AttributeError: + try: + impl = sys.implementation.name + except AttributeError: + impl = 'Python' + + major, minor, micro, release_level, serial = sys.version_info + + os_and_arch = platform.system() + if os_and_arch == "": + os_and_arch = sys.platform + try: + if sys.maxsize > 2**32: + os_and_arch += ' 64-bit' + else: + os_and_arch += ' 32-bit' + except AttributeError: + pass + + version = '%s %s.%s.%s (%s)' % (impl, major, minor, micro, os_and_arch) + write_string(client, version) + + # Don't just drop the connection - let the debugger close it after it finishes reading. + client.recv(1) + + elif response == ATCH: + debug_options = vspd.parse_debug_options(read_string(client)) + if redirect_output: + debug_options.add('RedirectOutput') + + if vspd.DETACHED: + write_bytes(client, ACPT) + try: + pid = os.getpid() + except AttributeError: + pid = 0 + write_int(client, pid) + + major, minor, micro, release_level, serial = sys.version_info + write_int(client, major) + write_int(client, minor) + write_int(client, micro) + + vspd.attach_process_from_socket(client, debug_options, report = True) + vspd.mark_all_threads_for_break(vspd.STEPPING_ATTACH_BREAK) + _attached.set() + client = None + else: + write_bytes(client, RJCT) + + elif response == REPL: + if not vspd.DETACHED: + write_bytes(client, ACPT) + vspd.connect_repl_using_socket(client) + client = None + else: + write_bytes(client, RJCT) + + except (socket.error, OSError): + pass + finally: + if client is not None: + client.close() + + server_thread = threading.Thread(target = server_thread_func) + server_thread.setDaemon(True) + server_thread.start() + + frames = [] + f = sys._getframe() + while True: + f = f.f_back + if f is None: + break + frames.append(f) + frames.reverse() + cur_thread = vspd.new_thread() + for f in frames: + cur_thread.push_frame(f) + def replace_trace_func(): + for f in frames: + f.f_trace = cur_thread.trace_func + replace_trace_func() + sys.settrace(cur_thread.trace_func) + vspd.intercept_threads(for_attach = True) + + +# Alias for convenience of users of pydevd +settrace = enable_attach + + +def wait_for_attach(timeout = None): + if vspd.DETACHED: + _attached.clear() + _attached.wait(timeout) + + +def break_into_debugger(): + if not vspd.DETACHED: + vspd.SEND_BREAK_COMPLETE = thread.get_ident() + vspd.mark_all_threads_for_break() + +def is_attached(): + return not vspd.DETACHED diff --git a/src/test/pythonFiles/folding/visualstudio_ipython_repl.py b/src/test/pythonFiles/folding/visualstudio_ipython_repl.py new file mode 100644 index 000000000000..33aa109de971 --- /dev/null +++ b/src/test/pythonFiles/folding/visualstudio_ipython_repl.py @@ -0,0 +1,430 @@ +# Python Tools for Visual Studio +# Copyright(c) Microsoft Corporation +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# +# See the Apache Version 2.0 License for specific language governing +# permissions and limitations under the License. + +"""Implements REPL support over IPython/ZMQ for VisualStudio""" + +__author__ = "Microsoft Corporation " +__version__ = "3.0.0.0" + +import re +import sys +from visualstudio_py_repl import BasicReplBackend, ReplBackend, UnsupportedReplException, _command_line_to_args_list +from visualstudio_py_util import to_bytes +try: + import thread +except: + import _thread as thread # Renamed as Py3k + +from base64 import decodestring + +try: + import IPython +except ImportError: + exc_value = sys.exc_info()[1] + raise UnsupportedReplException('IPython mode requires IPython 0.11 or later: ' + str(exc_value)) + +def is_ipython_versionorgreater(major, minor): + """checks if we are at least a specific IPython version""" + match = re.match('(\d+).(\d+)', IPython.__version__) + if match: + groups = match.groups() + if int(groups[0]) > major: + return True + elif int(groups[0]) == major: + return int(groups[1]) >= minor + + return False + +remove_escapes = re.compile(r'\x1b[^m]*m') + +try: + if is_ipython_versionorgreater(3, 0): + from IPython.kernel import KernelManager + from IPython.kernel.channels import HBChannel + from IPython.kernel.threaded import (ThreadedZMQSocketChannel, ThreadedKernelClient as KernelClient) + ShellChannel = StdInChannel = IOPubChannel = ThreadedZMQSocketChannel + elif is_ipython_versionorgreater(1, 0): + from IPython.kernel import KernelManager, KernelClient + from IPython.kernel.channels import ShellChannel, HBChannel, StdInChannel, IOPubChannel + else: + import IPython.zmq + KernelClient = object # was split out from KernelManager in 1.0 + from IPython.zmq.kernelmanager import (KernelManager, + ShellSocketChannel as ShellChannel, + SubSocketChannel as IOPubChannel, + StdInSocketChannel as StdInChannel, + HBSocketChannel as HBChannel) + + from IPython.utils.traitlets import Type +except ImportError: + exc_value = sys.exc_info()[1] + raise UnsupportedReplException(str(exc_value)) + + +# TODO: SystemExit exceptions come back to us as strings, can we automatically exit when ones raised somehow? + +##### +# Channels which forward events + +# Description of the messaging protocol +# http://ipython.scipy.org/doc/manual/html/development/messaging.html + + +class DefaultHandler(object): + def unknown_command(self, content): + import pprint + print('unknown command ' + str(type(self))) + pprint.pprint(content) + + def call_handlers(self, msg): + # msg_type: + # execute_reply + msg_type = 'handle_' + msg['msg_type'] + + getattr(self, msg_type, self.unknown_command)(msg['content']) + +class VsShellChannel(DefaultHandler, ShellChannel): + + def handle_execute_reply(self, content): + # we could have a payload here... + payload = content['payload'] + + for item in payload: + data = item.get('data') + if data is not None: + try: + # Could be named km.sub_channel for very old IPython, but + # those versions should not put 'data' in this payload + write_data = self._vs_backend.km.iopub_channel.write_data + except AttributeError: + pass + else: + write_data(data) + continue + + output = item.get('text', None) + if output is not None: + self._vs_backend.write_stdout(output) + self._vs_backend.send_command_executed() + + def handle_inspect_reply(self, content): + self.handle_object_info_reply(content) + + def handle_object_info_reply(self, content): + self._vs_backend.object_info_reply = content + self._vs_backend.members_lock.release() + + def handle_complete_reply(self, content): + self._vs_backend.complete_reply = content + self._vs_backend.members_lock.release() + + def handle_kernel_info_reply(self, content): + self._vs_backend.write_stdout(content['banner']) + + +class VsIOPubChannel(DefaultHandler, IOPubChannel): + def call_handlers(self, msg): + # only output events from our session or no sessions + # https://pytools.codeplex.com/workitem/1622 + parent = msg.get('parent_header') + if not parent or parent.get('session') == self.session.session: + msg_type = 'handle_' + msg['msg_type'] + getattr(self, msg_type, self.unknown_command)(msg['content']) + + def handle_display_data(self, content): + # called when user calls display() + data = content.get('data', None) + + if data is not None: + self.write_data(data) + + def handle_stream(self, content): + stream_name = content['name'] + if is_ipython_versionorgreater(3, 0): + output = content['text'] + else: + output = content['data'] + if stream_name == 'stdout': + self._vs_backend.write_stdout(output) + elif stream_name == 'stderr': + self._vs_backend.write_stderr(output) + # TODO: stdin can show up here, do we echo that? + + def handle_execute_result(self, content): + self.handle_execute_output(content) + + def handle_execute_output(self, content): + # called when an expression statement is printed, we treat + # identical to stream output but it always goes to stdout + output = content['data'] + execution_count = content['execution_count'] + self._vs_backend.execution_count = execution_count + 1 + self._vs_backend.send_prompt( + '\r\nIn [%d]: ' % (execution_count + 1), + ' ' + ('.' * (len(str(execution_count + 1)) + 2)) + ': ', + allow_multiple_statements=True + ) + self.write_data(output, execution_count) + + def write_data(self, data, execution_count = None): + output_xaml = data.get('application/xaml+xml', None) + if output_xaml is not None: + try: + if isinstance(output_xaml, str) and sys.version_info[0] >= 3: + output_xaml = output_xaml.encode('ascii') + self._vs_backend.write_xaml(decodestring(output_xaml)) + self._vs_backend.write_stdout('\n') + return + except: + pass + + output_png = data.get('image/png', None) + if output_png is not None: + try: + if isinstance(output_png, str) and sys.version_info[0] >= 3: + output_png = output_png.encode('ascii') + self._vs_backend.write_png(decodestring(output_png)) + self._vs_backend.write_stdout('\n') + return + except: + pass + + output_str = data.get('text/plain', None) + if output_str is not None: + if execution_count is not None: + if '\n' in output_str: + output_str = '\n' + output_str + output_str = 'Out[' + str(execution_count) + ']: ' + output_str + + self._vs_backend.write_stdout(output_str) + self._vs_backend.write_stdout('\n') + return + + def handle_error(self, content): + # TODO: this includes escape sequences w/ color, we need to unescape that + ename = content['ename'] + evalue = content['evalue'] + tb = content['traceback'] + self._vs_backend.write_stderr('\n'.join(tb)) + self._vs_backend.write_stdout('\n') + + def handle_execute_input(self, content): + # just a rebroadcast of the command to be executed, can be ignored + self._vs_backend.execution_count += 1 + self._vs_backend.send_prompt( + '\r\nIn [%d]: ' % (self._vs_backend.execution_count), + ' ' + ('.' * (len(str(self._vs_backend.execution_count)) + 2)) + ': ', + allow_multiple_statements=True + ) + pass + + def handle_status(self, content): + pass + + # Backwards compat w/ 0.13 + handle_pyin = handle_execute_input + handle_pyout = handle_execute_output + handle_pyerr = handle_error + + +class VsStdInChannel(DefaultHandler, StdInChannel): + def handle_input_request(self, content): + # queue this to another thread so we don't block the channel + def read_and_respond(): + value = self._vs_backend.read_line() + + self.input(value) + + thread.start_new_thread(read_and_respond, ()) + + +class VsHBChannel(DefaultHandler, HBChannel): + pass + + +class VsKernelManager(KernelManager, KernelClient): + shell_channel_class = Type(VsShellChannel) + if is_ipython_versionorgreater(1, 0): + iopub_channel_class = Type(VsIOPubChannel) + else: + sub_channel_class = Type(VsIOPubChannel) + stdin_channel_class = Type(VsStdInChannel) + hb_channel_class = Type(VsHBChannel) + + +class IPythonBackend(ReplBackend): + def __init__(self, mod_name = '__main__', launch_file = None): + ReplBackend.__init__(self) + self.launch_file = launch_file + self.mod_name = mod_name + self.km = VsKernelManager() + + if is_ipython_versionorgreater(0, 13): + # http://pytools.codeplex.com/workitem/759 + # IPython stopped accepting the ipython flag and switched to launcher, the new + # default is what we want though. + self.km.start_kernel(**{'extra_arguments': self.get_extra_arguments()}) + else: + self.km.start_kernel(**{'ipython': True, 'extra_arguments': self.get_extra_arguments()}) + self.km.start_channels() + self.exit_lock = thread.allocate_lock() + self.exit_lock.acquire() # used as an event + self.members_lock = thread.allocate_lock() + self.members_lock.acquire() + + self.km.shell_channel._vs_backend = self + self.km.stdin_channel._vs_backend = self + if is_ipython_versionorgreater(1, 0): + self.km.iopub_channel._vs_backend = self + else: + self.km.sub_channel._vs_backend = self + self.km.hb_channel._vs_backend = self + self.execution_count = 1 + + def get_extra_arguments(self): + if sys.version <= '2.': + return [unicode('--pylab=inline')] + return ['--pylab=inline'] + + def execute_file_as_main(self, filename, arg_string): + f = open(filename, 'rb') + try: + contents = f.read().replace(to_bytes("\r\n"), to_bytes("\n")) + finally: + f.close() + args = [filename] + _command_line_to_args_list(arg_string) + code = ''' +import sys +sys.argv = %(args)r +__file__ = %(filename)r +del sys +exec(compile(%(contents)r, %(filename)r, 'exec')) +''' % {'filename' : filename, 'contents':contents, 'args': args} + + self.run_command(code, True) + + def execution_loop(self): + # we've got a bunch of threads setup for communication, we just block + # here until we're requested to exit. + self.send_prompt('\r\nIn [1]: ', ' ...: ', allow_multiple_statements=True) + self.exit_lock.acquire() + + def run_command(self, command, silent = False): + if is_ipython_versionorgreater(3, 0): + self.km.execute(command, silent) + else: + self.km.shell_channel.execute(command, silent) + + def execute_file_ex(self, filetype, filename, args): + if filetype == 'script': + self.execute_file_as_main(filename, args) + else: + raise NotImplementedError("Cannot execute %s file" % filetype) + + def exit_process(self): + self.exit_lock.release() + + def get_members(self, expression): + """returns a tuple of the type name, instance members, and type members""" + text = expression + '.' + if is_ipython_versionorgreater(3, 0): + self.km.complete(text) + else: + self.km.shell_channel.complete(text, text, 1) + + self.members_lock.acquire() + + reply = self.complete_reply + + res = {} + text_len = len(text) + for member in reply['matches']: + res[member[text_len:]] = 'object' + + return ('unknown', res, {}) + + def get_signatures(self, expression): + """returns doc, args, vargs, varkw, defaults.""" + + if is_ipython_versionorgreater(3, 0): + self.km.inspect(expression, None, 2) + else: + self.km.shell_channel.object_info(expression) + + self.members_lock.acquire() + + reply = self.object_info_reply + if is_ipython_versionorgreater(3, 0): + data = reply['data'] + text = data['text/plain'] + text = remove_escapes.sub('', text) + return [(text, (), None, None, [])] + else: + argspec = reply['argspec'] + defaults = argspec['defaults'] + if defaults is not None: + defaults = [repr(default) for default in defaults] + else: + defaults = [] + return [(reply['docstring'], argspec['args'], argspec['varargs'], argspec['varkw'], defaults)] + + def interrupt_main(self): + """aborts the current running command""" + self.km.interrupt_kernel() + + def set_current_module(self, module): + pass + + def get_module_names(self): + """returns a list of module names""" + return [] + + def flush(self): + pass + + def init_debugger(self): + from os import path + self.run_command(''' +def __visualstudio_debugger_init(): + import sys + sys.path.append(''' + repr(path.dirname(__file__)) + ''') + import visualstudio_py_debugger + new_thread = visualstudio_py_debugger.new_thread() + sys.settrace(new_thread.trace_func) + visualstudio_py_debugger.intercept_threads(True) + +__visualstudio_debugger_init() +del __visualstudio_debugger_init +''', True) + + def attach_process(self, port, debugger_id): + self.run_command(''' +def __visualstudio_debugger_attach(): + import visualstudio_py_debugger + + def do_detach(): + visualstudio_py_debugger.DETACH_CALLBACKS.remove(do_detach) + + visualstudio_py_debugger.DETACH_CALLBACKS.append(do_detach) + visualstudio_py_debugger.attach_process(''' + str(port) + ''', ''' + repr(debugger_id) + ''', report = True, block = True) + +__visualstudio_debugger_attach() +del __visualstudio_debugger_attach +''', True) + +class IPythonBackendWithoutPyLab(IPythonBackend): + def get_extra_arguments(self): + return [] \ No newline at end of file diff --git a/src/test/pythonFiles/folding/visualstudio_ipython_repl_double_quotes.py b/src/test/pythonFiles/folding/visualstudio_ipython_repl_double_quotes.py new file mode 100644 index 000000000000..473046639147 --- /dev/null +++ b/src/test/pythonFiles/folding/visualstudio_ipython_repl_double_quotes.py @@ -0,0 +1,430 @@ +# Python Tools for Visual Studio +# Copyright(c) Microsoft Corporation +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# +# See the Apache Version 2.0 License for specific language governing +# permissions and limitations under the License. + +"""Implements REPL support over IPython/ZMQ for VisualStudio""" + +__author__ = "Microsoft Corporation " +__version__ = "3.0.0.0" + +import re +import sys +from visualstudio_py_repl import BasicReplBackend, ReplBackend, UnsupportedReplException, _command_line_to_args_list +from visualstudio_py_util import to_bytes +try: + import thread +except: + import _thread as thread # Renamed as Py3k + +from base64 import decodestring + +try: + import IPython +except ImportError: + exc_value = sys.exc_info()[1] + raise UnsupportedReplException('IPython mode requires IPython 0.11 or later: ' + str(exc_value)) + +def is_ipython_versionorgreater(major, minor): + """checks if we are at least a specific IPython version""" + match = re.match('(\d+).(\d+)', IPython.__version__) + if match: + groups = match.groups() + if int(groups[0]) > major: + return True + elif int(groups[0]) == major: + return int(groups[1]) >= minor + + return False + +remove_escapes = re.compile(r'\x1b[^m]*m') + +try: + if is_ipython_versionorgreater(3, 0): + from IPython.kernel import KernelManager + from IPython.kernel.channels import HBChannel + from IPython.kernel.threaded import (ThreadedZMQSocketChannel, ThreadedKernelClient as KernelClient) + ShellChannel = StdInChannel = IOPubChannel = ThreadedZMQSocketChannel + elif is_ipython_versionorgreater(1, 0): + from IPython.kernel import KernelManager, KernelClient + from IPython.kernel.channels import ShellChannel, HBChannel, StdInChannel, IOPubChannel + else: + import IPython.zmq + KernelClient = object # was split out from KernelManager in 1.0 + from IPython.zmq.kernelmanager import (KernelManager, + ShellSocketChannel as ShellChannel, + SubSocketChannel as IOPubChannel, + StdInSocketChannel as StdInChannel, + HBSocketChannel as HBChannel) + + from IPython.utils.traitlets import Type +except ImportError: + exc_value = sys.exc_info()[1] + raise UnsupportedReplException(str(exc_value)) + + +# TODO: SystemExit exceptions come back to us as strings, can we automatically exit when ones raised somehow? + +##### +# Channels which forward events + +# Description of the messaging protocol +# http://ipython.scipy.org/doc/manual/html/development/messaging.html + + +class DefaultHandler(object): + def unknown_command(self, content): + import pprint + print('unknown command ' + str(type(self))) + pprint.pprint(content) + + def call_handlers(self, msg): + # msg_type: + # execute_reply + msg_type = 'handle_' + msg['msg_type'] + + getattr(self, msg_type, self.unknown_command)(msg['content']) + +class VsShellChannel(DefaultHandler, ShellChannel): + + def handle_execute_reply(self, content): + # we could have a payload here... + payload = content['payload'] + + for item in payload: + data = item.get('data') + if data is not None: + try: + # Could be named km.sub_channel for very old IPython, but + # those versions should not put 'data' in this payload + write_data = self._vs_backend.km.iopub_channel.write_data + except AttributeError: + pass + else: + write_data(data) + continue + + output = item.get('text', None) + if output is not None: + self._vs_backend.write_stdout(output) + self._vs_backend.send_command_executed() + + def handle_inspect_reply(self, content): + self.handle_object_info_reply(content) + + def handle_object_info_reply(self, content): + self._vs_backend.object_info_reply = content + self._vs_backend.members_lock.release() + + def handle_complete_reply(self, content): + self._vs_backend.complete_reply = content + self._vs_backend.members_lock.release() + + def handle_kernel_info_reply(self, content): + self._vs_backend.write_stdout(content['banner']) + + +class VsIOPubChannel(DefaultHandler, IOPubChannel): + def call_handlers(self, msg): + # only output events from our session or no sessions + # https://pytools.codeplex.com/workitem/1622 + parent = msg.get('parent_header') + if not parent or parent.get('session') == self.session.session: + msg_type = 'handle_' + msg['msg_type'] + getattr(self, msg_type, self.unknown_command)(msg['content']) + + def handle_display_data(self, content): + # called when user calls display() + data = content.get('data', None) + + if data is not None: + self.write_data(data) + + def handle_stream(self, content): + stream_name = content['name'] + if is_ipython_versionorgreater(3, 0): + output = content['text'] + else: + output = content['data'] + if stream_name == 'stdout': + self._vs_backend.write_stdout(output) + elif stream_name == 'stderr': + self._vs_backend.write_stderr(output) + # TODO: stdin can show up here, do we echo that? + + def handle_execute_result(self, content): + self.handle_execute_output(content) + + def handle_execute_output(self, content): + # called when an expression statement is printed, we treat + # identical to stream output but it always goes to stdout + output = content['data'] + execution_count = content['execution_count'] + self._vs_backend.execution_count = execution_count + 1 + self._vs_backend.send_prompt( + '\r\nIn [%d]: ' % (execution_count + 1), + ' ' + ('.' * (len(str(execution_count + 1)) + 2)) + ': ', + allow_multiple_statements=True + ) + self.write_data(output, execution_count) + + def write_data(self, data, execution_count = None): + output_xaml = data.get('application/xaml+xml', None) + if output_xaml is not None: + try: + if isinstance(output_xaml, str) and sys.version_info[0] >= 3: + output_xaml = output_xaml.encode('ascii') + self._vs_backend.write_xaml(decodestring(output_xaml)) + self._vs_backend.write_stdout('\n') + return + except: + pass + + output_png = data.get('image/png', None) + if output_png is not None: + try: + if isinstance(output_png, str) and sys.version_info[0] >= 3: + output_png = output_png.encode('ascii') + self._vs_backend.write_png(decodestring(output_png)) + self._vs_backend.write_stdout('\n') + return + except: + pass + + output_str = data.get('text/plain', None) + if output_str is not None: + if execution_count is not None: + if '\n' in output_str: + output_str = '\n' + output_str + output_str = 'Out[' + str(execution_count) + ']: ' + output_str + + self._vs_backend.write_stdout(output_str) + self._vs_backend.write_stdout('\n') + return + + def handle_error(self, content): + # TODO: this includes escape sequences w/ color, we need to unescape that + ename = content['ename'] + evalue = content['evalue'] + tb = content['traceback'] + self._vs_backend.write_stderr('\n'.join(tb)) + self._vs_backend.write_stdout('\n') + + def handle_execute_input(self, content): + # just a rebroadcast of the command to be executed, can be ignored + self._vs_backend.execution_count += 1 + self._vs_backend.send_prompt( + '\r\nIn [%d]: ' % (self._vs_backend.execution_count), + ' ' + ('.' * (len(str(self._vs_backend.execution_count)) + 2)) + ': ', + allow_multiple_statements=True + ) + pass + + def handle_status(self, content): + pass + + # Backwards compat w/ 0.13 + handle_pyin = handle_execute_input + handle_pyout = handle_execute_output + handle_pyerr = handle_error + + +class VsStdInChannel(DefaultHandler, StdInChannel): + def handle_input_request(self, content): + # queue this to another thread so we don't block the channel + def read_and_respond(): + value = self._vs_backend.read_line() + + self.input(value) + + thread.start_new_thread(read_and_respond, ()) + + +class VsHBChannel(DefaultHandler, HBChannel): + pass + + +class VsKernelManager(KernelManager, KernelClient): + shell_channel_class = Type(VsShellChannel) + if is_ipython_versionorgreater(1, 0): + iopub_channel_class = Type(VsIOPubChannel) + else: + sub_channel_class = Type(VsIOPubChannel) + stdin_channel_class = Type(VsStdInChannel) + hb_channel_class = Type(VsHBChannel) + + +class IPythonBackend(ReplBackend): + def __init__(self, mod_name = '__main__', launch_file = None): + ReplBackend.__init__(self) + self.launch_file = launch_file + self.mod_name = mod_name + self.km = VsKernelManager() + + if is_ipython_versionorgreater(0, 13): + # http://pytools.codeplex.com/workitem/759 + # IPython stopped accepting the ipython flag and switched to launcher, the new + # default is what we want though. + self.km.start_kernel(**{'extra_arguments': self.get_extra_arguments()}) + else: + self.km.start_kernel(**{'ipython': True, 'extra_arguments': self.get_extra_arguments()}) + self.km.start_channels() + self.exit_lock = thread.allocate_lock() + self.exit_lock.acquire() # used as an event + self.members_lock = thread.allocate_lock() + self.members_lock.acquire() + + self.km.shell_channel._vs_backend = self + self.km.stdin_channel._vs_backend = self + if is_ipython_versionorgreater(1, 0): + self.km.iopub_channel._vs_backend = self + else: + self.km.sub_channel._vs_backend = self + self.km.hb_channel._vs_backend = self + self.execution_count = 1 + + def get_extra_arguments(self): + if sys.version <= '2.': + return [unicode('--pylab=inline')] + return ['--pylab=inline'] + + def execute_file_as_main(self, filename, arg_string): + f = open(filename, 'rb') + try: + contents = f.read().replace(to_bytes("\r\n"), to_bytes("\n")) + finally: + f.close() + args = [filename] + _command_line_to_args_list(arg_string) + code = """ +import sys +sys.argv = %(args)r +__file__ = %(filename)r +del sys +exec(compile(%(contents)r, %(filename)r, 'exec')) +""" % {'filename' : filename, 'contents':contents, 'args': args} + + self.run_command(code, True) + + def execution_loop(self): + # we've got a bunch of threads setup for communication, we just block + # here until we're requested to exit. + self.send_prompt('\r\nIn [1]: ', ' ...: ', allow_multiple_statements=True) + self.exit_lock.acquire() + + def run_command(self, command, silent = False): + if is_ipython_versionorgreater(3, 0): + self.km.execute(command, silent) + else: + self.km.shell_channel.execute(command, silent) + + def execute_file_ex(self, filetype, filename, args): + if filetype == 'script': + self.execute_file_as_main(filename, args) + else: + raise NotImplementedError("Cannot execute %s file" % filetype) + + def exit_process(self): + self.exit_lock.release() + + def get_members(self, expression): + """returns a tuple of the type name, instance members, and type members""" + text = expression + '.' + if is_ipython_versionorgreater(3, 0): + self.km.complete(text) + else: + self.km.shell_channel.complete(text, text, 1) + + self.members_lock.acquire() + + reply = self.complete_reply + + res = {} + text_len = len(text) + for member in reply['matches']: + res[member[text_len:]] = 'object' + + return ('unknown', res, {}) + + def get_signatures(self, expression): + """returns doc, args, vargs, varkw, defaults.""" + + if is_ipython_versionorgreater(3, 0): + self.km.inspect(expression, None, 2) + else: + self.km.shell_channel.object_info(expression) + + self.members_lock.acquire() + + reply = self.object_info_reply + if is_ipython_versionorgreater(3, 0): + data = reply['data'] + text = data['text/plain'] + text = remove_escapes.sub('', text) + return [(text, (), None, None, [])] + else: + argspec = reply['argspec'] + defaults = argspec['defaults'] + if defaults is not None: + defaults = [repr(default) for default in defaults] + else: + defaults = [] + return [(reply['docstring'], argspec['args'], argspec['varargs'], argspec['varkw'], defaults)] + + def interrupt_main(self): + """aborts the current running command""" + self.km.interrupt_kernel() + + def set_current_module(self, module): + pass + + def get_module_names(self): + """returns a list of module names""" + return [] + + def flush(self): + pass + + def init_debugger(self): + from os import path + self.run_command(""" +def __visualstudio_debugger_init(): + import sys + sys.path.append(""" + repr(path.dirname(__file__)) + """) + import visualstudio_py_debugger + new_thread = visualstudio_py_debugger.new_thread() + sys.settrace(new_thread.trace_func) + visualstudio_py_debugger.intercept_threads(True) + +__visualstudio_debugger_init() +del __visualstudio_debugger_init +""", True) + + def attach_process(self, port, debugger_id): + self.run_command(""" +def __visualstudio_debugger_attach(): + import visualstudio_py_debugger + + def do_detach(): + visualstudio_py_debugger.DETACH_CALLBACKS.remove(do_detach) + + visualstudio_py_debugger.DETACH_CALLBACKS.append(do_detach) + visualstudio_py_debugger.attach_process(""" + str(port) + """, """ + repr(debugger_id) + """, report = True, block = True) + +__visualstudio_debugger_attach() +del __visualstudio_debugger_attach +""", True) + +class IPythonBackendWithoutPyLab(IPythonBackend): + def get_extra_arguments(self): + return [] diff --git a/src/test/pythonFiles/folding/visualstudio_py_debugger.py b/src/test/pythonFiles/folding/visualstudio_py_debugger.py new file mode 100644 index 000000000000..ec18ff8c63b0 --- /dev/null +++ b/src/test/pythonFiles/folding/visualstudio_py_debugger.py @@ -0,0 +1,644 @@ +# Python Tools for Visual Studio +# Copyright(c) Microsoft Corporation +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# +# See the Apache Version 2.0 License for specific language governing +# permissions and limitations under the License. +# With number of modifications by Don Jayamanne + +from __future__ import with_statement + +__author__ = "Microsoft Corporation " +__version__ = "3.0.0.0" + +# This module MUST NOT import threading in global scope. This is because in a direct (non-ptvsd) +# attach scenario, it is loaded on the injected debugger attach thread, and if threading module +# hasn't been loaded already, it will assume that the thread on which it is being loaded is the +# main thread. This will cause issues when the thread goes away after attach completes. +_threading = None + +import sys +import ctypes +try: + import thread +except ImportError: + import _thread as thread +import socket +import struct +import weakref +import traceback +import types +import bisect +from os import path +import ntpath +import runpy +import datetime +from codecs import BOM_UTF8 + +try: + # In the local attach scenario, visualstudio_py_util is injected into globals() + # by PyDebugAttach before loading this module, and cannot be imported. + _vspu = visualstudio_py_util +except: + try: + import visualstudio_py_util as _vspu + except ImportError: + import ptvsd.visualstudio_py_util as _vspu + +to_bytes = _vspu.to_bytes +exec_file = _vspu.exec_file +exec_module = _vspu.exec_module +exec_code = _vspu.exec_code +read_bytes = _vspu.read_bytes +read_int = _vspu.read_int +read_string = _vspu.read_string +write_bytes = _vspu.write_bytes +write_int = _vspu.write_int +write_string = _vspu.write_string +safe_repr = _vspu.SafeRepr() + +try: + # In the local attach scenario, visualstudio_py_repl is injected into globals() + # by PyDebugAttach before loading this module, and cannot be imported. + _vspr = visualstudio_py_repl +except: + try: + import visualstudio_py_repl as _vspr + except ImportError: + import ptvsd.visualstudio_py_repl as _vspr + +try: + import stackless +except ImportError: + stackless = None + +try: + xrange +except: + xrange = range + +if sys.platform == 'cli': + import clr + from System.Runtime.CompilerServices import ConditionalWeakTable + IPY_SEEN_MODULES = ConditionalWeakTable[object, object]() + +# Import encodings early to avoid import on the debugger thread, which may cause deadlock +from encodings import utf_8 + +# WARNING: Avoid imports beyond this point, specifically on the debugger thread, as this may cause +# deadlock where the debugger thread performs an import while a user thread has the import lock + +# save start_new_thread so we can call it later, we'll intercept others calls to it. + +debugger_dll_handle = None +DETACHED = True +def thread_creator(func, args, kwargs = {}, *extra_args): + if not isinstance(args, tuple): + # args is not a tuple. This may be because we have become bound to a + # class, which has offset our arguments by one. + if isinstance(kwargs, tuple): + func, args = args, kwargs + kwargs = extra_args[0] if len(extra_args) > 0 else {} + + return _start_new_thread(new_thread_wrapper, (func, args, kwargs)) + +_start_new_thread = thread.start_new_thread +THREADS = {} +THREADS_LOCK = thread.allocate_lock() +MODULES = [] + +BREAK_ON_SYSTEMEXIT_ZERO = False +DEBUG_STDLIB = False +DJANGO_DEBUG = False + +RICH_EXCEPTIONS = False +IGNORE_DJANGO_TEMPLATE_WARNINGS = False + +# Py3k compat - alias unicode to str +try: + unicode +except: + unicode = str + +# A value of a synthesized child. The string is passed through to the variable list, and type is not displayed at all. +class SynthesizedValue(object): + def __init__(self, repr_value='', len_value=None): + self.repr_value = repr_value + self.len_value = len_value + def __repr__(self): + return self.repr_value + def __len__(self): + return self.len_value + +# Specifies list of files not to debug. Can be extended by other modules +# (the REPL does this for $attach support and not stepping into the REPL). +DONT_DEBUG = [path.normcase(__file__), path.normcase(_vspu.__file__)] +if sys.version_info >= (3, 3): + DONT_DEBUG.append(path.normcase('')) +if sys.version_info >= (3, 5): + DONT_DEBUG.append(path.normcase('')) + +# Contains information about all breakpoints in the process. Keys are line numbers on which +# there are breakpoints in any file, and values are dicts. For every line number, the +# corresponding dict contains all the breakpoints that fall on that line. The keys in that +# dict are tuples of the form (filename, breakpoint_id), each entry representing a single +# breakpoint, and values are BreakpointInfo objects. +# +# For example, given the following breakpoints: +# +# 1. In 'main.py' at line 10. +# 2. In 'main.py' at line 20. +# 3. In 'module.py' at line 10. +# +# the contents of BREAKPOINTS would be: +# {10: {('main.py', 1): ..., ('module.py', 3): ...}, 20: {('main.py', 2): ... }} +BREAKPOINTS = {} + +# Contains information about all pending (i.e. not yet bound) breakpoints in the process. +# Elements are BreakpointInfo objects. +PENDING_BREAKPOINTS = set() + +# Must be in sync with enum PythonBreakpointConditionKind in PythonBreakpoint.cs +BREAKPOINT_CONDITION_ALWAYS = 0 +BREAKPOINT_CONDITION_WHEN_TRUE = 1 +BREAKPOINT_CONDITION_WHEN_CHANGED = 2 + +# Must be in sync with enum PythonBreakpointPassCountKind in PythonBreakpoint.cs +BREAKPOINT_PASS_COUNT_ALWAYS = 0 +BREAKPOINT_PASS_COUNT_EVERY = 1 +BREAKPOINT_PASS_COUNT_WHEN_EQUAL = 2 +BREAKPOINT_PASS_COUNT_WHEN_EQUAL_OR_GREATER = 3 + +## Begin modification by Don Jayamanne +DJANGO_VERSIONS_IDENTIFIED = False +IS_DJANGO18 = False +IS_DJANGO19 = False +IS_DJANGO19_OR_HIGHER = False + +try: + dict_contains = dict.has_key +except: + try: + #Py3k does not have has_key anymore, and older versions don't have __contains__ + dict_contains = dict.__contains__ + except: + try: + dict_contains = dict.has_key + except NameError: + def dict_contains(d, key): + return d.has_key(key) +## End modification by Don Jayamanne + +class BreakpointInfo(object): + __slots__ = [ + 'breakpoint_id', 'filename', 'lineno', 'condition_kind', 'condition', + 'pass_count_kind', 'pass_count', 'is_bound', 'last_condition_value', + 'hit_count' + ] + + # For "when changed" breakpoints, this is used as the initial value of last_condition_value, + # such that it is guaranteed to not compare equal to any other value that it will get later. + _DUMMY_LAST_VALUE = object() + + def __init__(self, breakpoint_id, filename, lineno, condition_kind, condition, pass_count_kind, pass_count): + self.breakpoint_id = breakpoint_id + self.filename = filename + self.lineno = lineno + self.condition_kind = condition_kind + self.condition = condition + self.pass_count_kind = pass_count_kind + self.pass_count = pass_count + self.is_bound = False + self.last_condition_value = BreakpointInfo._DUMMY_LAST_VALUE + self.hit_count = 0 + + @staticmethod + def find_by_id(breakpoint_id): + for line, bp_dict in BREAKPOINTS.items(): + for (filename, bp_id), bp in bp_dict.items(): + if bp_id == breakpoint_id: + return bp + return None + +# lock for calling .send on the socket +send_lock = thread.allocate_lock() + +class _SendLockContextManager(object): + """context manager for send lock. Handles both acquiring/releasing the + send lock as well as detaching the debugger if the remote process + is disconnected""" + + def __enter__(self): + # mark that we're about to do socket I/O so we won't deliver + # debug events when we're debugging the standard library + cur_thread = get_thread_from_id(thread.get_ident()) + if cur_thread is not None: + cur_thread.is_sending = True + + send_lock.acquire() + + def __exit__(self, exc_type, exc_value, tb): + send_lock.release() + + # start sending debug events again + cur_thread = get_thread_from_id(thread.get_ident()) + if cur_thread is not None: + cur_thread.is_sending = False + + if exc_type is not None: + detach_threads() + detach_process() + # swallow the exception, we're no longer debugging + return True + +_SendLockCtx = _SendLockContextManager() + +SEND_BREAK_COMPLETE = False + +STEPPING_OUT = -1 # first value, we decrement below this +STEPPING_NONE = 0 +STEPPING_BREAK = 1 +STEPPING_LAUNCH_BREAK = 2 +STEPPING_ATTACH_BREAK = 3 +STEPPING_INTO = 4 +STEPPING_OVER = 5 # last value, we increment past this. + +USER_STEPPING = (STEPPING_OUT, STEPPING_INTO, STEPPING_OVER) + +FRAME_KIND_NONE = 0 +FRAME_KIND_PYTHON = 1 +FRAME_KIND_DJANGO = 2 + +DJANGO_BUILTINS = {'True': True, 'False': False, 'None': None} + +PYTHON_EVALUATION_RESULT_REPR_KIND_NORMAL = 0 # regular repr and hex repr (if applicable) for the evaluation result; length is len(result) +PYTHON_EVALUATION_RESULT_REPR_KIND_RAW = 1 # repr is raw representation of the value - see TYPES_WITH_RAW_REPR; length is len(repr) +PYTHON_EVALUATION_RESULT_REPR_KIND_RAWLEN = 2 # same as above, but only the length is reported, not the actual value + +PYTHON_EVALUATION_RESULT_EXPANDABLE = 1 +PYTHON_EVALUATION_RESULT_METHOD_CALL = 2 +PYTHON_EVALUATION_RESULT_SIDE_EFFECTS = 4 +PYTHON_EVALUATION_RESULT_RAW = 8 +PYTHON_EVALUATION_RESULT_HAS_RAW_REPR = 16 + +# Don't show attributes of these types if they come from the class (assume they are methods). +METHOD_TYPES = ( + types.FunctionType, + types.MethodType, + types.BuiltinFunctionType, + type("".__repr__), # method-wrapper +) + +# repr() for these types can be used as input for eval() to get the original value. +# float is intentionally not included because it is not always round-trippable (e.g inf, nan). +TYPES_WITH_ROUND_TRIPPING_REPR = set((type(None), int, bool, str, unicode)) +if sys.version[0] == '3': + TYPES_WITH_ROUND_TRIPPING_REPR.add(bytes) +else: + TYPES_WITH_ROUND_TRIPPING_REPR.add(long) + +# repr() for these types can be used as input for eval() to get the original value, provided that the same is true for all their elements. +COLLECTION_TYPES_WITH_ROUND_TRIPPING_REPR = set((tuple, list, set, frozenset)) + +# eval(repr(x)), but optimized for common types for which it is known that result == x. +def eval_repr(x): + def is_repr_round_tripping(x): + # Do exact type checks here - subclasses can override __repr__. + if type(x) in TYPES_WITH_ROUND_TRIPPING_REPR: + return True + elif type(x) in COLLECTION_TYPES_WITH_ROUND_TRIPPING_REPR: + # All standard sequence types are round-trippable if their elements are. + return all((is_repr_round_tripping(item) for item in x)) + else: + return False + if is_repr_round_tripping(x): + return x + else: + return eval(repr(x), {}) + +# key is type, value is function producing the raw repr +TYPES_WITH_RAW_REPR = { + unicode: (lambda s: s) +} + +# bytearray is 2.6+ +try: + # getfilesystemencoding is used here because it effectively corresponds to the notion of "locale encoding": + # current ANSI codepage on Windows, LC_CTYPE on Linux, UTF-8 on OS X - which is exactly what we want. + TYPES_WITH_RAW_REPR[bytearray] = lambda b: b.decode(sys.getfilesystemencoding(), 'ignore') +except: + pass + +if sys.version[0] == '3': + TYPES_WITH_RAW_REPR[bytes] = TYPES_WITH_RAW_REPR[bytearray] +else: + TYPES_WITH_RAW_REPR[str] = TYPES_WITH_RAW_REPR[unicode] + +if sys.version[0] == '3': + # work around a crashing bug on CPython 3.x where they take a hard stack overflow + # we'll never see this exception but it'll allow us to keep our try/except handler + # the same across all versions of Python + class StackOverflowException(Exception): pass +else: + StackOverflowException = RuntimeError + +ASBR = to_bytes('ASBR') +SETL = to_bytes('SETL') +THRF = to_bytes('THRF') +DETC = to_bytes('DETC') +NEWT = to_bytes('NEWT') +EXTT = to_bytes('EXTT') +EXIT = to_bytes('EXIT') +EXCP = to_bytes('EXCP') +EXC2 = to_bytes('EXC2') +MODL = to_bytes('MODL') +STPD = to_bytes('STPD') +BRKS = to_bytes('BRKS') +BRKF = to_bytes('BRKF') +BRKH = to_bytes('BRKH') +BRKC = to_bytes('BRKC') +BKHC = to_bytes('BKHC') +LOAD = to_bytes('LOAD') +EXCE = to_bytes('EXCE') +EXCR = to_bytes('EXCR') +CHLD = to_bytes('CHLD') +OUTP = to_bytes('OUTP') +REQH = to_bytes('REQH') +LAST = to_bytes('LAST') + +def get_thread_from_id(id): + THREADS_LOCK.acquire() + try: + return THREADS.get(id) + finally: + THREADS_LOCK.release() + +def should_send_frame(frame): + return (frame is not None and + frame.f_code not in DEBUG_ENTRYPOINTS and + path.normcase(frame.f_code.co_filename) not in DONT_DEBUG) + +KNOWN_DIRECTORIES = set((None, '')) +KNOWN_ZIPS = set() + +def is_file_in_zip(filename): + parent, name = path.split(path.abspath(filename)) + if parent in KNOWN_DIRECTORIES: + return False + elif parent in KNOWN_ZIPS: + return True + elif path.isdir(parent): + KNOWN_DIRECTORIES.add(parent) + return False + else: + KNOWN_ZIPS.add(parent) + return True + +def lookup_builtin(name, frame): + try: + return frame.f_builtins.get(bits) + except: + # http://ironpython.codeplex.com/workitem/30908 + builtins = frame.f_globals['__builtins__'] + if not isinstance(builtins, dict): + builtins = builtins.__dict__ + return builtins.get(name) + +def lookup_local(frame, name): + bits = name.split('.') + obj = frame.f_locals.get(bits[0]) or frame.f_globals.get(bits[0]) or lookup_builtin(bits[0], frame) + bits.pop(0) + while bits and obj is not None and type(obj) is types.ModuleType: + obj = getattr(obj, bits.pop(0), None) + return obj + +if sys.version_info[0] >= 3: + _EXCEPTIONS_MODULE = 'builtins' +else: + _EXCEPTIONS_MODULE = 'exceptions' + +def get_exception_name(exc_type): + if exc_type.__module__ == _EXCEPTIONS_MODULE: + return exc_type.__name__ + else: + return exc_type.__module__ + '.' + exc_type.__name__ + +# These constants come from Visual Studio - enum_EXCEPTION_STATE +BREAK_MODE_NEVER = 0 +BREAK_MODE_ALWAYS = 1 +BREAK_MODE_UNHANDLED = 32 + +BREAK_TYPE_NONE = 0 +BREAK_TYPE_UNHANDLED = 1 +BREAK_TYPE_HANDLED = 2 + +class ExceptionBreakInfo(object): + BUILT_IN_HANDLERS = { + path.normcase(''): ((None, None, '*'),), + path.normcase('build\\bdist.win32\\egg\\pkg_resources.py'): ((None, None, '*'),), + path.normcase('build\\bdist.win-amd64\\egg\\pkg_resources.py'): ((None, None, '*'),), + } + + def __init__(self): + self.default_mode = BREAK_MODE_UNHANDLED + self.break_on = { } + self.handler_cache = dict(self.BUILT_IN_HANDLERS) + self.handler_lock = thread.allocate_lock() + self.add_exception('exceptions.IndexError', BREAK_MODE_NEVER) + self.add_exception('builtins.IndexError', BREAK_MODE_NEVER) + self.add_exception('exceptions.KeyError', BREAK_MODE_NEVER) + self.add_exception('builtins.KeyError', BREAK_MODE_NEVER) + self.add_exception('exceptions.AttributeError', BREAK_MODE_NEVER) + self.add_exception('builtins.AttributeError', BREAK_MODE_NEVER) + self.add_exception('exceptions.StopIteration', BREAK_MODE_NEVER) + self.add_exception('builtins.StopIteration', BREAK_MODE_NEVER) + self.add_exception('exceptions.GeneratorExit', BREAK_MODE_NEVER) + self.add_exception('builtins.GeneratorExit', BREAK_MODE_NEVER) + + def clear(self): + self.default_mode = BREAK_MODE_UNHANDLED + self.break_on.clear() + self.handler_cache = dict(self.BUILT_IN_HANDLERS) + + def should_break(self, thread, ex_type, ex_value, trace): + probe_stack() + name = get_exception_name(ex_type) + mode = self.break_on.get(name, self.default_mode) + break_type = BREAK_TYPE_NONE + if mode & BREAK_MODE_ALWAYS: + if self.is_handled(thread, ex_type, ex_value, trace): + break_type = BREAK_TYPE_HANDLED + else: + break_type = BREAK_TYPE_UNHANDLED + elif (mode & BREAK_MODE_UNHANDLED) and not self.is_handled(thread, ex_type, ex_value, trace): + break_type = BREAK_TYPE_UNHANDLED + + if break_type: + if issubclass(ex_type, SystemExit): + if not BREAK_ON_SYSTEMEXIT_ZERO: + if not ex_value or (isinstance(ex_value, SystemExit) and not ex_value.code): + break_type = BREAK_TYPE_NONE + + return break_type + + def is_handled(self, thread, ex_type, ex_value, trace): + if trace is None: + # get out if we didn't get a traceback + return False + + if trace.tb_next is not None: + if should_send_frame(trace.tb_next.tb_frame) and should_debug_code(trace.tb_next.tb_frame.f_code): + # don't break if this is not the top of the traceback, + # unless the previous frame was not debuggable + return True + + cur_frame = trace.tb_frame + + while should_send_frame(cur_frame) and cur_frame.f_code is not None and cur_frame.f_code.co_filename is not None: + filename = path.normcase(cur_frame.f_code.co_filename) + if is_file_in_zip(filename): + # File is in a zip, so assume it handles exceptions + return True + + if not is_same_py_file(filename, __file__): + handlers = self.handler_cache.get(filename) + + if handlers is None: + # req handlers for this file from the debug engine + self.handler_lock.acquire() + + with _SendLockCtx: + write_bytes(conn, REQH) + write_string(conn, filename) + + # wait for the handler data to be received + self.handler_lock.acquire() + self.handler_lock.release() + + handlers = self.handler_cache.get(filename) + + if handlers is None: + # no code available, so assume unhandled + return False + + line = cur_frame.f_lineno + for line_start, line_end, expressions in handlers: + if line_start is None or line_start <= line < line_end: + if '*' in expressions: + return True + + for text in expressions: + try: + res = lookup_local(cur_frame, text) + if res is not None and issubclass(ex_type, res): + return True + except: + pass + + cur_frame = cur_frame.f_back + + return False + + def add_exception(self, name, mode=BREAK_MODE_UNHANDLED): + if name.startswith(_EXCEPTIONS_MODULE + '.'): + name = name[len(_EXCEPTIONS_MODULE) + 1:] + self.break_on[name] = mode + +BREAK_ON = ExceptionBreakInfo() + +def probe_stack(depth = 10): + """helper to make sure we have enough stack space to proceed w/o corrupting + debugger state.""" + if depth == 0: + return + probe_stack(depth - 1) + +PREFIXES = [path.normcase(sys.prefix)] +# If we're running in a virtual env, DEBUG_STDLIB should respect this too. +if hasattr(sys, 'base_prefix'): + PREFIXES.append(path.normcase(sys.base_prefix)) +if hasattr(sys, 'real_prefix'): + PREFIXES.append(path.normcase(sys.real_prefix)) + +def should_debug_code(code): + if not code or not code.co_filename: + return False + + filename = path.normcase(code.co_filename) + if not DEBUG_STDLIB: + for prefix in PREFIXES: + if prefix != '' and filename.startswith(prefix): + return False + + for dont_debug_file in DONT_DEBUG: + if is_same_py_file(filename, dont_debug_file): + return False + + if is_file_in_zip(filename): + # file in inside an egg or zip, so we can't debug it + return False + + return True + +attach_lock = thread.allocate() +attach_sent_break = False + +local_path_to_vs_path = {} + +def breakpoint_path_match(vs_path, local_path): + vs_path_norm = path.normcase(vs_path) + local_path_norm = path.normcase(local_path) + if local_path_to_vs_path.get(local_path_norm) == vs_path_norm: + return True + + # Walk the local filesystem from local_path up, matching agains win_path component by component, + # and stop when we no longer see an __init__.py. This should give a reasonably close approximation + # of matching the package name. + while True: + local_path, local_name = path.split(local_path) + vs_path, vs_name = ntpath.split(vs_path) + # Match the last component in the path. If one or both components are unavailable, then + # we have reached the root on the corresponding path without successfully matching. + if not local_name or not vs_name or path.normcase(local_name) != path.normcase(vs_name): + return False + # If we have an __init__.py, this module was inside the package, and we still need to match + # thatpackage, so walk up one level and keep matching. Otherwise, we've walked as far as we + # needed to, and matched all names on our way, so this is a match. + if not path.exists(path.join(local_path, '__init__.py')): + break + + local_path_to_vs_path[local_path_norm] = vs_path_norm + return True + +def update_all_thread_stacks(blocking_thread = None, check_is_blocked = True): + THREADS_LOCK.acquire() + all_threads = list(THREADS.values()) + THREADS_LOCK.release() + + for cur_thread in all_threads: + if cur_thread is blocking_thread: + continue + + cur_thread._block_starting_lock.acquire() + if not check_is_blocked or not cur_thread._is_blocked: + # release the lock, we're going to run user code to evaluate the frames + cur_thread._block_starting_lock.release() + + frames = cur_thread.get_frame_list() + + # re-acquire the lock and make sure we're still not blocked. If so send + # the frame list. + cur_thread._block_starting_lock.acquire() + if not check_is_blocked or not cur_thread._is_blocked: + cur_thread.send_frame_list(frames) + + cur_thread._block_starting_lock.release() diff --git a/src/test/pythonFiles/folding/visualstudio_py_repl.py b/src/test/pythonFiles/folding/visualstudio_py_repl.py new file mode 100644 index 000000000000..14259db2e30e --- /dev/null +++ b/src/test/pythonFiles/folding/visualstudio_py_repl.py @@ -0,0 +1,513 @@ +# Python Tools for Visual Studio + +# Copyright(c) Microsoft Corporation + +# All rights reserved. + +from __future__ import with_statement + +__author__ = "Microsoft Corporation " +__version__ = "3.0.0.0" + +# This module MUST NOT import threading in global scope. This is because in a direct (non-ptvsd) + +# attach scenario, it is loaded on the injected debugger attach thread, and if threading module + +# hasn't been loaded already, it will assume that the thread on which it is being loaded is the + +# main thread. This will cause issues when the thread goes away after attach completes. + +try: + import thread +except ImportError: + # Renamed in Python3k + import _thread as thread +try: + from ssl import SSLError +except: + SSLError = None + +import sys +import socket +import select +import time +import struct +import imp +import traceback +import random +import os +import inspect +import types +from collections import deque + +try: + # In the local attach scenario, visualstudio_py_util is injected into globals() + + # by PyDebugAttach before loading this module, and cannot be imported. + _vspu = visualstudio_py_util +except: + try: + import visualstudio_py_util as _vspu + except ImportError: + import ptvsd.visualstudio_py_util as _vspu +to_bytes = _vspu.to_bytes +read_bytes = _vspu.read_bytes +read_int = _vspu.read_int +read_string = _vspu.read_string +write_bytes = _vspu.write_bytes +write_int = _vspu.write_int +write_string = _vspu.write_string + +try: + unicode +except NameError: + unicode = str + +try: + BaseException +except NameError: + # BaseException not defined until Python 2.5 + BaseException = Exception + +DEBUG = os.environ.get('DEBUG_REPL') is not None + +__all__ = ['ReplBackend', 'BasicReplBackend', 'BACKEND'] + +def _debug_write(out): + if DEBUG: + sys.__stdout__.write(out) + sys.__stdout__.flush() + + +class SafeSendLock(object): + """a lock which ensures we're released if we take a KeyboardInterrupt exception acquiring it""" + def __init__(self): + self.lock = thread.allocate_lock() + + def __enter__(self): + self.acquire() + + def __exit__(self, exc_type, exc_value, tb): + self.release() + + def acquire(self): + try: + self.lock.acquire() + except KeyboardInterrupt: + try: + self.lock.release() + except: + pass + raise + + def release(self): + self.lock.release() + +def _command_line_to_args_list(cmdline): + """splits a string into a list using Windows command line syntax.""" + args_list = [] + + if cmdline and cmdline.strip(): + from ctypes import c_int, c_voidp, c_wchar_p + from ctypes import byref, POINTER, WinDLL + + clta = WinDLL('shell32').CommandLineToArgvW + clta.argtypes = [c_wchar_p, POINTER(c_int)] + clta.restype = POINTER(c_wchar_p) + + lf = WinDLL('kernel32').LocalFree + lf.argtypes = [c_voidp] + + pNumArgs = c_int() + r = clta(cmdline, byref(pNumArgs)) + if r: + for index in range(0, pNumArgs.value): + if sys.hexversion >= 0x030000F0: + argval = r[index] + else: + argval = r[index].encode('ascii', 'replace') + args_list.append(argval) + lf(r) + else: + sys.stderr.write('Error parsing script arguments:\n') + sys.stderr.write(cmdline + '\n') + + return args_list + + +class UnsupportedReplException(Exception): + def __init__(self, reason): + self.reason = reason + +# save the start_new_thread so we won't debug/break into the REPL comm thread. +start_new_thread = thread.start_new_thread +class ReplBackend(object): + """back end for executing REPL code. This base class handles all of the communication with the remote process while derived classes implement the actual inspection and introspection.""" + _MRES = to_bytes('MRES') + _SRES = to_bytes('SRES') + _MODS = to_bytes('MODS') + _IMGD = to_bytes('IMGD') + _PRPC = to_bytes('PRPC') + _RDLN = to_bytes('RDLN') + _STDO = to_bytes('STDO') + _STDE = to_bytes('STDE') + _DBGA = to_bytes('DBGA') + _DETC = to_bytes('DETC') + _DPNG = to_bytes('DPNG') + _DXAM = to_bytes('DXAM') + _CHWD = to_bytes('CHWD') + + _MERR = to_bytes('MERR') + _SERR = to_bytes('SERR') + _ERRE = to_bytes('ERRE') + _EXIT = to_bytes('EXIT') + _DONE = to_bytes('DONE') + _MODC = to_bytes('MODC') + + def __init__(self, *args, **kwargs): + import threading + self.conn = None + self.send_lock = SafeSendLock() + self.input_event = threading.Lock() + self.input_event.acquire() # lock starts acquired (we use it like a manual reset event) + self.input_string = None + self.exit_requested = False + + def connect(self, port): + self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.conn.connect(('127.0.0.1', port)) + + # start a new thread for communicating w/ the remote process + start_new_thread(self._repl_loop, ()) + + def connect_using_socket(self, socket): + self.conn = socket + start_new_thread(self._repl_loop, ()) + + def _repl_loop(self): + """loop on created thread which processes communicates with the REPL window""" + try: + while True: + if self.check_for_exit_repl_loop(): + break + + # we receive a series of 4 byte commands. Each command then + + # has it's own format which we must parse before continuing to + + # the next command. + self.flush() + self.conn.settimeout(10) + + # 2.x raises SSLError in case of timeout (http://bugs.python.org/issue10272) + if SSLError: + timeout_exc_types = (socket.timeout, SSLError) + else: + timeout_exc_types = socket.timeout + try: + inp = read_bytes(self.conn, 4) + except timeout_exc_types: + r, w, x = select.select([], [], [self.conn], 0) + if x: + # an exception event has occured on the socket... + raise + continue + + self.conn.settimeout(None) + if inp == '': + break + self.flush() + + cmd = ReplBackend._COMMANDS.get(inp) + if cmd is not None: + cmd(self) + except: + _debug_write('error in repl loop') + _debug_write(traceback.format_exc()) + self.exit_process() + + time.sleep(2) # try and exit gracefully, then interrupt main if necessary + + if sys.platform == 'cli': + # just kill us as fast as possible + import System + System.Environment.Exit(1) + + self.interrupt_main() + + def check_for_exit_repl_loop(self): + return False + + def _cmd_run(self): + """runs the received snippet of code""" + self.run_command(read_string(self.conn)) + + def _cmd_abrt(self): + """aborts the current running command""" + # abort command, interrupts execution of the main thread. + self.interrupt_main() + + def _cmd_exit(self): + """exits the interactive process""" + self.exit_requested = True + self.exit_process() + + def _cmd_mems(self): + """gets the list of members available for the given expression""" + expression = read_string(self.conn) + try: + name, inst_members, type_members = self.get_members(expression) + except: + with self.send_lock: + write_bytes(self.conn, ReplBackend._MERR) + _debug_write('error in eval') + _debug_write(traceback.format_exc()) + else: + with self.send_lock: + write_bytes(self.conn, ReplBackend._MRES) + write_string(self.conn, name) + self._write_member_dict(inst_members) + self._write_member_dict(type_members) + + def _cmd_sigs(self): + """gets the signatures for the given expression""" + expression = read_string(self.conn) + try: + sigs = self.get_signatures(expression) + except: + with self.send_lock: + write_bytes(self.conn, ReplBackend._SERR) + _debug_write('error in eval') + _debug_write(traceback.format_exc()) + else: + with self.send_lock: + write_bytes(self.conn, ReplBackend._SRES) + # single overload + write_int(self.conn, len(sigs)) + for doc, args, vargs, varkw, defaults in sigs: + # write overload + write_string(self.conn, (doc or '')[:4096]) + arg_count = len(args) + (vargs is not None) + (varkw is not None) + write_int(self.conn, arg_count) + + def_values = [''] * (len(args) - len(defaults)) + ['=' + d for d in defaults] + for arg, def_value in zip(args, def_values): + write_string(self.conn, (arg or '') + def_value) + if vargs is not None: + write_string(self.conn, '*' + vargs) + if varkw is not None: + write_string(self.conn, '**' + varkw) + + def _cmd_setm(self): + global exec_mod + """sets the current module which code will execute against""" + mod_name = read_string(self.conn) + self.set_current_module(mod_name) + + def _cmd_sett(self): + """sets the current thread and frame which code will execute against""" + thread_id = read_int(self.conn) + frame_id = read_int(self.conn) + frame_kind = read_int(self.conn) + self.set_current_thread_and_frame(thread_id, frame_id, frame_kind) + + def _cmd_mods(self): + """gets the list of available modules""" + try: + res = self.get_module_names() + res.sort() + except: + res = [] + + with self.send_lock: + write_bytes(self.conn, ReplBackend._MODS) + write_int(self.conn, len(res)) + for name, filename in res: + write_string(self.conn, name) + write_string(self.conn, filename) + + def _cmd_inpl(self): + """handles the input command which returns a string of input""" + self.input_string = read_string(self.conn) + self.input_event.release() + + def _cmd_excf(self): + """handles executing a single file""" + filename = read_string(self.conn) + args = read_string(self.conn) + self.execute_file(filename, args) + + def _cmd_excx(self): + """handles executing a single file, module or process""" + filetype = read_string(self.conn) + filename = read_string(self.conn) + args = read_string(self.conn) + self.execute_file_ex(filetype, filename, args) + + def _cmd_debug_attach(self): + import visualstudio_py_debugger + port = read_int(self.conn) + id = read_string(self.conn) + debug_options = visualstudio_py_debugger.parse_debug_options(read_string(self.conn)) + self.attach_process(port, id, debug_options) + + _COMMANDS = { + to_bytes('run '): _cmd_run, + to_bytes('abrt'): _cmd_abrt, + to_bytes('exit'): _cmd_exit, + to_bytes('mems'): _cmd_mems, + to_bytes('sigs'): _cmd_sigs, + to_bytes('mods'): _cmd_mods, + to_bytes('setm'): _cmd_setm, + to_bytes('sett'): _cmd_sett, + to_bytes('inpl'): _cmd_inpl, + to_bytes('excf'): _cmd_excf, + to_bytes('excx'): _cmd_excx, + to_bytes('dbga'): _cmd_debug_attach, + } + + def _write_member_dict(self, mem_dict): + write_int(self.conn, len(mem_dict)) + for name, type_name in mem_dict.items(): + write_string(self.conn, name) + write_string(self.conn, type_name) + + def on_debugger_detach(self): + with self.send_lock: + write_bytes(self.conn, ReplBackend._DETC) + + def init_debugger(self): + from os import path + sys.path.append(path.dirname(__file__)) + import visualstudio_py_debugger + visualstudio_py_debugger.DONT_DEBUG.append(path.normcase(__file__)) + new_thread = visualstudio_py_debugger.new_thread() + sys.settrace(new_thread.trace_func) + visualstudio_py_debugger.intercept_threads(True) + + def send_image(self, filename): + with self.send_lock: + write_bytes(self.conn, ReplBackend._IMGD) + write_string(self.conn, filename) + + def write_png(self, image_bytes): + with self.send_lock: + write_bytes(self.conn, ReplBackend._DPNG) + write_int(self.conn, len(image_bytes)) + write_bytes(self.conn, image_bytes) + + def write_xaml(self, xaml_bytes): + with self.send_lock: + write_bytes(self.conn, ReplBackend._DXAM) + write_int(self.conn, len(xaml_bytes)) + write_bytes(self.conn, xaml_bytes) + + def send_prompt(self, ps1, ps2, allow_multiple_statements): + """sends the current prompt to the interactive window""" + with self.send_lock: + write_bytes(self.conn, ReplBackend._PRPC) + write_string(self.conn, ps1) + write_string(self.conn, ps2) + write_int(self.conn, 1 if allow_multiple_statements else 0) + + def send_cwd(self): + """sends the current working directory""" + with self.send_lock: + write_bytes(self.conn, ReplBackend._CHWD) + write_string(self.conn, os.getcwd()) + + def send_error(self): + """reports that an error occured to the interactive window""" + with self.send_lock: + write_bytes(self.conn, ReplBackend._ERRE) + + def send_exit(self): + """reports the that the REPL process has exited to the interactive window""" + with self.send_lock: + write_bytes(self.conn, ReplBackend._EXIT) + + def send_command_executed(self): + with self.send_lock: + write_bytes(self.conn, ReplBackend._DONE) + + def send_modules_changed(self): + with self.send_lock: + write_bytes(self.conn, ReplBackend._MODC) + + def read_line(self): + """reads a line of input from standard input""" + with self.send_lock: + write_bytes(self.conn, ReplBackend._RDLN) + self.input_event.acquire() + return self.input_string + + def write_stdout(self, value): + """writes a string to standard output in the remote console""" + with self.send_lock: + write_bytes(self.conn, ReplBackend._STDO) + write_string(self.conn, value) + + def write_stderr(self, value): + """writes a string to standard input in the remote console""" + with self.send_lock: + write_bytes(self.conn, ReplBackend._STDE) + write_string(self.conn, value) + + ################################################################ + + # Implementation of execution, etc... + + def execution_loop(self): + """starts processing execution requests""" + raise NotImplementedError + + def run_command(self, command): + """runs the specified command which is a string containing code""" + raise NotImplementedError + + def execute_file(self, filename, args): + """executes the given filename as the main module""" + return self.execute_file_ex('script', filename, args) + + def execute_file_ex(self, filetype, filename, args): + """executes the given filename as a 'script', 'module' or 'process'.""" + raise NotImplementedError + + def interrupt_main(self): + """aborts the current running command""" + raise NotImplementedError + + def exit_process(self): + """exits the REPL process""" + raise NotImplementedError + + def get_members(self, expression): + """returns a tuple of the type name, instance members, and type members""" + raise NotImplementedError + + def get_signatures(self, expression): + """returns doc, args, vargs, varkw, defaults.""" + raise NotImplementedError + + def set_current_module(self, module): + """sets the module which code executes against""" + raise NotImplementedError + + def set_current_thread_and_frame(self, thread_id, frame_id, frame_kind): + """sets the current thread and frame which code will execute against""" + raise NotImplementedError + + def get_module_names(self): + """returns a list of module names""" + raise NotImplementedError + + def flush(self): + """flushes the stdout/stderr buffers""" + raise NotImplementedError + + def attach_process(self, port, debugger_id, debug_options): + """starts processing execution requests""" + raise NotImplementedError + +def exit_work_item(): + sys.exit(0) diff --git a/src/test/pythonFiles/formatting/black.output b/src/test/pythonFiles/formatting/black.output new file mode 100644 index 000000000000..be709f2d720a --- /dev/null +++ b/src/test/pythonFiles/formatting/black.output @@ -0,0 +1,54 @@ +--- src/test/pythonFiles/formatting/fileToFormat.py (original) ++++ src/test/pythonFiles/formatting/fileToFormat.py (formatted) +@@ -1,22 +1,38 @@ +-import math, sys; ++import math, sys ++ + + def example1(): + ####This is a long comment. This should be wrapped to fit within 72 characters. +- some_tuple=( 1,2, 3,'a' ); +- some_variable={'long':'Long code lines should be wrapped within 79 characters.', +- 'other':[math.pi, 100,200,300,9876543210,'This is a long string that goes on'], +- 'more':{'inner':'This whole logical line should be wrapped.',some_tuple:[1, +- 20,300,40000,500000000,60000000000000000]}} ++ some_tuple = (1, 2, 3, "a") ++ some_variable = { ++ "long": "Long code lines should be wrapped within 79 characters.", ++ "other": [ ++ math.pi, 100, 200, 300, 9876543210, "This is a long string that goes on" ++ ], ++ "more": { ++ "inner": "This whole logical line should be wrapped.", ++ some_tuple: [1, 20, 300, 40000, 500000000, 60000000000000000], ++ }, ++ } + return (some_tuple, some_variable) +-def example2(): return {'has_key() is deprecated':True}.has_key({'f':2}.has_key('')); +-class Example3( object ): +- def __init__ ( self, bar ): +- #Comments should have a space after the hash. +- if bar : bar+=1; bar=bar* bar ; return bar +- else: +- some_string = """ ++ ++ ++def example2(): ++ return {"has_key() is deprecated": True}.has_key({"f": 2}.has_key("")) ++ ++ ++class Example3(object): ++ ++ def __init__(self, bar): ++ # Comments should have a space after the hash. ++ if bar: ++ bar += 1 ++ bar = bar * bar ++ return bar ++ else: ++ some_string = """ + Indentation in multiline strings should not be touched. + Only actual code should be reindented. + """ +- return (sys.path, some_string) ++ return (sys.path, some_string) diff --git a/src/test/pythonFiles/formatting/fileToFormatOnEnter.py b/src/test/pythonFiles/formatting/fileToFormatOnEnter.py index bbd025363098..8adfd1fa1233 100644 --- a/src/test/pythonFiles/formatting/fileToFormatOnEnter.py +++ b/src/test/pythonFiles/formatting/fileToFormatOnEnter.py @@ -6,4 +6,8 @@ x+1 # @x x.y +if x<=1: +if 1<=x: +def __init__(self, age = 23) +while(1) x+""" diff --git a/src/test/pythonFiles/formatting/pythonGrammar.py b/src/test/pythonFiles/formatting/pythonGrammar.py new file mode 100644 index 000000000000..937cba401d3f --- /dev/null +++ b/src/test/pythonFiles/formatting/pythonGrammar.py @@ -0,0 +1,1572 @@ +# Python test set -- part 1, grammar. +# This just tests whether the parser accepts them all. + +from test.support import check_syntax_error +import inspect +import unittest +import sys +# testing import * +from sys import * + +# different import patterns to check that __annotations__ does not interfere +# with import machinery +import test.ann_module as ann_module +import typing +from collections import ChainMap +from test import ann_module2 +import test + +# These are shared with test_tokenize and other test modules. +# +# Note: since several test cases filter out floats by looking for "e" and ".", +# don't add hexadecimal literals that contain "e" or "E". +VALID_UNDERSCORE_LITERALS = [ + '0_0_0', + '4_2', + '1_0000_0000', + '0b1001_0100', + '0xffff_ffff', + '0o5_7_7', + '1_00_00.5', + '1_00_00.5e5', + '1_00_00e5_1', + '1e1_0', + '.1_4', + '.1_4e1', + '0b_0', + '0x_f', + '0o_5', + '1_00_00j', + '1_00_00.5j', + '1_00_00e5_1j', + '.1_4j', + '(1_2.5+3_3j)', + '(.5_6j)', +] +INVALID_UNDERSCORE_LITERALS = [ + # Trailing underscores: + '0_', + '42_', + '1.4j_', + '0x_', + '0b1_', + '0xf_', + '0o5_', + '0 if 1_Else 1', + # Underscores in the base selector: + '0_b0', + '0_xf', + '0_o5', + # Old-style octal, still disallowed: + '0_7', + '09_99', + # Multiple consecutive underscores: + '4_______2', + '0.1__4', + '0.1__4j', + '0b1001__0100', + '0xffff__ffff', + '0x___', + '0o5__77', + '1e1__0', + '1e1__0j', + # Underscore right before a dot: + '1_.4', + '1_.4j', + # Underscore right after a dot: + '1._4', + '1._4j', + '._5', + '._5j', + # Underscore right after a sign: + '1.0e+_1', + '1.0e+_1j', + # Underscore right before j: + '1.4_j', + '1.4e5_j', + # Underscore right before e: + '1_e1', + '1.4_e1', + '1.4_e1j', + # Underscore right after e: + '1e_1', + '1.4e_1', + '1.4e_1j', + # Complex cases with parens: + '(1+1.5_j_)', + '(1+1.5_j)', +] + + +class TokenTests(unittest.TestCase): + + def test_backslash(self): + # Backslash means line continuation: + x = 1 \ + + 1 + self.assertEqual(x, 2, 'backslash for line continuation') + + # Backslash does not means continuation in comments :\ + x = 0 + self.assertEqual(x, 0, 'backslash ending comment') + + def test_plain_integers(self): + self.assertEqual(type(000), type(0)) + self.assertEqual(0xff, 255) + self.assertEqual(0o377, 255) + self.assertEqual(2147483647, 0o17777777777) + self.assertEqual(0b1001, 9) + # "0x" is not a valid literal + self.assertRaises(SyntaxError, eval, "0x") + from sys import maxsize + if maxsize == 2147483647: + self.assertEqual(-2147483647 - 1, -0o20000000000) + # XXX -2147483648 + self.assertTrue(0o37777777777 > 0) + self.assertTrue(0xffffffff > 0) + self.assertTrue(0b1111111111111111111111111111111 > 0) + for s in ('2147483648', '0o40000000000', '0x100000000', + '0b10000000000000000000000000000000'): + try: + x = eval(s) + except OverflowError: + self.fail("OverflowError on huge integer literal %r" % s) + elif maxsize == 9223372036854775807: + self.assertEqual(-9223372036854775807 - 1, -0o1000000000000000000000) + self.assertTrue(0o1777777777777777777777 > 0) + self.assertTrue(0xffffffffffffffff > 0) + self.assertTrue(0b11111111111111111111111111111111111111111111111111111111111111 > 0) + for s in '9223372036854775808', '0o2000000000000000000000', \ + '0x10000000000000000', \ + '0b100000000000000000000000000000000000000000000000000000000000000': + try: + x = eval(s) + except OverflowError: + self.fail("OverflowError on huge integer literal %r" % s) + else: + self.fail('Weird maxsize value %r' % maxsize) + + def test_long_integers(self): + x = 0 + x = 0xffffffffffffffff + x = 0Xffffffffffffffff + x = 0o77777777777777777 + x = 0O77777777777777777 + x = 123456789012345678901234567890 + x = 0b100000000000000000000000000000000000000000000000000000000000000000000 + x = 0B111111111111111111111111111111111111111111111111111111111111111111111 + + def test_floats(self): + x = 3.14 + x = 314. + x = 0.314 + # XXX x = 000.314 + x = .314 + x = 3e14 + x = 3E14 + x = 3e-14 + x = 3e+14 + x = 3.e14 + x = .3e14 + x = 3.1e4 + + def test_float_exponent_tokenization(self): + # See issue 21642. + self.assertEqual(1 if 1 else 0, 1) + self.assertEqual(1 if 0 else 0, 0) + self.assertRaises(SyntaxError, eval, "0 if 1Else 0") + + def test_underscore_literals(self): + for lit in VALID_UNDERSCORE_LITERALS: + self.assertEqual(eval(lit), eval(lit.replace('_', ''))) + for lit in INVALID_UNDERSCORE_LITERALS: + self.assertRaises(SyntaxError, eval, lit) + # Sanity check: no literal begins with an underscore + self.assertRaises(NameError, eval, "_0") + + def test_string_literals(self): + x = ''; y = ""; self.assertTrue(len(x) == 0 and x == y) + x = '\''; y = "'"; self.assertTrue(len(x) == 1 and x == y and ord(x) == 39) + x = '"'; y = "\""; self.assertTrue(len(x) == 1 and x == y and ord(x) == 34) + x = "doesn't \"shrink\" does it" + y = 'doesn\'t "shrink" does it' + self.assertTrue(len(x) == 24 and x == y) + x = "does \"shrink\" doesn't it" + y = 'does "shrink" doesn\'t it' + self.assertTrue(len(x) == 24 and x == y) + x = """ +The "quick" +brown fox +jumps over +the 'lazy' dog. +""" + y = '\nThe "quick"\nbrown fox\njumps over\nthe \'lazy\' dog.\n' + self.assertEqual(x, y) + y = ''' +The "quick" +brown fox +jumps over +the 'lazy' dog. +''' + self.assertEqual(x, y) + y = "\n\ +The \"quick\"\n\ +brown fox\n\ +jumps over\n\ +the 'lazy' dog.\n\ +" + self.assertEqual(x, y) + y = '\n\ +The \"quick\"\n\ +brown fox\n\ +jumps over\n\ +the \'lazy\' dog.\n\ +' + self.assertEqual(x, y) + + def test_ellipsis(self): + x = ... + self.assertTrue(x is Ellipsis) + self.assertRaises(SyntaxError, eval, ".. .") + + def test_eof_error(self): + samples = ("def foo(", "\ndef foo(", "def foo(\n") + for s in samples: + with self.assertRaises(SyntaxError) as cm: + compile(s, "", "exec") + self.assertIn("unexpected EOF", str(cm.exception)) + +var_annot_global: int # a global annotated is necessary for test_var_annot + +# custom namespace for testing __annotations__ + +class CNS: + def __init__(self): + self._dct = {} + def __setitem__(self, item, value): + self._dct[item.lower()] = value + def __getitem__(self, item): + return self._dct[item] + + +class GrammarTests(unittest.TestCase): + + check_syntax_error = check_syntax_error + + # single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE + # XXX can't test in a script -- this rule is only used when interactive + + # file_input: (NEWLINE | stmt)* ENDMARKER + # Being tested as this very moment this very module + + # expr_input: testlist NEWLINE + # XXX Hard to test -- used only in calls to input() + + def test_eval_input(self): + # testlist ENDMARKER + x = eval('1, 0 or 1') + + def test_var_annot_basics(self): + # all these should be allowed + var1: int = 5 + var2: [int, str] + my_lst = [42] + def one(): + return 1 + int.new_attr: int + [list][0]: type + my_lst[one() - 1]: int = 5 + self.assertEqual(my_lst, [5]) + + def test_var_annot_syntax_errors(self): + # parser pass + check_syntax_error(self, "def f: int") + check_syntax_error(self, "x: int: str") + check_syntax_error(self, "def f():\n" + " nonlocal x: int\n") + # AST pass + check_syntax_error(self, "[x, 0]: int\n") + check_syntax_error(self, "f(): int\n") + check_syntax_error(self, "(x,): int") + check_syntax_error(self, "def f():\n" + " (x, y): int = (1, 2)\n") + # symtable pass + check_syntax_error(self, "def f():\n" + " x: int\n" + " global x\n") + check_syntax_error(self, "def f():\n" + " global x\n" + " x: int\n") + + def test_var_annot_basic_semantics(self): + # execution order + with self.assertRaises(ZeroDivisionError): + no_name[does_not_exist]: no_name_again = 1 / 0 + with self.assertRaises(NameError): + no_name[does_not_exist]: 1 / 0 = 0 + global var_annot_global + + # function semantics + def f(): + st: str = "Hello" + a.b: int = (1, 2) + return st + self.assertEqual(f.__annotations__, {}) + def f_OK(): + x: 1 / 0 + f_OK() + def fbad(): + x: int + print(x) + with self.assertRaises(UnboundLocalError): + fbad() + def f2bad(): + (no_such_global): int + print(no_such_global) + try: + f2bad() + except Exception as e: + self.assertIs(type(e), NameError) + + # class semantics + class C: + __foo: int + s: str = "attr" + z = 2 + def __init__(self, x): + self.x: int = x + self.assertEqual(C.__annotations__, {'_C__foo': int, 's': str}) + with self.assertRaises(NameError): + class CBad: + no_such_name_defined.attr: int = 0 + with self.assertRaises(NameError): + class Cbad2(C): + x: int + x.y: list = [] + + def test_var_annot_metaclass_semantics(self): + class CMeta(type): + @classmethod + def __prepare__(metacls, name, bases, **kwds): + return {'__annotations__': CNS()} + class CC(metaclass=CMeta): + XX: 'ANNOT' + self.assertEqual(CC.__annotations__['xx'], 'ANNOT') + + def test_var_annot_module_semantics(self): + with self.assertRaises(AttributeError): + print(test.__annotations__) + self.assertEqual(ann_module.__annotations__, + {1: 2, 'x': int, 'y': str, 'f': typing.Tuple[int, int]}) + self.assertEqual(ann_module.M.__annotations__, + {'123': 123, 'o': type}) + self.assertEqual(ann_module2.__annotations__, {}) + + def test_var_annot_in_module(self): + # check that functions fail the same way when executed + # outside of module where they were defined + from test.ann_module3 import f_bad_ann, g_bad_ann, D_bad_ann + with self.assertRaises(NameError): + f_bad_ann() + with self.assertRaises(NameError): + g_bad_ann() + with self.assertRaises(NameError): + D_bad_ann(5) + + def test_var_annot_simple_exec(self): + gns = {}; lns = {} + exec("'docstring'\n" + "__annotations__[1] = 2\n" + "x: int = 5\n", gns, lns) + self.assertEqual(lns["__annotations__"], {1: 2, 'x': int}) + with self.assertRaises(KeyError): + gns['__annotations__'] + + def test_var_annot_custom_maps(self): + # tests with custom locals() and __annotations__ + ns = {'__annotations__': CNS()} + exec('X: int; Z: str = "Z"; (w): complex = 1j', ns) + self.assertEqual(ns['__annotations__']['x'], int) + self.assertEqual(ns['__annotations__']['z'], str) + with self.assertRaises(KeyError): + ns['__annotations__']['w'] + nonloc_ns = {} + class CNS2: + def __init__(self): + self._dct = {} + def __setitem__(self, item, value): + nonlocal nonloc_ns + self._dct[item] = value + nonloc_ns[item] = value + def __getitem__(self, item): + return self._dct[item] + exec('x: int = 1', {}, CNS2()) + self.assertEqual(nonloc_ns['__annotations__']['x'], int) + + def test_var_annot_refleak(self): + # complex case: custom locals plus custom __annotations__ + # this was causing refleak + cns = CNS() + nonloc_ns = {'__annotations__': cns} + class CNS2: + def __init__(self): + self._dct = {'__annotations__': cns} + def __setitem__(self, item, value): + nonlocal nonloc_ns + self._dct[item] = value + nonloc_ns[item] = value + def __getitem__(self, item): + return self._dct[item] + exec('X: str', {}, CNS2()) + self.assertEqual(nonloc_ns['__annotations__']['x'], str) + + def test_funcdef(self): + ### [decorators] 'def' NAME parameters ['->' test] ':' suite + ### decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE + ### decorators: decorator+ + ### parameters: '(' [typedargslist] ')' + ### typedargslist: ((tfpdef ['=' test] ',')* + ### ('*' [tfpdef] (',' tfpdef ['=' test])* [',' '**' tfpdef] | '**' tfpdef) + ### | tfpdef ['=' test] (',' tfpdef ['=' test])* [',']) + ### tfpdef: NAME [':' test] + ### varargslist: ((vfpdef ['=' test] ',')* + ### ('*' [vfpdef] (',' vfpdef ['=' test])* [',' '**' vfpdef] | '**' vfpdef) + ### | vfpdef ['=' test] (',' vfpdef ['=' test])* [',']) + ### vfpdef: NAME + def f1(): pass + f1() + f1(*()) + f1(*(), **{}) + def f2(one_argument): pass + def f3(two, arguments): pass + self.assertEqual(f2.__code__.co_varnames, ('one_argument',)) + self.assertEqual(f3.__code__.co_varnames, ('two', 'arguments')) + def a1(one_arg,): pass + def a2(two, args,): pass + def v0(*rest): pass + def v1(a, *rest): pass + def v2(a, b, *rest): pass + + f1() + f2(1) + f2(1,) + f3(1, 2) + f3(1, 2,) + v0() + v0(1) + v0(1,) + v0(1, 2) + v0(1, 2, 3, 4, 5, 6, 7, 8, 9, 0) + v1(1) + v1(1,) + v1(1, 2) + v1(1, 2, 3) + v1(1, 2, 3, 4, 5, 6, 7, 8, 9, 0) + v2(1, 2) + v2(1, 2, 3) + v2(1, 2, 3, 4) + v2(1, 2, 3, 4, 5, 6, 7, 8, 9, 0) + + def d01(a=1): pass + d01() + d01(1) + d01(*(1,)) + d01(*[] or [2]) + d01(*() or (), *{} and (), **() or {}) + d01(**{'a': 2}) + d01(**{'a': 2} or {}) + def d11(a, b=1): pass + d11(1) + d11(1, 2) + d11(1, **{'b': 2}) + def d21(a, b, c=1): pass + d21(1, 2) + d21(1, 2, 3) + d21(*(1, 2, 3)) + d21(1, *(2, 3)) + d21(1, 2, *(3,)) + d21(1, 2, **{'c': 3}) + def d02(a=1, b=2): pass + d02() + d02(1) + d02(1, 2) + d02(*(1, 2)) + d02(1, *(2,)) + d02(1, **{'b': 2}) + d02(**{'a': 1, 'b': 2}) + def d12(a, b=1, c=2): pass + d12(1) + d12(1, 2) + d12(1, 2, 3) + def d22(a, b, c=1, d=2): pass + d22(1, 2) + d22(1, 2, 3) + d22(1, 2, 3, 4) + def d01v(a=1, *rest): pass + d01v() + d01v(1) + d01v(1, 2) + d01v(*(1, 2, 3, 4)) + d01v(*(1,)) + d01v(**{'a': 2}) + def d11v(a, b=1, *rest): pass + d11v(1) + d11v(1, 2) + d11v(1, 2, 3) + def d21v(a, b, c=1, *rest): pass + d21v(1, 2) + d21v(1, 2, 3) + d21v(1, 2, 3, 4) + d21v(*(1, 2, 3, 4)) + d21v(1, 2, **{'c': 3}) + def d02v(a=1, b=2, *rest): pass + d02v() + d02v(1) + d02v(1, 2) + d02v(1, 2, 3) + d02v(1, *(2, 3, 4)) + d02v(**{'a': 1, 'b': 2}) + def d12v(a, b=1, c=2, *rest): pass + d12v(1) + d12v(1, 2) + d12v(1, 2, 3) + d12v(1, 2, 3, 4) + d12v(*(1, 2, 3, 4)) + d12v(1, 2, *(3, 4, 5)) + d12v(1, *(2,), **{'c': 3}) + def d22v(a, b, c=1, d=2, *rest): pass + d22v(1, 2) + d22v(1, 2, 3) + d22v(1, 2, 3, 4) + d22v(1, 2, 3, 4, 5) + d22v(*(1, 2, 3, 4)) + d22v(1, 2, *(3, 4, 5)) + d22v(1, *(2, 3), **{'d': 4}) + + # keyword argument type tests + try: + str('x', **{b'foo': 1}) + except TypeError: + pass + else: + self.fail('Bytes should not work as keyword argument names') + # keyword only argument tests + def pos0key1(*, key): return key + pos0key1(key=100) + def pos2key2(p1, p2, *, k1, k2=100): return p1, p2, k1, k2 + pos2key2(1, 2, k1=100) + pos2key2(1, 2, k1=100, k2=200) + pos2key2(1, 2, k2=100, k1=200) + def pos2key2dict(p1, p2, *, k1=100, k2, **kwarg): return p1, p2, k1, k2, kwarg + pos2key2dict(1, 2, k2=100, tokwarg1=100, tokwarg2=200) + pos2key2dict(1, 2, tokwarg1=100, tokwarg2=200, k2=100) + + self.assertRaises(SyntaxError, eval, "def f(*): pass") + self.assertRaises(SyntaxError, eval, "def f(*,): pass") + self.assertRaises(SyntaxError, eval, "def f(*, **kwds): pass") + + # keyword arguments after *arglist + def f(*args, **kwargs): + return args, kwargs + self.assertEqual(f(1, x=2, *[3, 4], y=5), ((1, 3, 4), + {'x': 2, 'y': 5})) + self.assertEqual(f(1, *(2, 3), 4), ((1, 2, 3, 4), {})) + self.assertRaises(SyntaxError, eval, "f(1, x=2, *(3,4), x=5)") + self.assertEqual(f(**{'eggs': 'scrambled', 'spam': 'fried'}), + ((), {'eggs': 'scrambled', 'spam': 'fried'})) + self.assertEqual(f(spam='fried', **{'eggs': 'scrambled'}), + ((), {'eggs': 'scrambled', 'spam': 'fried'})) + + # Check ast errors in *args and *kwargs + check_syntax_error(self, "f(*g(1=2))") + check_syntax_error(self, "f(**g(1=2))") + + # argument annotation tests + def f(x) -> list: pass + self.assertEqual(f.__annotations__, {'return': list}) + def f(x: int): pass + self.assertEqual(f.__annotations__, {'x': int}) + def f(*x: str): pass + self.assertEqual(f.__annotations__, {'x': str}) + def f(**x: float): pass + self.assertEqual(f.__annotations__, {'x': float}) + def f(x, y: 1 + 2): pass + self.assertEqual(f.__annotations__, {'y': 3}) + def f(a, b: 1, c: 2, d): pass + self.assertEqual(f.__annotations__, {'b': 1, 'c': 2}) + def f(a, b: 1, c: 2, d, e: 3 = 4, f=5, *g: 6): pass + self.assertEqual(f.__annotations__, + {'b': 1, 'c': 2, 'e': 3, 'g': 6}) + def f(a, b: 1, c: 2, d, e: 3 = 4, f=5, *g: 6, h: 7, i=8, j: 9 = 10, + **k: 11) -> 12: pass + self.assertEqual(f.__annotations__, + {'b': 1, 'c': 2, 'e': 3, 'g': 6, 'h': 7, 'j': 9, + 'k': 11, 'return': 12}) + # Check for issue #20625 -- annotations mangling + class Spam: + def f(self, *, __kw: 1): + pass + class Ham(Spam): pass + self.assertEqual(Spam.f.__annotations__, {'_Spam__kw': 1}) + self.assertEqual(Ham.f.__annotations__, {'_Spam__kw': 1}) + # Check for SF Bug #1697248 - mixing decorators and a return annotation + def null(x): return x + @null + def f(x) -> list: pass + self.assertEqual(f.__annotations__, {'return': list}) + + # test closures with a variety of opargs + closure = 1 + def f(): return closure + def f(x=1): return closure + def f(*, k=1): return closure + def f() -> int: return closure + + # Check trailing commas are permitted in funcdef argument list + def f(a,): pass + def f(*args,): pass + def f(**kwds,): pass + def f(a, *args,): pass + def f(a, **kwds,): pass + def f(*args, b,): pass + def f(*, b,): pass + def f(*args, **kwds,): pass + def f(a, *args, b,): pass + def f(a, *, b,): pass + def f(a, *args, **kwds,): pass + def f(*args, b, **kwds,): pass + def f(*, b, **kwds,): pass + def f(a, *args, b, **kwds,): pass + def f(a, *, b, **kwds,): pass + + def test_lambdef(self): + ### lambdef: 'lambda' [varargslist] ':' test + l1 = lambda: 0 + self.assertEqual(l1(), 0) + l2 = lambda: a[d] # XXX just testing the expression + l3 = lambda: [2 < x for x in [-1, 3, 0]] + self.assertEqual(l3(), [0, 1, 0]) + l4 = lambda x=lambda y=lambda z=1: z: y(): x() + self.assertEqual(l4(), 1) + l5 = lambda x, y, z=2: x + y + z + self.assertEqual(l5(1, 2), 5) + self.assertEqual(l5(1, 2, 3), 6) + check_syntax_error(self, "lambda x: x = 2") + check_syntax_error(self, "lambda (None,): None") + l6 = lambda x, y, *, k=20: x + y + k + self.assertEqual(l6(1, 2), 1 + 2 + 20) + self.assertEqual(l6(1, 2, k=10), 1 + 2 + 10) + + # check that trailing commas are permitted + l10 = lambda a,: 0 + l11 = lambda *args,: 0 + l12 = lambda **kwds,: 0 + l13 = lambda a, *args,: 0 + l14 = lambda a, **kwds,: 0 + l15 = lambda *args, b,: 0 + l16 = lambda *, b,: 0 + l17 = lambda *args, **kwds,: 0 + l18 = lambda a, *args, b,: 0 + l19 = lambda a, *, b,: 0 + l20 = lambda a, *args, **kwds,: 0 + l21 = lambda *args, b, **kwds,: 0 + l22 = lambda *, b, **kwds,: 0 + l23 = lambda a, *args, b, **kwds,: 0 + l24 = lambda a, *, b, **kwds,: 0 + + + ### stmt: simple_stmt | compound_stmt + # Tested below + + def test_simple_stmt(self): + ### simple_stmt: small_stmt (';' small_stmt)* [';'] + x = 1; pass; del x + def foo(): + # verify statements that end with semi-colons + x = 1; pass; del x; + foo() + + ### small_stmt: expr_stmt | pass_stmt | del_stmt | flow_stmt | import_stmt | global_stmt | access_stmt + # Tested below + + def test_expr_stmt(self): + # (exprlist '=')* exprlist + 1 + 1, 2, 3 + x = 1 + x = 1, 2, 3 + x = y = z = 1, 2, 3 + x, y, z = 1, 2, 3 + abc = a, b, c = x, y, z = xyz = 1, 2, (3, 4) + + check_syntax_error(self, "x + 1 = 1") + check_syntax_error(self, "a + 1 = b + 2") + + # Check the heuristic for print & exec covers significant cases + # As well as placing some limits on false positives + def test_former_statements_refer_to_builtins(self): + keywords = "print", "exec" + # Cases where we want the custom error + cases = [ + "{} foo", + "{} {{1:foo}}", + "if 1: {} foo", + "if 1: {} {{1:foo}}", + "if 1:\n {} foo", + "if 1:\n {} {{1:foo}}", + ] + for keyword in keywords: + custom_msg = "call to '{}'".format(keyword) + for case in cases: + source = case.format(keyword) + with self.subTest(source=source): + with self.assertRaisesRegex(SyntaxError, custom_msg): + exec(source) + source = source.replace("foo", "(foo.)") + with self.subTest(source=source): + with self.assertRaisesRegex(SyntaxError, "invalid syntax"): + exec(source) + + def test_del_stmt(self): + # 'del' exprlist + abc = [1, 2, 3] + x, y, z = abc + xyz = x, y, z + + del abc + del x, y, (z, xyz) + + def test_pass_stmt(self): + # 'pass' + pass + + # flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt + # Tested below + + def test_break_stmt(self): + # 'break' + while 1: break + + def test_continue_stmt(self): + # 'continue' + i = 1 + while i: i = 0; continue + + msg = "" + while not msg: + msg = "ok" + try: + continue + msg = "continue failed to continue inside try" + except: + msg = "continue inside try called except block" + if msg != "ok": + self.fail(msg) + + msg = "" + while not msg: + msg = "finally block not called" + try: + continue + finally: + msg = "ok" + if msg != "ok": + self.fail(msg) + + def test_break_continue_loop(self): + # This test warrants an explanation. It is a test specifically for SF bugs + # #463359 and #462937. The bug is that a 'break' statement executed or + # exception raised inside a try/except inside a loop, *after* a continue + # statement has been executed in that loop, will cause the wrong number of + # arguments to be popped off the stack and the instruction pointer reset to + # a very small number (usually 0.) Because of this, the following test + # *must* written as a function, and the tracking vars *must* be function + # arguments with default values. Otherwise, the test will loop and loop. + + def test_inner(extra_burning_oil=1, count=0): + big_hippo = 2 + while big_hippo: + count += 1 + try: + if extra_burning_oil and big_hippo == 1: + extra_burning_oil -= 1 + break + big_hippo -= 1 + continue + except: + raise + if count > 2 or big_hippo != 1: + self.fail("continue then break in try/except in loop broken!") + test_inner() + + def test_return(self): + # 'return' [testlist] + def g1(): return + def g2(): return 1 + g1() + x = g2() + check_syntax_error(self, "class foo:return 1") + + def test_break_in_finally(self): + count = 0 + while count < 2: + count += 1 + try: + pass + finally: + break + self.assertEqual(count, 1) + + count = 0 + while count < 2: + count += 1 + try: + continue + finally: + break + self.assertEqual(count, 1) + + count = 0 + while count < 2: + count += 1 + try: + 1 / 0 + finally: + break + self.assertEqual(count, 1) + + for count in [0, 1]: + self.assertEqual(count, 0) + try: + pass + finally: + break + self.assertEqual(count, 0) + + for count in [0, 1]: + self.assertEqual(count, 0) + try: + continue + finally: + break + self.assertEqual(count, 0) + + for count in [0, 1]: + self.assertEqual(count, 0) + try: + 1 / 0 + finally: + break + self.assertEqual(count, 0) + + def test_continue_in_finally(self): + count = 0 + while count < 2: + count += 1 + try: + pass + finally: + continue + break + self.assertEqual(count, 2) + + count = 0 + while count < 2: + count += 1 + try: + break + finally: + continue + self.assertEqual(count, 2) + + count = 0 + while count < 2: + count += 1 + try: + 1 / 0 + finally: + continue + break + self.assertEqual(count, 2) + + for count in [0, 1]: + try: + pass + finally: + continue + break + self.assertEqual(count, 1) + + for count in [0, 1]: + try: + break + finally: + continue + self.assertEqual(count, 1) + + for count in [0, 1]: + try: + 1 / 0 + finally: + continue + break + self.assertEqual(count, 1) + + def test_return_in_finally(self): + def g1(): + try: + pass + finally: + return 1 + self.assertEqual(g1(), 1) + + def g2(): + try: + return 2 + finally: + return 3 + self.assertEqual(g2(), 3) + + def g3(): + try: + 1 / 0 + finally: + return 4 + self.assertEqual(g3(), 4) + + def test_yield(self): + # Allowed as standalone statement + def g(): yield 1 + def g(): yield from () + # Allowed as RHS of assignment + def g(): x = yield 1 + def g(): x = yield from () + # Ordinary yield accepts implicit tuples + def g(): yield 1, 1 + def g(): x = yield 1, 1 + # 'yield from' does not + check_syntax_error(self, "def g(): yield from (), 1") + check_syntax_error(self, "def g(): x = yield from (), 1") + # Requires parentheses as subexpression + def g(): 1, (yield 1) + def g(): 1, (yield from ()) + check_syntax_error(self, "def g(): 1, yield 1") + check_syntax_error(self, "def g(): 1, yield from ()") + # Requires parentheses as call argument + def g(): f((yield 1)) + def g(): f((yield 1), 1) + def g(): f((yield from ())) + def g(): f((yield from ()), 1) + check_syntax_error(self, "def g(): f(yield 1)") + check_syntax_error(self, "def g(): f(yield 1, 1)") + check_syntax_error(self, "def g(): f(yield from ())") + check_syntax_error(self, "def g(): f(yield from (), 1)") + # Not allowed at top level + check_syntax_error(self, "yield") + check_syntax_error(self, "yield from") + # Not allowed at class scope + check_syntax_error(self, "class foo:yield 1") + check_syntax_error(self, "class foo:yield from ()") + # Check annotation refleak on SyntaxError + check_syntax_error(self, "def g(a:(yield)): pass") + + def test_yield_in_comprehensions(self): + # Check yield in comprehensions + def g(): [x for x in [(yield 1)]] + def g(): [x for x in [(yield from ())]] + + check = self.check_syntax_error + check("def g(): [(yield x) for x in ()]", + "'yield' inside list comprehension") + check("def g(): [x for x in () if not (yield x)]", + "'yield' inside list comprehension") + check("def g(): [y for x in () for y in [(yield x)]]", + "'yield' inside list comprehension") + check("def g(): {(yield x) for x in ()}", + "'yield' inside set comprehension") + check("def g(): {(yield x): x for x in ()}", + "'yield' inside dict comprehension") + check("def g(): {x: (yield x) for x in ()}", + "'yield' inside dict comprehension") + check("def g(): ((yield x) for x in ())", + "'yield' inside generator expression") + check("def g(): [(yield from x) for x in ()]", + "'yield' inside list comprehension") + check("class C: [(yield x) for x in ()]", + "'yield' inside list comprehension") + check("[(yield x) for x in ()]", + "'yield' inside list comprehension") + + def test_raise(self): + # 'raise' test [',' test] + try: raise RuntimeError('just testing') + except RuntimeError: pass + try: raise KeyboardInterrupt + except KeyboardInterrupt: pass + + def test_import(self): + # 'import' dotted_as_names + import sys + import time, sys + # 'from' dotted_name 'import' ('*' | '(' import_as_names ')' | import_as_names) + from time import time + from time import (time) + # not testable inside a function, but already done at top of the module + # from sys import * + from sys import path, argv + from sys import (path, argv) + from sys import (path, argv,) + + def test_global(self): + # 'global' NAME (',' NAME)* + global a + global a, b + global one, two, three, four, five, six, seven, eight, nine, ten + + def test_nonlocal(self): + # 'nonlocal' NAME (',' NAME)* + x = 0 + y = 0 + def f(): + nonlocal x + nonlocal x, y + + def test_assert(self): + # assertTruestmt: 'assert' test [',' test] + assert 1 + assert 1, 1 + assert lambda x: x + assert 1, lambda x: x + 1 + + try: + assert True + except AssertionError as e: + self.fail("'assert True' should not have raised an AssertionError") + + try: + assert True, 'this should always pass' + except AssertionError as e: + self.fail("'assert True, msg' should not have " + "raised an AssertionError") + + # these tests fail if python is run with -O, so check __debug__ + @unittest.skipUnless(__debug__, "Won't work if __debug__ is False") + def testAssert2(self): + try: + assert 0, "msg" + except AssertionError as e: + self.assertEqual(e.args[0], "msg") + else: + self.fail("AssertionError not raised by assert 0") + + try: + assert False + except AssertionError as e: + self.assertEqual(len(e.args), 0) + else: + self.fail("AssertionError not raised by 'assert False'") + + + ### compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | funcdef | classdef + # Tested below + + def test_if(self): + # 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite] + if 1: pass + if 1: pass + else: pass + if 0: pass + elif 0: pass + if 0: pass + elif 0: pass + elif 0: pass + elif 0: pass + else: pass + + def test_while(self): + # 'while' test ':' suite ['else' ':' suite] + while 0: pass + while 0: pass + else: pass + + # Issue1920: "while 0" is optimized away, + # ensure that the "else" clause is still present. + x = 0 + while 0: + x = 1 + else: + x = 2 + self.assertEqual(x, 2) + + def test_for(self): + # 'for' exprlist 'in' exprlist ':' suite ['else' ':' suite] + for i in 1, 2, 3: pass + for i, j, k in (): pass + else: pass + class Squares: + def __init__(self, max): + self.max = max + self.sofar = [] + def __len__(self): return len(self.sofar) + def __getitem__(self, i): + if not 0 <= i < self.max: raise IndexError + n = len(self.sofar) + while n <= i: + self.sofar.append(n * n) + n = n + 1 + return self.sofar[i] + n = 0 + for x in Squares(10): n = n + x + if n != 285: + self.fail('for over growing sequence') + + result = [] + for x, in [(1,), (2,), (3,)]: + result.append(x) + self.assertEqual(result, [1, 2, 3]) + + def test_try(self): + ### try_stmt: 'try' ':' suite (except_clause ':' suite)+ ['else' ':' suite] + ### | 'try' ':' suite 'finally' ':' suite + ### except_clause: 'except' [expr ['as' expr]] + try: + 1 / 0 + except ZeroDivisionError: + pass + else: + pass + try: 1 / 0 + except EOFError: pass + except TypeError as msg: pass + except: pass + else: pass + try: 1 / 0 + except (EOFError, TypeError, ZeroDivisionError): pass + try: 1 / 0 + except (EOFError, TypeError, ZeroDivisionError) as msg: pass + try: pass + finally: pass + + def test_suite(self): + # simple_stmt | NEWLINE INDENT NEWLINE* (stmt NEWLINE*)+ DEDENT + if 1: pass + if 1: + pass + if 1: + # + # + # + pass + pass + # + pass + # + + def test_test(self): + ### and_test ('or' and_test)* + ### and_test: not_test ('and' not_test)* + ### not_test: 'not' not_test | comparison + if not 1: pass + if 1 and 1: pass + if 1 or 1: pass + if not not not 1: pass + if not 1 and 1 and 1: pass + if 1 and 1 or 1 and 1 and 1 or not 1 and 1: pass + + def test_comparison(self): + ### comparison: expr (comp_op expr)* + ### comp_op: '<'|'>'|'=='|'>='|'<='|'!='|'in'|'not' 'in'|'is'|'is' 'not' + if 1: pass + x = (1 == 1) + if 1 == 1: pass + if 1 != 1: pass + if 1 < 1: pass + if 1 > 1: pass + if 1 <= 1: pass + if 1 >= 1: pass + if 1 is 1: pass + if 1 is not 1: pass + if 1 in (): pass + if 1 not in (): pass + if 1 < 1 > 1 == 1 >= 1 <= 1 != 1 in 1 not in 1 is 1 is not 1: pass + + def test_binary_mask_ops(self): + x = 1 & 1 + x = 1 ^ 1 + x = 1 | 1 + + def test_shift_ops(self): + x = 1 << 1 + x = 1 >> 1 + x = 1 << 1 >> 1 + + def test_additive_ops(self): + x = 1 + x = 1 + 1 + x = 1 - 1 - 1 + x = 1 - 1 + 1 - 1 + 1 + + def test_multiplicative_ops(self): + x = 1 * 1 + x = 1 / 1 + x = 1 % 1 + x = 1 / 1 * 1 % 1 + + def test_unary_ops(self): + x = +1 + x = -1 + x = ~1 + x = ~1 ^ 1 & 1 | 1 & 1 ^ -1 + x = -1 * 1 / 1 + 1 * 1 - -1 * 1 + + def test_selectors(self): + ### trailer: '(' [testlist] ')' | '[' subscript ']' | '.' NAME + ### subscript: expr | [expr] ':' [expr] + + import sys, time + c = sys.path[0] + x = time.time() + x = sys.modules['time'].time() + a = '01234' + c = a[0] + c = a[-1] + s = a[0:5] + s = a[:5] + s = a[0:] + s = a[:] + s = a[-5:] + s = a[:-1] + s = a[-4:-3] + # A rough test of SF bug 1333982. http://python.org/sf/1333982 + # The testing here is fairly incomplete. + # Test cases should include: commas with 1 and 2 colons + d = {} + d[1] = 1 + d[1,] = 2 + d[1, 2] = 3 + d[1, 2, 3] = 4 + L = list(d) + L.sort(key=lambda x: (type(x).__name__, x)) + self.assertEqual(str(L), '[1, (1,), (1, 2), (1, 2, 3)]') + + def test_atoms(self): + ### atom: '(' [testlist] ')' | '[' [testlist] ']' | '{' [dictsetmaker] '}' | NAME | NUMBER | STRING + ### dictsetmaker: (test ':' test (',' test ':' test)* [',']) | (test (',' test)* [',']) + + x = (1) + x = (1 or 2 or 3) + x = (1 or 2 or 3, 2, 3) + + x = [] + x = [1] + x = [1 or 2 or 3] + x = [1 or 2 or 3, 2, 3] + x = [] + + x = {} + x = {'one': 1} + x = {'one': 1,} + x = {'one' or 'two': 1 or 2} + x = {'one': 1, 'two': 2} + x = {'one': 1, 'two': 2,} + x = {'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6} + + x = {'one'} + x = {'one', 1,} + x = {'one', 'two', 'three'} + x = {2, 3, 4,} + + x = x + x = 'x' + x = 123 + + ### exprlist: expr (',' expr)* [','] + ### testlist: test (',' test)* [','] + # These have been exercised enough above + + def test_classdef(self): + # 'class' NAME ['(' [testlist] ')'] ':' suite + class B: pass + class B2(): pass + class C1(B): pass + class C2(B): pass + class D(C1, C2, B): pass + class C: + def meth1(self): pass + def meth2(self, arg): pass + def meth3(self, a1, a2): pass + + # decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE + # decorators: decorator+ + # decorated: decorators (classdef | funcdef) + def class_decorator(x): return x + @class_decorator + class G: pass + + def test_dictcomps(self): + # dictorsetmaker: ( (test ':' test (comp_for | + # (',' test ':' test)* [','])) | + # (test (comp_for | (',' test)* [','])) ) + nums = [1, 2, 3] + self.assertEqual({i: i + 1 for i in nums}, {1: 2, 2: 3, 3: 4}) + + def test_listcomps(self): + # list comprehension tests + nums = [1, 2, 3, 4, 5] + strs = ["Apple", "Banana", "Coconut"] + spcs = [" Apple", " Banana ", "Coco nut "] + + self.assertEqual([s.strip() for s in spcs], ['Apple', 'Banana', 'Coco nut']) + self.assertEqual([3 * x for x in nums], [3, 6, 9, 12, 15]) + self.assertEqual([x for x in nums if x > 2], [3, 4, 5]) + self.assertEqual([(i, s) for i in nums for s in strs], + [(1, 'Apple'), (1, 'Banana'), (1, 'Coconut'), + (2, 'Apple'), (2, 'Banana'), (2, 'Coconut'), + (3, 'Apple'), (3, 'Banana'), (3, 'Coconut'), + (4, 'Apple'), (4, 'Banana'), (4, 'Coconut'), + (5, 'Apple'), (5, 'Banana'), (5, 'Coconut')]) + self.assertEqual([(i, s) for i in nums for s in [f for f in strs if "n" in f]], + [(1, 'Banana'), (1, 'Coconut'), (2, 'Banana'), (2, 'Coconut'), + (3, 'Banana'), (3, 'Coconut'), (4, 'Banana'), (4, 'Coconut'), + (5, 'Banana'), (5, 'Coconut')]) + self.assertEqual([(lambda a:[a ** i for i in range(a + 1)])(j) for j in range(5)], + [[1], [1, 1], [1, 2, 4], [1, 3, 9, 27], [1, 4, 16, 64, 256]]) + + def test_in_func(l): + return [0 < x < 3 for x in l if x > 2] + + self.assertEqual(test_in_func(nums), [False, False, False]) + + def test_nested_front(): + self.assertEqual([[y for y in [x, x + 1]] for x in [1, 3, 5]], + [[1, 2], [3, 4], [5, 6]]) + + test_nested_front() + + check_syntax_error(self, "[i, s for i in nums for s in strs]") + check_syntax_error(self, "[x if y]") + + suppliers = [ + (1, "Boeing"), + (2, "Ford"), + (3, "Macdonalds") + ] + + parts = [ + (10, "Airliner"), + (20, "Engine"), + (30, "Cheeseburger") + ] + + suppart = [ + (1, 10), (1, 20), (2, 20), (3, 30) + ] + + x = [ + (sname, pname) + for (sno, sname) in suppliers + for (pno, pname) in parts + for (sp_sno, sp_pno) in suppart + if sno == sp_sno and pno == sp_pno + ] + + self.assertEqual(x, [('Boeing', 'Airliner'), ('Boeing', 'Engine'), ('Ford', 'Engine'), + ('Macdonalds', 'Cheeseburger')]) + + def test_genexps(self): + # generator expression tests + g = ([x for x in range(10)] for x in range(1)) + self.assertEqual(next(g), [x for x in range(10)]) + try: + next(g) + self.fail('should produce StopIteration exception') + except StopIteration: + pass + + a = 1 + try: + g = (a for d in a) + next(g) + self.fail('should produce TypeError') + except TypeError: + pass + + self.assertEqual(list((x, y) for x in 'abcd' for y in 'abcd'), [(x, y) for x in 'abcd' for y in 'abcd']) + self.assertEqual(list((x, y) for x in 'ab' for y in 'xy'), [(x, y) for x in 'ab' for y in 'xy']) + + a = [x for x in range(10)] + b = (x for x in (y for y in a)) + self.assertEqual(sum(b), sum([x for x in range(10)])) + + self.assertEqual(sum(x ** 2 for x in range(10)), sum([x ** 2 for x in range(10)])) + self.assertEqual(sum(x * x for x in range(10) if x % 2), sum([x * x for x in range(10) if x % 2])) + self.assertEqual(sum(x for x in (y for y in range(10))), sum([x for x in range(10)])) + self.assertEqual(sum(x for x in (y for y in (z for z in range(10)))), sum([x for x in range(10)])) + self.assertEqual(sum(x for x in [y for y in (z for z in range(10))]), sum([x for x in range(10)])) + self.assertEqual(sum(x for x in (y for y in (z for z in range(10) if True)) if True), sum([x for x in range(10)])) + self.assertEqual(sum(x for x in (y for y in (z for z in range(10) if True) if False) if True), 0) + check_syntax_error(self, "foo(x for x in range(10), 100)") + check_syntax_error(self, "foo(100, x for x in range(10))") + + def test_comprehension_specials(self): + # test for outmost iterable precomputation + x = 10; g = (i for i in range(x)); x = 5 + self.assertEqual(len(list(g)), 10) + + # This should hold, since we're only precomputing outmost iterable. + x = 10; t = False; g = ((i, j) for i in range(x) if t for j in range(x)) + x = 5; t = True; + self.assertEqual([(i, j) for i in range(10) for j in range(5)], list(g)) + + # Grammar allows multiple adjacent 'if's in listcomps and genexps, + # even though it's silly. Make sure it works (ifelse broke this.) + self.assertEqual([x for x in range(10) if x % 2 if x % 3], [1, 5, 7]) + self.assertEqual(list(x for x in range(10) if x % 2 if x % 3), [1, 5, 7]) + + # verify unpacking single element tuples in listcomp/genexp. + self.assertEqual([x for x, in [(4,), (5,), (6,)]], [4, 5, 6]) + self.assertEqual(list(x for x, in [(7,), (8,), (9,)]), [7, 8, 9]) + + def test_with_statement(self): + class manager(object): + def __enter__(self): + return (1, 2) + def __exit__(self, *args): + pass + + with manager(): + pass + with manager() as x: + pass + with manager() as (x, y): + pass + with manager(), manager(): + pass + with manager() as x, manager() as y: + pass + with manager() as x, manager(): + pass + + def test_if_else_expr(self): + # Test ifelse expressions in various cases + def _checkeval(msg, ret): + "helper to check that evaluation of expressions is done correctly" + print(msg) + return ret + + # the next line is not allowed anymore + #self.assertEqual([ x() for x in lambda: True, lambda: False if x() ], [True]) + self.assertEqual([x() for x in (lambda:True, lambda:False) if x()], [True]) + self.assertEqual([x(False) for x in (lambda x:False if x else True, lambda x:True if x else False) if x(False)], [True]) + self.assertEqual((5 if 1 else _checkeval("check 1", 0)), 5) + self.assertEqual((_checkeval("check 2", 0) if 0 else 5), 5) + self.assertEqual((5 and 6 if 0 else 1), 1) + self.assertEqual(((5 and 6) if 0 else 1), 1) + self.assertEqual((5 and (6 if 1 else 1)), 6) + self.assertEqual((0 or _checkeval("check 3", 2) if 0 else 3), 3) + self.assertEqual((1 or _checkeval("check 4", 2) if 1 else _checkeval("check 5", 3)), 1) + self.assertEqual((0 or 5 if 1 else _checkeval("check 6", 3)), 5) + self.assertEqual((not 5 if 1 else 1), False) + self.assertEqual((not 5 if 0 else 1), 1) + self.assertEqual((6 + 1 if 1 else 2), 7) + self.assertEqual((6 - 1 if 1 else 2), 5) + self.assertEqual((6 * 2 if 1 else 4), 12) + self.assertEqual((6 / 2 if 1 else 3), 3) + self.assertEqual((6 < 4 if 0 else 2), 2) + + def test_paren_evaluation(self): + self.assertEqual(16 // (4 // 2), 8) + self.assertEqual((16 // 4) // 2, 2) + self.assertEqual(16 // 4 // 2, 2) + self.assertTrue(False is (2 is 3)) + self.assertFalse((False is 2) is 3) + self.assertFalse(False is 2 is 3) + + def test_matrix_mul(self): + # This is not intended to be a comprehensive test, rather just to be few + # samples of the @ operator in test_grammar.py. + class M: + def __matmul__(self, o): + return 4 + def __imatmul__(self, o): + self.other = o + return self + m = M() + self.assertEqual(m @ m, 4) + m @= 42 + self.assertEqual(m.other, 42) + + def test_async_await(self): + async def test(): + def sum(): + pass + if 1: + await someobj() + + self.assertEqual(test.__name__, 'test') + self.assertTrue(bool(test.__code__.co_flags & inspect.CO_COROUTINE)) + + def decorator(func): + setattr(func, '_marked', True) + return func + + @decorator + async def test2(): + return 22 + self.assertTrue(test2._marked) + self.assertEqual(test2.__name__, 'test2') + self.assertTrue(bool(test2.__code__.co_flags & inspect.CO_COROUTINE)) + + def test_async_for(self): + class Done(Exception): pass + + class AIter: + def __aiter__(self): + return self + async def __anext__(self): + raise StopAsyncIteration + + async def foo(): + async for i in AIter(): + pass + async for i, j in AIter(): + pass + async for i in AIter(): + pass + else: + pass + raise Done + + with self.assertRaises(Done): + foo().send(None) + + def test_async_with(self): + class Done(Exception): pass + + class manager: + async def __aenter__(self): + return (1, 2) + async def __aexit__(self, *exc): + return False + + async def foo(): + async with manager(): + pass + async with manager() as x: + pass + async with manager() as (x, y): + pass + async with manager(), manager(): + pass + async with manager() as x, manager() as y: + pass + async with manager() as x, manager(): + pass + raise Done + + with self.assertRaises(Done): + foo().send(None) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/hover/functionHover.py b/src/test/pythonFiles/hover/functionHover.py new file mode 100644 index 000000000000..a0f765a5a41f --- /dev/null +++ b/src/test/pythonFiles/hover/functionHover.py @@ -0,0 +1,9 @@ +def my_func(): + """ + This is a test. + + It also includes this text, too. + """ + pass + +my_func() diff --git a/src/test/pythonFiles/refactoring/source folder/with empty line.py b/src/test/pythonFiles/refactoring/source folder/with empty line.py new file mode 100644 index 000000000000..01ed75727900 --- /dev/null +++ b/src/test/pythonFiles/refactoring/source folder/with empty line.py @@ -0,0 +1,8 @@ +import os + +def one(): + return True + +def two(): + if one(): + print("A" + one()) diff --git a/src/test/pythonFiles/refactoring/source folder/without empty line.py b/src/test/pythonFiles/refactoring/source folder/without empty line.py new file mode 100644 index 000000000000..a449eb106f5c --- /dev/null +++ b/src/test/pythonFiles/refactoring/source folder/without empty line.py @@ -0,0 +1,8 @@ +import os + +def one(): + return True + +def two(): + if one(): + print("A" + one()) \ No newline at end of file diff --git a/src/test/pythonFiles/terminalExec/sample1_normalized.py b/src/test/pythonFiles/terminalExec/sample1_normalized.py new file mode 100644 index 000000000000..8591baeb6489 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample1_normalized.py @@ -0,0 +1,25 @@ +# Sample block 1 + +def square(x): + return x**2 + +print('hello') +# Sample block 2 + +a = 2 + +if a < 2: + print('less than 2') +else: + print('more than 2') + +print('hello') +# Sample block 3 + +for i in range(5): + print(i) + print(i) + print(i) + print(i) + +print('complete') diff --git a/src/test/pythonFiles/terminalExec/sample1_raw.py b/src/test/pythonFiles/terminalExec/sample1_raw.py new file mode 100644 index 000000000000..fe050c7af289 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample1_raw.py @@ -0,0 +1,24 @@ +# Sample block 1 +def square(x): + return x**2 + +print('hello') +# Sample block 2 +a = 2 +if a < 2: + print('less than 2') +else: + print('more than 2') + +print('hello') + +# Sample block 3 +for i in range(5): + print(i) + + print(i) + print(i) + + print(i) + +print('complete') diff --git a/src/test/pythonFiles/terminalExec/sample2_normalized.py b/src/test/pythonFiles/terminalExec/sample2_normalized.py new file mode 100644 index 000000000000..a333d4e0daae --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample2_normalized.py @@ -0,0 +1,7 @@ +def add(x, y): + """Adds x to y""" + # Some comment + return x + y + +v = add(1, 7) +print(v) diff --git a/src/test/pythonFiles/terminalExec/sample2_raw.py b/src/test/pythonFiles/terminalExec/sample2_raw.py new file mode 100644 index 000000000000..6ab7e67637f4 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample2_raw.py @@ -0,0 +1,8 @@ +def add(x, y): + """Adds x to y""" + # Some comment + + return x + y + +v = add(1, 7) +print(v) diff --git a/src/test/pythonFiles/terminalExec/sample3_normalized.py b/src/test/pythonFiles/terminalExec/sample3_normalized.py new file mode 100644 index 000000000000..4fa62091c66d --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample3_normalized.py @@ -0,0 +1,5 @@ +if True: + print(1) + print(2) + +print(3) diff --git a/src/test/pythonFiles/terminalExec/sample3_raw.py b/src/test/pythonFiles/terminalExec/sample3_raw.py new file mode 100644 index 000000000000..fee6c839aa89 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample3_raw.py @@ -0,0 +1,6 @@ +if True: + print(1) + + print(2) + +print(3) diff --git a/src/test/pythonFiles/terminalExec/sample4_normalized.py b/src/test/pythonFiles/terminalExec/sample4_normalized.py new file mode 100644 index 000000000000..2c49d10253ff --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample4_normalized.py @@ -0,0 +1,6 @@ +class pc(object): + def __init__(self, pcname, model): + self.pcname = pcname + self.model = model + def print_name(self): + print('Workstation name is', self.pcname, 'model is', self.model) diff --git a/src/test/pythonFiles/terminalExec/sample4_raw.py b/src/test/pythonFiles/terminalExec/sample4_raw.py new file mode 100644 index 000000000000..fbf0d68fe5f8 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample4_raw.py @@ -0,0 +1,7 @@ +class pc(object): + def __init__(self, pcname, model): + self.pcname = pcname + self.model = model + + def print_name(self): + print('Workstation name is', self.pcname, 'model is', self.model) diff --git a/src/test/pythonFiles/terminalExec/sample5_normalized.py b/src/test/pythonFiles/terminalExec/sample5_normalized.py new file mode 100644 index 000000000000..822d51bd15d9 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample5_normalized.py @@ -0,0 +1,8 @@ +for i in range(10): + print('a') + for j in range(5): + print('b') + print('b2') + for k in range(2): + print('c') + print('done with first loop') diff --git a/src/test/pythonFiles/terminalExec/sample5_raw.py b/src/test/pythonFiles/terminalExec/sample5_raw.py new file mode 100644 index 000000000000..19caa9cf26a6 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample5_raw.py @@ -0,0 +1,11 @@ +for i in range(10): + print('a') + for j in range(5): + print('b') + + print('b2') + + for k in range(2): + print('c') + + print('done with first loop') diff --git a/src/test/pythonFiles/terminalExec/sample6_normalized.py b/src/test/pythonFiles/terminalExec/sample6_normalized.py new file mode 100644 index 000000000000..242bf35abea8 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample6_normalized.py @@ -0,0 +1,15 @@ +if True: + print(1) +else: print(2) + +print('🔨') +print(3) +print(3) + +if True: + print(1) +else: print(2) + +if True: + print(1) +else: print(2) diff --git a/src/test/pythonFiles/terminalExec/sample6_raw.py b/src/test/pythonFiles/terminalExec/sample6_raw.py new file mode 100644 index 000000000000..b064ca962070 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample6_raw.py @@ -0,0 +1,12 @@ +if True: + print(1) +else: print(2) +print('🔨') +print(3) +print(3) +if True: + print(1) +else: print(2) +if True: + print(1) +else: print(2) diff --git a/src/test/pythonFiles/terminalExec/sample7_normalized.py b/src/test/pythonFiles/terminalExec/sample7_normalized.py new file mode 100644 index 000000000000..2288800fc985 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample7_normalized.py @@ -0,0 +1,8 @@ +if True: + print(1) + print(1) +else: + print(2) + print(2) + +print(3) diff --git a/src/test/pythonFiles/terminalExec/sample7_raw.py b/src/test/pythonFiles/terminalExec/sample7_raw.py new file mode 100644 index 000000000000..62d01b9659c6 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample7_raw.py @@ -0,0 +1,9 @@ +if True: + print(1) + + print(1) +else: + print(2) + + print(2) +print(3) diff --git a/src/test/pythonFiles/terminalExec/sample_normalized.py b/src/test/pythonFiles/terminalExec/sample_normalized.py new file mode 100644 index 000000000000..8ee9b90cdd27 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample_normalized.py @@ -0,0 +1,5 @@ +import sys +print(sys.executable) +print("1234") +print(1) +print(2) diff --git a/src/test/pythonFiles/terminalExec/sample_raw.py b/src/test/pythonFiles/terminalExec/sample_raw.py new file mode 100644 index 000000000000..d1b32aaf606c --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample_raw.py @@ -0,0 +1,8 @@ +import sys + +print(sys.executable) + +print("1234") + +print(1) +print(2) diff --git a/src/test/refactor/extension.refactor.extract.var.test.ts b/src/test/refactor/extension.refactor.extract.var.test.ts index 126ef1a3fee9..320d277de524 100644 --- a/src/test/refactor/extension.refactor.extract.var.test.ts +++ b/src/test/refactor/extension.refactor.extract.var.test.ts @@ -3,12 +3,13 @@ import * as assert from 'assert'; import * as fs from 'fs-extra'; import * as path from 'path'; -import * as vscode from 'vscode'; -import { Position } from 'vscode'; +import { commands, Position, Range, Selection, TextEditorCursorStyle, TextEditorLineNumbersStyle, TextEditorOptions, Uri, window, workspace } from 'vscode'; import { PythonSettings } from '../../client/common/configSettings'; import { getTextEditsFromPatch } from '../../client/common/editor'; import { extractVariable } from '../../client/providers/simpleRefactorProvider'; import { RefactorProxy } from '../../client/refactor/proxy'; +import { PythonVersionInformation } from '../../client/unittests/common/types'; +import { rootWorkspaceUri } from '../common'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; import { closeActiveWindows, initialize, initializeTest, IS_CI_SERVER } from './../initialize'; import { MockOutputChannel } from './../mockClasses'; @@ -23,13 +24,13 @@ interface RenameResponse { 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 }; + const oldExecuteCommand = commands.executeCommand; + const options: TextEditorOptions = { cursorStyle: TextEditorCursorStyle.Line, insertSpaces: true, lineNumbers: TextEditorLineNumbersStyle.Off, tabSize: 4 }; let refactorTargetFile = ''; let ioc: UnitTestIocContainer; suiteSetup(initialize); suiteTeardown(() => { - vscode.commands.executeCommand = oldExecuteCommand; + commands.executeCommand = oldExecuteCommand; return closeActiveWindows(); }); setup(async () => { @@ -37,10 +38,10 @@ suite('Variable Extraction', () => { refactorTargetFile = path.join(refactorTargetFileDir, `refactor${new Date().getTime()}.py`); fs.copySync(refactorSourceFile, refactorTargetFile, { overwrite: true }); await initializeTest(); - (vscode).commands.executeCommand = (cmd) => Promise.resolve(); + (commands).executeCommand = (cmd) => Promise.resolve(); }); teardown(async () => { - vscode.commands.executeCommand = oldExecuteCommand; + commands.executeCommand = oldExecuteCommand; try { await fs.unlink(refactorTargetFile); } catch { } @@ -55,12 +56,12 @@ suite('Variable Extraction', () => { } async function testingVariableExtraction(shouldError: boolean, startPos: Position, endPos: Position): Promise { - const pythonSettings = PythonSettings.getInstance(vscode.Uri.file(refactorTargetFile)); - const rangeOfTextToExtract = new vscode.Range(startPos, endPos); + const pythonSettings = PythonSettings.getInstance(Uri.file(refactorTargetFile)); + const rangeOfTextToExtract = new Range(startPos, endPos); const proxy = new RefactorProxy(EXTENSION_DIR, pythonSettings, path.dirname(refactorTargetFile), ioc.serviceContainer); const DIFF = '--- a/refactor.py\n+++ b/refactor.py\n@@ -232,7 +232,8 @@\n sys.stdout.flush()\n \n def watch(self):\n- self._write_response("STARTED")\n+ myNewVariable = "STARTED"\n+ self._write_response(myNewVariable)\n while True:\n try:\n self._process_request(self._input.readline())\n'; - const mockTextDoc = await vscode.workspace.openTextDocument(refactorTargetFile); + const mockTextDoc = await workspace.openTextDocument(refactorTargetFile); const expectedTextEdits = getTextEditsFromPatch(mockTextDoc.getText(), DIFF); try { const response = await proxy.extractVariable(mockTextDoc, 'myNewVariable', refactorTargetFile, rangeOfTextToExtract, options); @@ -81,27 +82,35 @@ suite('Variable Extraction', () => { } } - test('Extract Variable', async () => { - const startPos = new vscode.Position(234, 29); - const endPos = new vscode.Position(234, 38); - await testingVariableExtraction(false, startPos, endPos); + // tslint:disable-next-line:no-function-expression + test('Extract Variable', async function () { + const pyVersion: PythonVersionInformation = await ioc.getPythonMajorMinorVersion(rootWorkspaceUri); + + if (pyVersion.major === 3 && pyVersion.minor === 7) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } else { + const startPos = new Position(234, 29); + const endPos = new Position(234, 38); + await testingVariableExtraction(false, startPos, endPos); + } }); test('Extract Variable fails if whole string not selected', async () => { - const startPos = new vscode.Position(234, 20); - const endPos = new vscode.Position(234, 38); + const startPos = new Position(234, 20); + const endPos = new Position(234, 38); await testingVariableExtraction(true, startPos, endPos); }); async function testingVariableExtractionEndToEnd(shouldError: boolean, startPos: Position, endPos: Position): Promise { const ch = new MockOutputChannel('Python'); - const rangeOfTextToExtract = new vscode.Range(startPos, endPos); + const rangeOfTextToExtract = new Range(startPos, endPos); - const textDocument = await vscode.workspace.openTextDocument(refactorTargetFile); - const editor = await vscode.window.showTextDocument(textDocument); + const textDocument = await workspace.openTextDocument(refactorTargetFile); + const editor = await window.showTextDocument(textDocument); - editor.selections = [new vscode.Selection(rangeOfTextToExtract.start, rangeOfTextToExtract.end)]; - editor.selection = new vscode.Selection(rangeOfTextToExtract.start, rangeOfTextToExtract.end); + editor.selections = [new Selection(rangeOfTextToExtract.start, rangeOfTextToExtract.end)]; + editor.selection = new Selection(rangeOfTextToExtract.start, rangeOfTextToExtract.end); try { await extractVariable(EXTENSION_DIR, editor, rangeOfTextToExtract, ch, ioc.serviceContainer); if (shouldError) { @@ -125,15 +134,15 @@ suite('Variable Extraction', () => { // This test fails on linux (text document not getting updated in time) if (!IS_CI_SERVER) { test('Extract Variable (end to end)', async () => { - const startPos = new vscode.Position(234, 29); - const endPos = new vscode.Position(234, 38); + const startPos = new Position(234, 29); + const endPos = new Position(234, 38); await testingVariableExtractionEndToEnd(false, startPos, endPos); }); } test('Extract Variable fails if whole string not selected (end to end)', async () => { - const startPos = new vscode.Position(234, 20); - const endPos = new vscode.Position(234, 38); + const startPos = new Position(234, 20); + const endPos = new Position(234, 38); await testingVariableExtractionEndToEnd(true, startPos, endPos); }); }); diff --git a/src/test/refactor/rename.test.ts b/src/test/refactor/rename.test.ts new file mode 100644 index 000000000000..03bffd245899 --- /dev/null +++ b/src/test/refactor/rename.test.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { EOL } from 'os'; +import * as path from 'path'; +import * as typeMoq from 'typemoq'; +import { Range, TextEditorCursorStyle, TextEditorLineNumbersStyle, TextEditorOptions, window, workspace } from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import '../../client/common/extensions'; +import { BufferDecoder } from '../../client/common/process/decoder'; +import { ProcessService } from '../../client/common/process/proc'; +import { PythonExecutionFactory } from '../../client/common/process/pythonExecutionFactory'; +import { IProcessServiceFactory, IPythonExecutionFactory } from '../../client/common/process/types'; +import { IConfigurationService, IPythonSettings } from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { RefactorProxy } from '../../client/refactor/proxy'; +import { PYTHON_PATH } from '../common'; +import { closeActiveWindows, initialize, initializeTest } from './../initialize'; + +type RenameResponse = { + results: [{ diff: string }]; +}; + +suite('Refactor Rename', () => { + const options: TextEditorOptions = { cursorStyle: TextEditorCursorStyle.Line, insertSpaces: true, lineNumbers: TextEditorLineNumbersStyle.Off, tabSize: 4 }; + let pythonSettings: typeMoq.IMock; + let serviceContainer: typeMoq.IMock; + suiteSetup(initialize); + setup(async () => { + pythonSettings = typeMoq.Mock.ofType(); + pythonSettings.setup(p => p.pythonPath).returns(() => PYTHON_PATH); + const configService = typeMoq.Mock.ofType(); + configService.setup(c => c.getSettings(typeMoq.It.isAny())).returns(() => pythonSettings.object); + const processServiceFactory = typeMoq.Mock.ofType(); + processServiceFactory.setup(p => p.create(typeMoq.It.isAny())).returns(() => Promise.resolve(new ProcessService(new BufferDecoder()))); + + serviceContainer = typeMoq.Mock.ofType(); + serviceContainer.setup(s => s.get(typeMoq.It.isValue(IConfigurationService), typeMoq.It.isAny())).returns(() => configService.object); + serviceContainer.setup(s => s.get(typeMoq.It.isValue(IProcessServiceFactory), typeMoq.It.isAny())).returns(() => processServiceFactory.object); + serviceContainer.setup(s => s.get(typeMoq.It.isValue(IPythonExecutionFactory), typeMoq.It.isAny())).returns(() => new PythonExecutionFactory(serviceContainer.object)); + await initializeTest(); + }); + teardown(closeActiveWindows); + suiteTeardown(closeActiveWindows); + + test('Rename function in source without a trailing empty line', async () => { + const sourceFile = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'refactoring', 'source folder', 'without empty line.py'); + const expectedDiff = `--- a/${path.basename(sourceFile)}${EOL}+++ b/${path.basename(sourceFile)}${EOL}@@ -1,8 +1,8 @@${EOL} import os${EOL} ${EOL}-def one():${EOL}+def three():${EOL} return True${EOL} ${EOL} def two():${EOL}- if one():${EOL}- print(\"A\" + one())${EOL}+ if three():${EOL}+ print(\"A\" + three())${EOL}` + .splitLines({ removeEmptyEntries: false, trim: false }); + + const proxy = new RefactorProxy(EXTENSION_ROOT_DIR, pythonSettings.object, path.dirname(sourceFile), serviceContainer.object); + const textDocument = await workspace.openTextDocument(sourceFile); + await window.showTextDocument(textDocument); + + const response = await proxy.rename(textDocument, 'three', sourceFile, new Range(7, 20, 7, 23), options); + expect(response.results).to.be.lengthOf(1); + expect(response.results[0].diff.splitLines({ removeEmptyEntries: false, trim: false })).to.be.deep.equal(expectedDiff); + }); + test('Rename function in source with a trailing empty line', async () => { + const sourceFile = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'refactoring', 'source folder', 'with empty line.py'); + const expectedDiff = `--- a/${path.basename(sourceFile)}${EOL}+++ b/${path.basename(sourceFile)}${EOL}@@ -1,8 +1,8 @@${EOL} import os${EOL} ${EOL}-def one():${EOL}+def three():${EOL} return True${EOL} ${EOL} def two():${EOL}- if one():${EOL}- print(\"A\" + one())${EOL}+ if three():${EOL}+ print(\"A\" + three())${EOL}` + .splitLines({ removeEmptyEntries: false, trim: false }); + + const proxy = new RefactorProxy(EXTENSION_ROOT_DIR, pythonSettings.object, path.dirname(sourceFile), serviceContainer.object); + const textDocument = await workspace.openTextDocument(sourceFile); + await window.showTextDocument(textDocument); + + const response = await proxy.rename(textDocument, 'three', sourceFile, new Range(7, 20, 7, 23), options); + expect(response.results).to.be.lengthOf(1); + expect(response.results[0].diff.splitLines({ removeEmptyEntries: false, trim: false })).to.be.deep.equal(expectedDiff); + }); +}); diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index f38344a474bd..5ce82911dea3 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { Container } from 'inversify'; +import * as TypeMoq from 'typemoq'; import { Disposable, Memento, OutputChannel } from 'vscode'; import { STANDARD_OUTPUT_CHANNEL } from '../client/common/constants'; import { Logger } from '../client/common/logger'; @@ -16,7 +17,7 @@ import { ProcessService } from '../client/common/process/proc'; import { PythonExecutionFactory } from '../client/common/process/pythonExecutionFactory'; import { PythonToolExecutionService } from '../client/common/process/pythonToolService'; import { registerTypes as processRegisterTypes } from '../client/common/process/serviceRegistry'; -import { IBufferDecoder, IProcessService, IPythonExecutionFactory, IPythonToolExecutionService } from '../client/common/process/types'; +import { IBufferDecoder, IProcessServiceFactory, IPythonExecutionFactory, IPythonToolExecutionService } from '../client/common/process/types'; import { registerTypes as commonRegisterTypes } from '../client/common/serviceRegistry'; import { GLOBAL_MEMENTO, ICurrentProcess, IDisposableRegistry, ILogger, IMemento, IOutputChannel, IPathUtils, Is64Bit, IsWindows, WORKSPACE_MEMENTO } from '../client/common/types'; import { registerTypes as variableRegisterTypes } from '../client/common/variables/serviceRegistry'; @@ -30,7 +31,7 @@ import { TEST_OUTPUT_CHANNEL } from '../client/unittests/common/constants'; import { registerTypes as unittestsRegisterTypes } from '../client/unittests/serviceRegistry'; import { MockOutputChannel } from './mockClasses'; import { MockMemento } from './mocks/mementos'; -import { IOriginalProcessService, MockProcessService } from './mocks/proc'; +import { MockProcessService } from './mocks/proc'; import { MockProcess } from './mocks/process'; export class IocContainer { @@ -93,8 +94,11 @@ export class IocContainer { } public registerMockProcessTypes() { this.serviceManager.addSingleton(IBufferDecoder, BufferDecoder); - this.serviceManager.addSingleton(IOriginalProcessService, ProcessService); - this.serviceManager.addSingleton(IProcessService, MockProcessService); + const processServiceFactory = TypeMoq.Mock.ofType(); + // tslint:disable-next-line:no-any + const processService = new MockProcessService(new ProcessService(new BufferDecoder(), process.env as any)); + processServiceFactory.setup(f => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService)); + this.serviceManager.addSingletonInstance(IProcessServiceFactory, processServiceFactory.object); this.serviceManager.addSingleton(IPythonExecutionFactory, PythonExecutionFactory); this.serviceManager.addSingleton(IPythonToolExecutionService, PythonToolExecutionService); } diff --git a/src/test/signature/signature.jedi.test.ts b/src/test/signature/signature.jedi.test.ts new file mode 100644 index 000000000000..a81dbd8d1813 --- /dev/null +++ b/src/test/signature/signature.jedi.test.ts @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { rootWorkspaceUri } from '../common'; +import { IsLanguageServerTest } from '../constants'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { UnitTestIocContainer } from '../unittests/serviceRegistry'; + +const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'signature'); + +class SignatureHelpResult { + constructor( + public line: number, + public index: number, + public signaturesCount: number, + public activeParameter: number, + public parameterName: string | null) { } +} + +// tslint:disable-next-line:max-func-body-length +suite('Signatures (Jedi)', () => { + let isPython2: boolean; + let ioc: UnitTestIocContainer; + suiteSetup(async function () { + if (IsLanguageServerTest()) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + initializeDI(); + isPython2 = await ioc.getPythonMajorVersion(rootWorkspaceUri) === 2; + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + ioc.dispose(); + }); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + } + + test('For ctor', async () => { + const expected = [ + new SignatureHelpResult(5, 11, 0, 0, null), + new SignatureHelpResult(5, 12, 1, 0, 'name'), + new SignatureHelpResult(5, 13, 0, 0, null), + new SignatureHelpResult(5, 14, 0, 0, null), + new SignatureHelpResult(5, 15, 0, 0, null), + new SignatureHelpResult(5, 16, 0, 0, null), + new SignatureHelpResult(5, 17, 0, 0, null), + new SignatureHelpResult(5, 18, 1, 1, 'age'), + new SignatureHelpResult(5, 19, 1, 1, 'age'), + new SignatureHelpResult(5, 20, 0, 0, null) + ]; + + const document = await openDocument(path.join(autoCompPath, 'classCtor.py')); + for (let i = 0; i < expected.length; i += 1) { + await checkSignature(expected[i], document!.uri, i); + } + }); + + test('For intrinsic', async () => { + const expected = [ + new SignatureHelpResult(0, 0, 0, 0, null), + new SignatureHelpResult(0, 1, 0, 0, null), + new SignatureHelpResult(0, 2, 0, 0, null), + new SignatureHelpResult(0, 3, 0, 0, null), + new SignatureHelpResult(0, 4, 0, 0, null), + new SignatureHelpResult(0, 5, 0, 0, null), + new SignatureHelpResult(0, 6, 1, 0, 'stop'), + new SignatureHelpResult(0, 7, 1, 0, 'stop') + // new SignatureHelpResult(0, 6, 1, 0, 'start'), + // new SignatureHelpResult(0, 7, 1, 0, 'start'), + // new SignatureHelpResult(0, 8, 1, 1, 'stop'), + // new SignatureHelpResult(0, 9, 1, 1, 'stop'), + // new SignatureHelpResult(0, 10, 1, 1, 'stop'), + // new SignatureHelpResult(0, 11, 1, 2, 'step'), + // new SignatureHelpResult(1, 0, 1, 2, 'step') + ]; + + const document = await openDocument(path.join(autoCompPath, 'basicSig.py')); + for (let i = 0; i < expected.length; i += 1) { + await checkSignature(expected[i], document!.uri, i); + } + }); + + test('For ellipsis', async function () { + if (isPython2) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + return; + } + const expected = [ + new SignatureHelpResult(0, 5, 0, 0, null), + new SignatureHelpResult(0, 6, 1, 0, 'value'), + new SignatureHelpResult(0, 7, 1, 0, 'value'), + new SignatureHelpResult(0, 8, 1, 1, '...'), + new SignatureHelpResult(0, 9, 1, 1, '...'), + new SignatureHelpResult(0, 10, 1, 1, '...'), + new SignatureHelpResult(0, 11, 1, 2, 'sep'), + new SignatureHelpResult(0, 12, 1, 2, 'sep') + ]; + + const document = await openDocument(path.join(autoCompPath, 'ellipsis.py')); + for (let i = 0; i < expected.length; i += 1) { + await checkSignature(expected[i], document!.uri, i); + } + }); + + test('For pow', async () => { + let expected: SignatureHelpResult; + if (isPython2) { + expected = new SignatureHelpResult(0, 4, 1, 0, 'x'); + } else { + expected = new SignatureHelpResult(0, 4, 1, 0, null); + } + + const document = await openDocument(path.join(autoCompPath, 'noSigPy3.py')); + await checkSignature(expected, document!.uri, 0); + }); +}); + +async function openDocument(documentPath: string): Promise { + const document = await vscode.workspace.openTextDocument(documentPath); + await vscode.window.showTextDocument(document!); + return document; +} + +async function checkSignature(expected: SignatureHelpResult, uri: vscode.Uri, caseIndex: number) { + const position = new vscode.Position(expected.line, expected.index); + const actual = await vscode.commands.executeCommand('vscode.executeSignatureHelpProvider', uri, position); + assert.equal(actual!.signatures.length, expected.signaturesCount, `Signature count does not match, case ${caseIndex}`); + if (expected.signaturesCount > 0) { + assert.equal(actual!.activeParameter, expected.activeParameter, `Parameter index does not match, case ${caseIndex}`); + if (expected.parameterName) { + const parameter = actual!.signatures[0].parameters[expected.activeParameter]; + assert.equal(parameter.label, expected.parameterName, `Parameter name is incorrect, case ${caseIndex}`); + } + } +} diff --git a/src/test/signature/signature.test.ts b/src/test/signature/signature.ls.test.ts similarity index 85% rename from src/test/signature/signature.test.ts rename to src/test/signature/signature.ls.test.ts index a4d002e4e750..49aa4fa9c83f 100644 --- a/src/test/signature/signature.test.ts +++ b/src/test/signature/signature.ls.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; import { rootWorkspaceUri } from '../common'; +import { IsLanguageServerTest } from '../constants'; import { closeActiveWindows, initialize, initializeTest } from '../initialize'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; @@ -20,10 +21,14 @@ class SignatureHelpResult { } // tslint:disable-next-line:max-func-body-length -suite('Signatures', () => { +suite('Signatures (Language Server)', () => { let isPython2: boolean; let ioc: UnitTestIocContainer; - suiteSetup(async () => { + suiteSetup(async function () { + if (!IsLanguageServerTest()) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } await initialize(); initializeDI(); isPython2 = await ioc.getPythonMajorVersion(rootWorkspaceUri) === 2; @@ -43,7 +48,7 @@ suite('Signatures', () => { test('For ctor', async () => { const expected = [ - new SignatureHelpResult(5, 11, 0, 0, null), + new SignatureHelpResult(5, 11, 1, -1, null), new SignatureHelpResult(5, 12, 1, 0, 'name'), new SignatureHelpResult(5, 13, 1, 0, 'name'), new SignatureHelpResult(5, 14, 1, 0, 'name'), @@ -52,7 +57,7 @@ suite('Signatures', () => { new SignatureHelpResult(5, 17, 1, 0, 'name'), new SignatureHelpResult(5, 18, 1, 1, 'age'), new SignatureHelpResult(5, 19, 1, 1, 'age'), - new SignatureHelpResult(5, 20, 0, 0, null) + new SignatureHelpResult(5, 20, 1, -1, null) ]; const document = await openDocument(path.join(autoCompPath, 'classCtor.py')); @@ -63,19 +68,18 @@ suite('Signatures', () => { test('For intrinsic', async () => { const expected = [ - new SignatureHelpResult(0, 0, 0, 0, null), - new SignatureHelpResult(0, 1, 0, 0, null), - new SignatureHelpResult(0, 2, 0, 0, null), - new SignatureHelpResult(0, 3, 0, 0, null), - new SignatureHelpResult(0, 4, 0, 0, null), - new SignatureHelpResult(0, 5, 0, 0, null), + new SignatureHelpResult(0, 0, 1, -1, null), + new SignatureHelpResult(0, 1, 1, -1, null), + new SignatureHelpResult(0, 2, 1, -1, null), + new SignatureHelpResult(0, 3, 1, -1, null), + new SignatureHelpResult(0, 4, 1, -1, null), + new SignatureHelpResult(0, 5, 1, -1, null), new SignatureHelpResult(0, 6, 1, 0, 'start'), new SignatureHelpResult(0, 7, 1, 0, 'start'), new SignatureHelpResult(0, 8, 1, 1, 'stop'), new SignatureHelpResult(0, 9, 1, 1, 'stop'), new SignatureHelpResult(0, 10, 1, 1, 'stop'), - new SignatureHelpResult(0, 11, 1, 2, 'step'), - new SignatureHelpResult(1, 0, 1, 2, 'step') + new SignatureHelpResult(0, 11, 1, 2, 'step') ]; const document = await openDocument(path.join(autoCompPath, 'basicSig.py')); @@ -91,7 +95,7 @@ suite('Signatures', () => { return; } const expected = [ - new SignatureHelpResult(0, 5, 0, 0, null), + new SignatureHelpResult(0, 5, 1, -1, null), new SignatureHelpResult(0, 6, 1, 0, 'value'), new SignatureHelpResult(0, 7, 1, 0, 'value'), new SignatureHelpResult(0, 8, 1, 1, '...'), diff --git a/src/test/standardTest.ts b/src/test/standardTest.ts index b64b3074f577..b147d0a156b8 100644 --- a/src/test/standardTest.ts +++ b/src/test/standardTest.ts @@ -2,7 +2,7 @@ import * as path from 'path'; -process.env.CODE_TESTS_WORKSPACE = path.join(__dirname, '..', '..', 'src', 'test'); +process.env.CODE_TESTS_WORKSPACE = process.env.CODE_TESTS_WORKSPACE ? process.env.CODE_TESTS_WORKSPACE : path.join(__dirname, '..', '..', 'src', 'test'); process.env.IS_CI_SERVER_TEST_DEBUGGER = ''; function start() { diff --git a/src/test/terminals/codeExecution/codeExecutionManager.test.ts b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts similarity index 92% rename from src/test/terminals/codeExecution/codeExecutionManager.test.ts rename to src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts index 8dec4d7f5025..05c395a1e571 100644 --- a/src/test/terminals/codeExecution/codeExecutionManager.test.ts +++ b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts @@ -69,7 +69,7 @@ suite('Terminal - Code Execution Manager', () => { serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); await commandHandler!(); - helper.verify(async h => await h.getFileToExecute(), TypeMoq.Times.once()); + helper.verify(async h => h.getFileToExecute(), TypeMoq.Times.once()); }); test('Ensure executeFileInterTerminal will use provided file', async () => { @@ -96,8 +96,8 @@ suite('Terminal - Code Execution Manager', () => { const fileToExecute = Uri.file('x'); await commandHandler!(fileToExecute); - helper.verify(async h => await h.getFileToExecute(), TypeMoq.Times.never()); - executionService.verify(async e => await e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + helper.verify(async h => h.getFileToExecute(), TypeMoq.Times.never()); + executionService.verify(async e => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); }); test('Ensure executeFileInterTerminal will use active file', async () => { @@ -119,12 +119,12 @@ suite('Terminal - Code Execution Manager', () => { const fileToExecute = Uri.file('x'); const helper = TypeMoq.Mock.ofType(); serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); - helper.setup(async h => await h.getFileToExecute()).returns(() => Promise.resolve(fileToExecute)); + helper.setup(async h => h.getFileToExecute()).returns(() => Promise.resolve(fileToExecute)); const executionService = TypeMoq.Mock.ofType(); serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('standard'))).returns(() => executionService.object); await commandHandler!(fileToExecute); - executionService.verify(async e => await e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + executionService.verify(async e => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); }); async function testExecutionOfSelectionWithoutAnyActiveDocument(commandId: string, executionSericeId: string) { @@ -150,7 +150,7 @@ suite('Terminal - Code Execution Manager', () => { documentManager.setup(d => d.activeTextEditor).returns(() => undefined); await commandHandler!(); - executionService.verify(async e => await e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never()); + executionService.verify(async e => e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never()); } test('Ensure executeSelectionInTerminal will do nothing if theres no active document', async () => { @@ -186,7 +186,7 @@ suite('Terminal - Code Execution Manager', () => { documentManager.setup(d => d.activeTextEditor).returns(() => { return {} as any; }); await commandHandler!(); - executionService.verify(async e => await e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never()); + executionService.verify(async e => e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never()); } test('Ensure executeSelectionInTerminal will do nothing if no text is selected', async () => { @@ -218,7 +218,7 @@ suite('Terminal - Code Execution Manager', () => { const helper = TypeMoq.Mock.ofType(); serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); helper.setup(h => h.getSelectedTextToExecute).returns(() => () => Promise.resolve(textSelected)); - helper.setup(h => h.normalizeLines).returns(() => () => textSelected); + helper.setup(h => h.normalizeLines).returns(() => () => Promise.resolve(textSelected)).verifiable(TypeMoq.Times.once()); const executionService = TypeMoq.Mock.ofType(); serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionServiceId))).returns(() => executionService.object); const document = TypeMoq.Mock.ofType(); @@ -228,7 +228,8 @@ suite('Terminal - Code Execution Manager', () => { documentManager.setup(d => d.activeTextEditor).returns(() => activeEditor.object); await commandHandler!(); - executionService.verify(async e => await e.execute(TypeMoq.It.isValue(textSelected), TypeMoq.It.isValue(activeDocumentUri)), TypeMoq.Times.once()); + executionService.verify(async e => e.execute(TypeMoq.It.isValue(textSelected), TypeMoq.It.isValue(activeDocumentUri)), TypeMoq.Times.once()); + helper.verifyAll(); } test('Ensure executeSelectionInTerminal will normalize selected text and send it to the terminal', async () => { await testExecutionOfSelectionIsSentToTerminal(Commands.Exec_Selection_In_Terminal, 'standard'); diff --git a/src/test/terminals/codeExecution/djangoShellCodeExect.test.ts b/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts similarity index 98% rename from src/test/terminals/codeExecution/djangoShellCodeExect.test.ts rename to src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts index b6814214abeb..e85da9496e88 100644 --- a/src/test/terminals/codeExecution/djangoShellCodeExect.test.ts +++ b/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts @@ -13,6 +13,7 @@ import { ITerminalService, ITerminalServiceFactory } from '../../../client/commo import { IConfigurationService, IPythonSettings, ITerminalSettings } from '../../../client/common/types'; import { DjangoShellCodeExecutionProvider } from '../../../client/terminals/codeExecution/djangoShellCodeExecution'; import { ICodeExecutionService } from '../../../client/terminals/types'; +import { PYTHON_PATH } from '../../common'; // tslint:disable-next-line:max-func-body-length suite('Terminal - Django Shell Code Execution', () => { @@ -86,7 +87,7 @@ suite('Terminal - Django Shell Code Execution', () => { }); test('Ensure python path is returned as is, when building repl args on Windows', async () => { - const pythonPath = 'python'; + const pythonPath = PYTHON_PATH; const terminalArgs = ['-a', 'b', 'c']; const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); @@ -102,7 +103,7 @@ suite('Terminal - Django Shell Code Execution', () => { }); test('Ensure python path is returned as is, on non Windows', async () => { - const pythonPath = 'python'; + const pythonPath = PYTHON_PATH; const terminalArgs = ['-a', 'b', 'c']; const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); diff --git a/src/test/terminals/codeExecution/helper.test.ts b/src/test/terminals/codeExecution/helper.test.ts deleted file mode 100644 index 064ca0d55e72..000000000000 --- a/src/test/terminals/codeExecution/helper.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// tslint:disable:no-multiline-string no-trailing-whitespace - -import { expect } from 'chai'; -import { EOL } from 'os'; -import * as TypeMoq from 'typemoq'; -import { Range, Selection, TextDocument, TextEditor, TextLine, Uri } from 'vscode'; -import { IApplicationShell, IDocumentManager } from '../../../client/common/application/types'; -import { PythonLanguage } from '../../../client/common/constants'; -import { CodeExecutionHelper } from '../../../client/terminals/codeExecution/helper'; -import { ICodeExecutionHelper } from '../../../client/terminals/types'; - -// tslint:disable-next-line:max-func-body-length -suite('Terminal - Code Execution Helper', () => { - let documentManager: TypeMoq.IMock; - let applicationShell: TypeMoq.IMock; - let helper: ICodeExecutionHelper; - let document: TypeMoq.IMock; - let editor: TypeMoq.IMock; - setup(() => { - documentManager = TypeMoq.Mock.ofType(); - applicationShell = TypeMoq.Mock.ofType(); - helper = new CodeExecutionHelper(documentManager.object, applicationShell.object); - - document = TypeMoq.Mock.ofType(); - editor = TypeMoq.Mock.ofType(); - editor.setup(e => e.document).returns(() => document.object); - }); - - test('Ensure blank lines are removed', async () => { - const code = ['import sys', '', '', '', 'print(sys.executable)', '', 'print("1234")', '', '', 'print(1)', 'print(2)']; - const expectedCode = code.filter(line => line.trim().length > 0).join(EOL); - const normalizedZCode = helper.normalizeLines(code.join(EOL)); - expect(normalizedZCode).to.be.equal(expectedCode); - }); - test('Display message if there\s no active file', async () => { - documentManager.setup(doc => doc.activeTextEditor).returns(() => undefined); - - const uri = await helper.getFileToExecute(); - expect(uri).to.be.an('undefined'); - applicationShell.verify(a => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); - }); - - test('Display message if active file is unsaved', async () => { - documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); - document.setup(doc => doc.isUntitled).returns(() => true); - - const uri = await helper.getFileToExecute(); - expect(uri).to.be.an('undefined'); - applicationShell.verify(a => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); - }); - - test('Display message if active file is non-python', async () => { - document.setup(doc => doc.isUntitled).returns(() => false); - document.setup(doc => doc.languageId).returns(() => 'html'); - documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); - - const uri = await helper.getFileToExecute(); - expect(uri).to.be.an('undefined'); - applicationShell.verify(a => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); - }); - - test('Returns file uri', async () => { - document.setup(doc => doc.isUntitled).returns(() => false); - document.setup(doc => doc.languageId).returns(() => PythonLanguage.language); - const expectedUri = Uri.file('one.py'); - document.setup(doc => doc.uri).returns(() => expectedUri); - documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); - - const uri = await helper.getFileToExecute(); - expect(uri).to.be.deep.equal(expectedUri); - }); - - test('Returns current line if nothing is selected', async () => { - const lineContents = 'Line Contents'; - editor.setup(e => e.selection).returns(() => new Selection(3, 0, 3, 0)); - const textLine = TypeMoq.Mock.ofType(); - textLine.setup(t => t.text).returns(() => lineContents); - document.setup(d => d.lineAt(TypeMoq.It.isAny())).returns(() => textLine.object); - - const content = await helper.getSelectedTextToExecute(editor.object); - expect(content).to.be.equal(lineContents); - }); - - test('Returns selected text', async () => { - const lineContents = 'Line Contents'; - editor.setup(e => e.selection).returns(() => new Selection(3, 0, 10, 5)); - const textLine = TypeMoq.Mock.ofType(); - textLine.setup(t => t.text).returns(() => lineContents); - document.setup(d => d.getText(TypeMoq.It.isAny())).returns((r: Range) => `${r.start.line}.${r.start.character}.${r.end.line}.${r.end.character}`); - - const content = await helper.getSelectedTextToExecute(editor.object); - expect(content).to.be.equal('3.0.10.5'); - }); -}); diff --git a/src/test/terminals/codeExecution/helper.unit.test.ts b/src/test/terminals/codeExecution/helper.unit.test.ts new file mode 100644 index 000000000000..62c00bbd0115 --- /dev/null +++ b/src/test/terminals/codeExecution/helper.unit.test.ts @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-multiline-string no-trailing-whitespace max-func-body-length no-any + +import { expect } from 'chai'; +import * as fs from 'fs-extra'; +import { EOL } from 'os'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Range, Selection, TextDocument, TextEditor, TextLine, Uri } from 'vscode'; +import { IApplicationShell, IDocumentManager } from '../../../client/common/application/types'; +import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../../client/common/constants'; +import '../../../client/common/extensions'; +import { BufferDecoder } from '../../../client/common/process/decoder'; +import { ProcessService } from '../../../client/common/process/proc'; +import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; +import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { CodeExecutionHelper } from '../../../client/terminals/codeExecution/helper'; +import { ICodeExecutionHelper } from '../../../client/terminals/types'; +import { PYTHON_PATH } from '../../common'; + +const TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'terminalExec'); + +suite('Terminal - Code Execution Helper', () => { + let documentManager: TypeMoq.IMock; + let applicationShell: TypeMoq.IMock; + let helper: ICodeExecutionHelper; + let document: TypeMoq.IMock; + let editor: TypeMoq.IMock; + let processService: TypeMoq.IMock; + let configService: TypeMoq.IMock; + setup(() => { + const serviceContainer = TypeMoq.Mock.ofType(); + documentManager = TypeMoq.Mock.ofType(); + applicationShell = TypeMoq.Mock.ofType(); + const envVariablesProvider = TypeMoq.Mock.ofType(); + processService = TypeMoq.Mock.ofType(); + configService = TypeMoq.Mock.ofType(); + const pythonSettings = TypeMoq.Mock.ofType(); + pythonSettings.setup(p => p.pythonPath).returns(() => PYTHON_PATH); + processService.setup((x: any) => x.then).returns(() => undefined); + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + envVariablesProvider.setup(e => e.getEnvironmentVariables(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); + const processServiceFactory = TypeMoq.Mock.ofType(); + processServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())).returns(() => processServiceFactory.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDocumentManager), TypeMoq.It.isAny())).returns(() => documentManager.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())).returns(() => applicationShell.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider), TypeMoq.It.isAny())).returns(() => envVariablesProvider.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())).returns(() => configService.object); + helper = new CodeExecutionHelper(serviceContainer.object); + + document = TypeMoq.Mock.ofType(); + editor = TypeMoq.Mock.ofType(); + editor.setup(e => e.document).returns(() => document.object); + }); + + async function ensureBlankLinesAreRemoved(source: string, expectedSource: string) { + const actualProcessService = new ProcessService(new BufferDecoder()); + processService.setup(p => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => { + return actualProcessService.exec.apply(actualProcessService, [file, args, options]); + }); + const normalizedZCode = await helper.normalizeLines(source); + // In case file has been saved with different line endings. + expectedSource = expectedSource.splitLines({ removeEmptyEntries: false, trim: false }).join(EOL); + expect(normalizedZCode).to.be.equal(expectedSource); + } + test('Ensure blank lines are NOT removed when code is not indented (simple)', async () => { + const code = ['import sys', '', '', '', 'print(sys.executable)', '', 'print("1234")', '', '', 'print(1)', 'print(2)']; + const expectedCode = code.filter(line => line.trim().length > 0).join(EOL); + await ensureBlankLinesAreRemoved(code.join(EOL), expectedCode); + }); + ['', '1', '2', '3', '4', '5', '6', '7'].forEach(fileNameSuffix => { + test(`Ensure blank lines are removed (Sample${fileNameSuffix})`, async () => { + const code = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_raw.py`), 'utf8'); + const expectedCode = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_normalized.py`), 'utf8'); + await ensureBlankLinesAreRemoved(code, expectedCode); + }); + // test(`Ensure blank lines are removed, including leading empty lines (${fileName})`, async () => { + // const code = await fs.readFile(path.join(TEST_FILES_PATH, `${fileName}_raw.py`), 'utf8'); + // const expectedCode = await fs.readFile(path.join(TEST_FILES_PATH, `${fileName}_normalized.py`), 'utf8'); + // await ensureBlankLinesAreRemoved(['', '', ''].join(EOL) + EOL + code, expectedCode); + // }); + }); + test('Display message if there\s no active file', async () => { + documentManager.setup(doc => doc.activeTextEditor).returns(() => undefined); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.an('undefined'); + applicationShell.verify(a => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + }); + + test('Display message if active file is unsaved', async () => { + documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); + document.setup(doc => doc.isUntitled).returns(() => true); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.an('undefined'); + applicationShell.verify(a => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + }); + + test('Display message if active file is non-python', async () => { + document.setup(doc => doc.isUntitled).returns(() => false); + document.setup(doc => doc.languageId).returns(() => 'html'); + documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.an('undefined'); + applicationShell.verify(a => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + }); + + test('Returns file uri', async () => { + document.setup(doc => doc.isUntitled).returns(() => false); + document.setup(doc => doc.languageId).returns(() => PYTHON_LANGUAGE); + const expectedUri = Uri.file('one.py'); + document.setup(doc => doc.uri).returns(() => expectedUri); + documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.deep.equal(expectedUri); + }); + + test('Returns file uri even if saving fails', async () => { + document.setup(doc => doc.isUntitled).returns(() => false); + document.setup(doc => doc.isDirty).returns(() => true); + document.setup(doc => doc.languageId).returns(() => PYTHON_LANGUAGE); + document.setup(doc => doc.save()).returns(() => Promise.resolve(false)); + const expectedUri = Uri.file('one.py'); + document.setup(doc => doc.uri).returns(() => expectedUri); + documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.deep.equal(expectedUri); + }); + + test('Dirty files are saved', async () => { + document.setup(doc => doc.isUntitled).returns(() => false); + document.setup(doc => doc.isDirty).returns(() => true); + document.setup(doc => doc.languageId).returns(() => PYTHON_LANGUAGE); + const expectedUri = Uri.file('one.py'); + document.setup(doc => doc.uri).returns(() => expectedUri); + documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.deep.equal(expectedUri); + document.verify(doc => doc.save(), TypeMoq.Times.once()); + }); + + test('Non-Dirty files are not-saved', async () => { + document.setup(doc => doc.isUntitled).returns(() => false); + document.setup(doc => doc.isDirty).returns(() => false); + document.setup(doc => doc.languageId).returns(() => PYTHON_LANGUAGE); + const expectedUri = Uri.file('one.py'); + document.setup(doc => doc.uri).returns(() => expectedUri); + documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.deep.equal(expectedUri); + document.verify(doc => doc.save(), TypeMoq.Times.never()); + }); + + test('Returns current line if nothing is selected', async () => { + const lineContents = 'Line Contents'; + editor.setup(e => e.selection).returns(() => new Selection(3, 0, 3, 0)); + const textLine = TypeMoq.Mock.ofType(); + textLine.setup(t => t.text).returns(() => lineContents); + document.setup(d => d.lineAt(TypeMoq.It.isAny())).returns(() => textLine.object); + + const content = await helper.getSelectedTextToExecute(editor.object); + expect(content).to.be.equal(lineContents); + }); + + test('Returns selected text', async () => { + const lineContents = 'Line Contents'; + editor.setup(e => e.selection).returns(() => new Selection(3, 0, 10, 5)); + const textLine = TypeMoq.Mock.ofType(); + textLine.setup(t => t.text).returns(() => lineContents); + document.setup(d => d.getText(TypeMoq.It.isAny())).returns((r: Range) => `${r.start.line}.${r.start.character}.${r.end.line}.${r.end.character}`); + + const content = await helper.getSelectedTextToExecute(editor.object); + expect(content).to.be.equal('3.0.10.5'); + }); + + test('saveFileIfDirty will not fail if file is not opened', async () => { + documentManager.setup(d => d.textDocuments).returns(() => []).verifiable(TypeMoq.Times.once()); + + await helper.saveFileIfDirty(Uri.file(`${__filename}.py`)); + documentManager.verifyAll(); + }); + + test('File will be saved if file is dirty', async () => { + documentManager.setup(d => d.textDocuments).returns(() => [document.object]).verifiable(TypeMoq.Times.once()); + document.setup(doc => doc.isUntitled).returns(() => false); + document.setup(doc => doc.isDirty).returns(() => true); + document.setup(doc => doc.languageId).returns(() => PYTHON_LANGUAGE); + const expectedUri = Uri.file('one.py'); + document.setup(doc => doc.uri).returns(() => expectedUri); + + await helper.saveFileIfDirty(expectedUri); + documentManager.verifyAll(); + document.verify(doc => doc.save(), TypeMoq.Times.once()); + }); + + test('File will be not saved if file is not dirty', async () => { + documentManager.setup(d => d.textDocuments).returns(() => [document.object]).verifiable(TypeMoq.Times.once()); + document.setup(doc => doc.isUntitled).returns(() => false); + document.setup(doc => doc.isDirty).returns(() => false); + document.setup(doc => doc.languageId).returns(() => PYTHON_LANGUAGE); + const expectedUri = Uri.file('one.py'); + document.setup(doc => doc.uri).returns(() => expectedUri); + + await helper.saveFileIfDirty(expectedUri); + documentManager.verifyAll(); + document.verify(doc => doc.save(), TypeMoq.Times.never()); + }); +}); diff --git a/src/test/terminals/codeExecution/terminalCodeExec.test.ts b/src/test/terminals/codeExecution/terminalCodeExec.test.ts index 4466de64354f..8fec1ace6b8f 100644 --- a/src/test/terminals/codeExecution/terminalCodeExec.test.ts +++ b/src/test/terminals/codeExecution/terminalCodeExec.test.ts @@ -15,9 +15,10 @@ import { DjangoShellCodeExecutionProvider } from '../../../client/terminals/code import { ReplProvider } from '../../../client/terminals/codeExecution/repl'; import { TerminalCodeExecutionProvider } from '../../../client/terminals/codeExecution/terminalCodeExecution'; import { ICodeExecutionService } from '../../../client/terminals/types'; +import { PYTHON_PATH } from '../../common'; // tslint:disable-next-line:max-func-body-length -suite('Terminal Code Execution', () => { +suite('Terminal - Code Execution', () => { // tslint:disable-next-line:max-func-body-length ['Terminal Execution', 'Repl Execution', 'Django Execution'].forEach(testSuiteName => { let terminalSettings: TypeMoq.IMock; @@ -99,7 +100,7 @@ suite('Terminal Code Execution', () => { platform.setup(p => p.isWindows).returns(() => isWindows); platform.setup(p => p.isMac).returns(() => isOsx); platform.setup(p => p.isLinux).returns(() => isLinux); - settings.setup(s => s.pythonPath).returns(() => 'python'); + settings.setup(s => s.pythonPath).returns(() => PYTHON_PATH); terminalSettings.setup(t => t.launchArgs).returns(() => []); await executor.initializeRepl(); @@ -130,12 +131,12 @@ suite('Terminal Code Execution', () => { workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); workspaceFolder.setup(w => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to'))); platform.setup(p => p.isWindows).returns(() => false); - settings.setup(s => s.pythonPath).returns(() => 'python'); + settings.setup(s => s.pythonPath).returns(() => PYTHON_PATH); terminalSettings.setup(t => t.launchArgs).returns(() => []); await executor.executeFile(file); - terminalService.verify(async t => await t.sendText(TypeMoq.It.isValue(`cd ${path.dirname(file.fsPath).fileToCommandArgument()}`)), TypeMoq.Times.once()); + terminalService.verify(async t => t.sendText(TypeMoq.It.isValue(`cd ${path.dirname(file.fsPath).fileToCommandArgument()}`)), TypeMoq.Times.once()); } test('Ensure we set current directory before executing file (non windows)', async () => { await ensureWeSetCurrentDirectoryBeforeExecutingAFile(false); @@ -150,12 +151,12 @@ suite('Terminal Code Execution', () => { workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); workspaceFolder.setup(w => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to'))); platform.setup(p => p.isWindows).returns(() => isWindows); - settings.setup(s => s.pythonPath).returns(() => 'python'); + settings.setup(s => s.pythonPath).returns(() => PYTHON_PATH); terminalSettings.setup(t => t.launchArgs).returns(() => []); await executor.executeFile(file); const dir = path.dirname(file.fsPath).fileToCommandArgument(); - terminalService.verify(async t => await t.sendText(TypeMoq.It.isValue(`cd ${dir}`)), TypeMoq.Times.once()); + terminalService.verify(async t => t.sendText(TypeMoq.It.isValue(`cd ${dir}`)), TypeMoq.Times.once()); } test('Ensure we set current directory (and quote it when containing spaces) before executing file (non windows)', async () => { @@ -172,12 +173,12 @@ suite('Terminal Code Execution', () => { workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); workspaceFolder.setup(w => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to', 'file with spaces in path'))); platform.setup(p => p.isWindows).returns(() => isWindows); - settings.setup(s => s.pythonPath).returns(() => 'python'); + settings.setup(s => s.pythonPath).returns(() => PYTHON_PATH); terminalSettings.setup(t => t.launchArgs).returns(() => []); await executor.executeFile(file); - terminalService.verify(async t => await t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); + terminalService.verify(async t => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); } test('Ensure we do not set current directory before executing file if in the same directory (non windows)', async () => { await ensureWeDoNotSetCurrentDirectoryBeforeExecutingFileInSameDirectory(false); @@ -191,12 +192,12 @@ suite('Terminal Code Execution', () => { terminalSettings.setup(t => t.executeInFileDir).returns(() => true); workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); platform.setup(p => p.isWindows).returns(() => isWindows); - settings.setup(s => s.pythonPath).returns(() => 'python'); + settings.setup(s => s.pythonPath).returns(() => PYTHON_PATH); terminalSettings.setup(t => t.launchArgs).returns(() => []); await executor.executeFile(file); - terminalService.verify(async t => await t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); + terminalService.verify(async t => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); } test('Ensure we do not set current directory before executing file if file is not in a workspace (non windows)', async () => { await ensureWeDoNotSetCurrentDirectoryBeforeExecutingFileNotInSameDirectory(false); @@ -215,12 +216,12 @@ suite('Terminal Code Execution', () => { await executor.executeFile(file); const expectedPythonPath = isWindows ? pythonPath.replace(/\\/g, '/') : pythonPath; const expectedArgs = terminalArgs.concat(file.fsPath.fileToCommandArgument()); - terminalService.verify(async t => await t.sendCommand(TypeMoq.It.isValue(expectedPythonPath), TypeMoq.It.isValue(expectedArgs)), TypeMoq.Times.once()); + terminalService.verify(async t => t.sendCommand(TypeMoq.It.isValue(expectedPythonPath), TypeMoq.It.isValue(expectedArgs)), TypeMoq.Times.once()); } test('Ensure python file execution script is sent to terminal on windows', async () => { const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); - await testFileExecution(true, 'python', [], file); + await testFileExecution(true, PYTHON_PATH, [], file); }); test('Ensure python file execution script is sent to terminal on windows with fully qualified python path', async () => { @@ -230,12 +231,12 @@ suite('Terminal Code Execution', () => { test('Ensure python file execution script is not quoted when no spaces in file path', async () => { const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); - await testFileExecution(true, 'python', [], file); + await testFileExecution(true, PYTHON_PATH, [], file); }); test('Ensure python file execution script supports custom python arguments', async () => { const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); - await testFileExecution(false, 'python', ['-a', '-b', '-c'], file); + await testFileExecution(false, PYTHON_PATH, ['-a', '-b', '-c'], file); }); function testReplCommandArguments(isWindows: boolean, pythonPath: string, expectedPythonPath: string, terminalArgs: string[]) { @@ -265,7 +266,7 @@ suite('Terminal Code Execution', () => { }); test('Ensure python path is returned as is, when building repl args on Windows', () => { - const pythonPath = 'python'; + const pythonPath = PYTHON_PATH; const terminalArgs = ['-a', 'b', 'c']; testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs); @@ -279,7 +280,7 @@ suite('Terminal Code Execution', () => { }); test('Ensure python path is returned as is, on non Windows', () => { - const pythonPath = 'python'; + const pythonPath = PYTHON_PATH; const terminalArgs = ['-a', 'b', 'c']; testReplCommandArguments(false, pythonPath, pythonPath, terminalArgs); @@ -291,8 +292,8 @@ suite('Terminal Code Execution', () => { // tslint:disable-next-line:no-any await executor.execute(undefined as any as string); - terminalService.verify(async t => await t.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); - terminalService.verify(async t => await t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); + terminalService.verify(async t => t.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); + terminalService.verify(async t => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); }); test('Ensure repl is initialized once before sending text to the repl', async () => { @@ -308,7 +309,7 @@ suite('Terminal Code Execution', () => { await executor.execute('cmd3'); const expectedTerminalArgs = isDjangoRepl ? terminalArgs.concat(['manage.py', 'shell']) : terminalArgs; - terminalService.verify(async t => await t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), TypeMoq.Times.once()); + terminalService.verify(async t => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), TypeMoq.Times.once()); }); test('Ensure repl is re-initialized when temrinal is closed', async () => { @@ -334,15 +335,15 @@ suite('Terminal Code Execution', () => { const expectedTerminalArgs = isDjangoRepl ? terminalArgs.concat(['manage.py', 'shell']) : terminalArgs; expect(closeTerminalCallback).not.to.be.an('undefined', 'Callback not initialized'); - terminalService.verify(async t => await t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), TypeMoq.Times.once()); + terminalService.verify(async t => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), TypeMoq.Times.once()); closeTerminalCallback!.call(terminalService.object); await executor.execute('cmd4'); - terminalService.verify(async t => await t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), TypeMoq.Times.exactly(2)); + terminalService.verify(async t => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), TypeMoq.Times.exactly(2)); closeTerminalCallback!.call(terminalService.object); await executor.execute('cmd5'); - terminalService.verify(async t => await t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), TypeMoq.Times.exactly(3)); + terminalService.verify(async t => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), TypeMoq.Times.exactly(3)); }); test('Ensure code is sent to terminal', async () => { @@ -353,10 +354,10 @@ suite('Terminal Code Execution', () => { terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); await executor.execute('cmd1'); - terminalService.verify(async t => await t.sendText('cmd1'), TypeMoq.Times.once()); + terminalService.verify(async t => t.sendText('cmd1'), TypeMoq.Times.once()); await executor.execute('cmd2'); - terminalService.verify(async t => await t.sendText('cmd2'), TypeMoq.Times.once()); + terminalService.verify(async t => t.sendText('cmd2'), TypeMoq.Times.once()); }); }); }); diff --git a/src/test/testRunner.ts b/src/test/testRunner.ts index 30f64f619d8e..a30c80b11bbc 100644 --- a/src/test/testRunner.ts +++ b/src/test/testRunner.ts @@ -25,20 +25,20 @@ interface ITestRunnerOptions { // http://gotwarlost.github.io/istanbul/public/apidocs/files/lib_instrumenter.js.html#l478. type CoverState = { - path: string, - s: {}, - b: {}, - f: {}, - fnMap: {}, - statementMap: {}, - branchMap: {} + path: string; + s: {}; + b: {}; + f: {}; + fnMap: {}; + statementMap: {}; + branchMap: {}; }; type Instrumenter = istanbul.Instrumenter & { coverState: CoverState }; type TestCallback = (error?: Error, failures?: number) => void; // Linux: prevent a weird NPE when mocha on Linux requires the window size from the TTY. -// Since we are not running in a tty environment, we just implementt he method statically. +// Since we are not running in a tty environment, we just implement the method statically. const tty = require('tty'); if (!tty.getWindowSize) { tty.getWindowSize = function (): number[] { return [80, 75]; }; @@ -49,10 +49,23 @@ let mocha = new Mocha({ useColors: true }); +export type SetupOptions = MochaSetupOptions & { + testFilesSuffix?: string; + reporter?: string; + reporterOptions?: { + mochaFile?: string; + properties?: string; + }; +}; + +let testFilesGlob = 'test'; let coverageOptions: { coverageConfig: string } | undefined; -export function configure(mochaOpts: MochaSetupOptions, coverageOpts?: { coverageConfig: string }): void { - mocha = new Mocha(mochaOpts); +export function configure(setupOptions: SetupOptions, coverageOpts?: { coverageConfig: string }): void { + if (setupOptions.testFilesSuffix) { + testFilesGlob = setupOptions.testFilesSuffix; + } + mocha = new Mocha(setupOptions); coverageOptions = coverageOpts; } @@ -70,7 +83,7 @@ export function run(testsRoot: string, callback: TestCallback): void { } // Run the tests. - glob('**/**.test.js', { cwd: testsRoot }, (error, files) => { + glob(`**/**.${testFilesGlob}.js`, { cwd: testsRoot }, (error, files) => { if (error) { return callback(error); } @@ -94,7 +107,7 @@ function getCoverageOptions(testsRoot: string): ITestRunnerOptions | undefined { class CoverageRunner { private coverageVar: string = `$$cov_${new Date().getTime()}$$`; private sourceFiles: string[] = []; - private instrumenter: Instrumenter; + private instrumenter!: Instrumenter; private get coverage(): { [key: string]: CoverState } { if (global[this.coverageVar] === undefined || Object.keys(global[this.coverageVar]).length === 0) { diff --git a/src/test/unittests.ts b/src/test/unittests.ts new file mode 100644 index 000000000000..f2c965417650 --- /dev/null +++ b/src/test/unittests.ts @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-require-imports no-var-requires + +if ((Reflect as any).metadata === undefined) { + require('reflect-metadata'); +} +import * as glob from 'glob'; +import * as Mocha from 'mocha'; +import * as path from 'path'; +import { MochaSetupOptions } from 'vscode/lib/testrunner'; +import { MOCHA_CI_REPORTER_ID, MOCHA_CI_REPORTFILE, + MOCHA_REPORTER_JUNIT } from './ciConstants'; +import * as vscodeMoscks from './vscode-mock'; + +export function runTests(testOptions?: { grep?: string; timeout?: number }) { + vscodeMoscks.initialize(); + + const grep: string | undefined = testOptions ? testOptions.grep : undefined; + const timeout: number | undefined = testOptions ? testOptions.timeout : undefined; + + const options: MochaSetupOptions = { + ui: 'tdd', + useColors: true, + timeout, + grep + }; + + let temp_mocha: Mocha | undefined; + + if (MOCHA_REPORTER_JUNIT === true) { + temp_mocha = new Mocha({ + grep: undefined, + ui: 'tdd', + timeout, + reporter: MOCHA_CI_REPORTER_ID, + reporterOptions: { + useColors: false, + mochaFile: MOCHA_CI_REPORTFILE, + bail: false + }, + slow: undefined + }); + } else { + // we are running on the command line or debugger... + temp_mocha = new Mocha(options); + } + + const mocha: Mocha = temp_mocha; + + require('source-map-support').install(); + const testsRoot = __dirname; + glob('**/**.unit.test.js', { cwd: testsRoot }, (error, files) => { + if (error) { + return reportErrors(error); + } + try { + files.forEach(file => mocha.addFile(path.join(testsRoot, file))); + mocha.run(failures => { + if (failures === 0) { + return; + } + reportErrors(undefined, failures); + }); + } catch (error) { + reportErrors(error); + } + }); +} +function reportErrors(error?: Error, failures?: number) { + let failed = false; + if (error) { + console.error(error); + failed = true; + } + if (failures && failures >= 0) { + console.error(`${failures} failed tests 👎.`); + failed = true; + } + if (failed) { + process.exit(1); + } +} +// this allows us to run hygiene as a git pre-commit hook or via debugger. +if (require.main === module) { + // When running from debugger, allow custom args. + const args = process.argv0.length > 2 ? process.argv.slice(2) : []; + const timeoutArgIndex = args.findIndex(arg => arg.startsWith('timeout=')); + const grepArgIndex = args.findIndex(arg => arg.startsWith('grep=')); + const timeout: number | undefined = timeoutArgIndex >= 0 ? parseInt(args[timeoutArgIndex].split('=')[1].trim(), 10) : undefined; + let grep: string | undefined = grepArgIndex >= 0 ? args[grepArgIndex].split('=')[1].trim() : undefined; + grep = grep && grep.length > 0 ? grep : undefined; + + runTests({ grep, timeout }); +} diff --git a/src/test/unittests/argsService.unit.test.ts b/src/test/unittests/argsService.unit.test.ts new file mode 100644 index 000000000000..e0ca1043c84e --- /dev/null +++ b/src/test/unittests/argsService.unit.test.ts @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any no-conditional-assignment no-increment-decrement no-invalid-this insecure-random +import { fail } from 'assert'; +import { expect } from 'chai'; +import { spawnSync } from 'child_process'; +import * as path from 'path'; +import * as typeMoq from 'typemoq'; +import { EnumEx } from '../../client/common/enumUtils'; +import { ILogger, Product } from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { ArgumentsHelper } from '../../client/unittests/common/argumentsHelper'; +import { ArgumentsService as NoseTestArgumentsService } from '../../client/unittests/nosetest/services/argsService'; +import { ArgumentsService as PyTestArgumentsService } from '../../client/unittests/pytest/services/argsService'; +import { IArgumentsHelper, IArgumentsService } from '../../client/unittests/types'; +import { ArgumentsService as UnitTestArgumentsService } from '../../client/unittests/unittest/services/argsService'; +import { PYTHON_PATH } from '../common'; + +suite('Unit Tests - argsService', () => { + [Product.unittest, Product.nosetest, Product.pytest] + .forEach(product => { + const productNames = EnumEx.getNamesAndValues(Product); + const productName = productNames.find(item => item.value === product)!.name; + suite(productName, () => { + let argumentsService: IArgumentsService; + let moduleName = ''; + let expectedWithArgs: string[] = []; + let expectedWithoutArgs: string[] = []; + + suiteSetup(() => { + const serviceContainer = typeMoq.Mock.ofType(); + const logger = typeMoq.Mock.ofType(); + + serviceContainer + .setup(s => s.get(typeMoq.It.isValue(ILogger), typeMoq.It.isAny())) + .returns(() => logger.object); + + const argsHelper = new ArgumentsHelper(serviceContainer.object); + + serviceContainer + .setup(s => s.get(typeMoq.It.isValue(IArgumentsHelper), typeMoq.It.isAny())) + .returns(() => argsHelper); + + switch (product) { + case Product.unittest: { + argumentsService = new UnitTestArgumentsService(serviceContainer.object); + moduleName = 'unittest'; + break; + } + case Product.nosetest: { + argumentsService = new NoseTestArgumentsService(serviceContainer.object); + moduleName = 'nose'; + break; + } + case Product.pytest: { + moduleName = 'pytest'; + argumentsService = new PyTestArgumentsService(serviceContainer.object); + break; + } + default: { + throw new Error('Unrecognized Test Framework'); + } + } + + expectedWithArgs = getOptions(product, moduleName, true); + expectedWithoutArgs = getOptions(product, moduleName, false); + }); + + test('Check for new/unrecognized options with values', () => { + const options = argumentsService.getKnownOptions(); + const optionsNotFound = expectedWithArgs.filter(item => options.withArgs.indexOf(item) === -1); + + if (optionsNotFound.length > 0) { + fail('', optionsNotFound.join(', '), 'Options not found'); + } + }); + test('Check for new/unrecognized options without values', () => { + const options = argumentsService.getKnownOptions(); + const optionsNotFound = expectedWithoutArgs.filter(item => options.withoutArgs.indexOf(item) === -1); + + if (optionsNotFound.length > 0) { + fail('', optionsNotFound.join(', '), 'Options not found'); + } + }); + test('Test getting value for an option with a single value', () => { + for (const option of expectedWithArgs) { + const args = ['--some-option-with-a-value', '1234', '--another-value-with-inline=1234', option, 'abcd']; + const value = argumentsService.getOptionValue(args, option); + expect(value).to.equal('abcd'); + } + }); + test('Test getting value for an option with a multiple value', () => { + for (const option of expectedWithArgs) { + const args = ['--some-option-with-a-value', '1234', '--another-value-with-inline=1234', option, 'abcd', option, 'xyz']; + const value = argumentsService.getOptionValue(args, option); + expect(value).to.deep.equal(['abcd', 'xyz']); + } + }); + test('Test getting the test folder in unittest with -s', function () { + if (product !== Product.unittest) { + return this.skip(); + } + const dir = path.join('a', 'b', 'c'); + const args = ['anzy', '--one', '--three', '-s', dir]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(1); + expect(testDirs[0]).to.equal(dir); + }); + test('Test getting the test folder in unittest with -s in the middle', function () { + if (product !== Product.unittest) { + return this.skip(); + } + const dir = path.join('a', 'b', 'c'); + const args = ['anzy', '--one', '--three', '-s', dir, 'some other', '--value', '1234']; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(1); + expect(testDirs[0]).to.equal(dir); + }); + test('Test getting the test folder in unittest with --start-directory', function () { + if (product !== Product.unittest) { + return this.skip(); + } + const dir = path.join('a', 'b', 'c'); + const args = ['anzy', '--one', '--three', '--start-directory', dir]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(1); + expect(testDirs[0]).to.equal(dir); + }); + test('Test getting the test folder in unittest with --start-directory in the middle', function () { + if (product !== Product.unittest) { + return this.skip(); + } + const dir = path.join('a', 'b', 'c'); + const args = ['anzy', '--one', '--three', '--start-directory', dir, 'some other', '--value', '1234']; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(1); + expect(testDirs[0]).to.equal(dir); + }); + test('Test getting the test folder in nosetest', function () { + if (product !== Product.nosetest) { + return this.skip(); + } + const dir = path.join('a', 'b', 'c'); + const args = ['anzy', '--one', '--three', dir]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(1); + expect(testDirs[0]).to.equal(dir); + }); + test('Test getting the test folder in nosetest (with multiple dirs)', function () { + if (product !== Product.nosetest) { + return this.skip(); + } + const dir = path.join('a', 'b', 'c'); + const dir2 = path.join('a', 'b', '2'); + const args = ['anzy', '--one', '--three', dir, dir2]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(2); + expect(testDirs[0]).to.equal(dir); + expect(testDirs[1]).to.equal(dir2); + }); + test('Test getting the test folder in pytest', function () { + if (product !== Product.pytest) { + return this.skip(); + } + const dir = path.join('a', 'b', 'c'); + const args = ['anzy', '--one', '--rootdir', dir]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(1); + expect(testDirs[0]).to.equal(dir); + }); + test('Test getting the test folder in pytest (with multiple dirs)', function () { + if (product !== Product.pytest) { + return this.skip(); + } + const dir = path.join('a', 'b', 'c'); + const dir2 = path.join('a', 'b', '2'); + const args = ['anzy', '--one', '--rootdir', dir, '--rootdir', dir2]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(2); + expect(testDirs[0]).to.equal(dir); + expect(testDirs[1]).to.equal(dir2); + }); + test('Test getting the test folder in pytest (with multiple dirs in the middle)', function () { + if (product !== Product.pytest) { + return this.skip(); + } + const dir = path.join('a', 'b', 'c'); + const dir2 = path.join('a', 'b', '2'); + const args = ['anzy', '--one', '--rootdir', dir, '--rootdir', dir2, '-xyz']; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(2); + expect(testDirs[0]).to.equal(dir); + expect(testDirs[1]).to.equal(dir2); + }); + test('Test getting the test folder in pytest (with single positional dir)', function () { + if (product !== Product.pytest) { + return this.skip(); + } + const dir = path.join('a', 'b', 'c'); + const args = ['anzy', '--one', dir]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(1); + expect(testDirs[0]).to.equal(dir); + }); + test('Test getting the test folder in pytest (with multiple positional dirs)', function () { + if (product !== Product.pytest) { + return this.skip(); + } + const dir = path.join('a', 'b', 'c'); + const dir2 = path.join('a', 'b', '2'); + const args = ['anzy', '--one', dir, dir2]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(2); + expect(testDirs[0]).to.equal(dir); + expect(testDirs[1]).to.equal(dir2); + }); + test('Test getting the test folder in pytest (with multiple dirs excluding python files)', function () { + if (product !== Product.pytest) { + return this.skip(); + } + const dir = path.join('a', 'b', 'c'); + const dir2 = path.join('a', 'b', '2'); + const args = ['anzy', '--one', dir, dir2, path.join(dir, 'one.py')]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(2); + expect(testDirs[0]).to.equal(dir); + expect(testDirs[1]).to.equal(dir2); + }); + test('Test filtering of arguments', () => { + const args: string[] = []; + const knownOptions = argumentsService.getKnownOptions(); + const argumentsToRemove: string[] = []; + const expectedFilteredArgs: string[] = []; + // Generate some random arguments. + for (let i = 0; i < 5; i += 1) { + args.push(knownOptions.withArgs[i], `Random Value ${i}`); + args.push(knownOptions.withoutArgs[i]); + + if (i % 2 === 0) { + argumentsToRemove.push(knownOptions.withArgs[i], knownOptions.withoutArgs[i]); + } else { + expectedFilteredArgs.push(knownOptions.withArgs[i], `Random Value ${i}`); + expectedFilteredArgs.push(knownOptions.withoutArgs[i]); + } + } + + const filteredArgs = argumentsService.filterArguments(args, argumentsToRemove); + expect(filteredArgs).to.be.deep.equal(expectedFilteredArgs); + }); + }); + }); +}); + +function getOptions(product: Product, moduleName: string, withValues: boolean) { + // const result = spawnSync('/Users/donjayamanne/Desktop/Development/PythonStuff/vscodePythonTesting/testingFolder/venv/bin/python', ['-m', moduleName, '-h']); + const result = spawnSync(PYTHON_PATH, ['-m', moduleName, '-h']); + const output = result.stdout.toString(); + + // Our regex isn't the best, so lets exclude stuff that shouldn't be captured. + const knownOptionsWithoutArgs: string[] = []; + const knownOptionsWithArgs: string[] = []; + if (product === Product.pytest) { + knownOptionsWithArgs.push(...['-c', '-p', '-r']); + } + + if (withValues) { + return getOptionsWithArguments(output) + .concat(...knownOptionsWithArgs) + .filter(item => knownOptionsWithoutArgs.indexOf(item) === -1) + .sort(); + } else { + return getOptionsWithoutArguments(output) + .concat(...knownOptionsWithoutArgs) + .filter(item => knownOptionsWithArgs.indexOf(item) === -1) + // In pytest, any option begining with --log- is known to have args. + .filter(item => product === Product.pytest ? !item.startsWith('--log-') : true) + .sort(); + } +} + +function getOptionsWithoutArguments(output: string) { + return getMatches('\\s{1,}(-{1,2}[A-Za-z0-9-]+)(?:,|\\s{2,})', output); +} +function getOptionsWithArguments(output: string) { + return getMatches('\\s{1,}(-{1,2}[A-Za-z0-9-]+)(?:=|\\s{0,1}[A-Z])', output); +} + +function getMatches(pattern, str) { + const matches: string[] = []; + const regex = new RegExp(pattern, 'gm'); + let result; + while ((result = regex.exec(str)) !== null) { + if (result.index === regex.lastIndex) { + regex.lastIndex++; + } + matches.push(result[1].trim()); + } + return matches + .sort() + .reduce((items, item) => items.indexOf(item) === -1 ? items.concat([item]) : items, []); +} diff --git a/src/test/unittests/banners/languageServerSurvey.unit.test.ts b/src/test/unittests/banners/languageServerSurvey.unit.test.ts new file mode 100644 index 000000000000..bf3b1660cab8 --- /dev/null +++ b/src/test/unittests/banners/languageServerSurvey.unit.test.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { IBrowserService, IConfigurationService, IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; +import { LanguageServerSurveyBanner, LSSurveyStateKeys } from '../../../client/languageServices/languageServerSurveyBanner'; + +suite('Language Server Survey Banner', () => { + let config: typemoq.IMock; + let appShell: typemoq.IMock; + let browser: typemoq.IMock; + const message = 'Can you please take 2 minutes to tell us how the Experimental Debugger is working for you?'; + const yes = 'Yes, take survey now'; + const no = 'No, thanks'; + + setup(() => { + config = typemoq.Mock.ofType(); + appShell = typemoq.Mock.ofType(); + browser = typemoq.Mock.ofType(); + }); + test('Is debugger enabled upon creation?', () => { + const enabledValue: boolean = true; + const attemptCounter: number = 0; + const completionsCount: number = 0; + const testBanner: LanguageServerSurveyBanner = preparePopup(attemptCounter, completionsCount, enabledValue, 0, 100, appShell.object, browser.object); + expect(testBanner.enabled).to.be.equal(true, 'Sampling 100/100 should always enable the banner.'); + }); + test('Do not show banner when it is disabled', () => { + appShell.setup(a => a.showInformationMessage(typemoq.It.isValue(message), + typemoq.It.isValue(yes), + typemoq.It.isValue(no))) + .verifiable(typemoq.Times.never()); + const enabledValue: boolean = true; + const attemptCounter: number = 0; + const completionsCount: number = 0; + const testBanner: LanguageServerSurveyBanner = preparePopup(attemptCounter, completionsCount, enabledValue, 0, 0, appShell.object, browser.object); + testBanner.showBanner().ignoreErrors(); + }); + test('shouldShowBanner must return false when Banner is implicitly disabled by sampling', () => { + const enabledValue: boolean = true; + const attemptCounter: number = 0; + const completionsCount: number = 0; + const testBanner: LanguageServerSurveyBanner = preparePopup(attemptCounter, completionsCount, enabledValue, 0, 0, appShell.object, browser.object); + expect(testBanner.enabled).to.be.equal(false, 'We implicitly disabled the banner, it should never show.'); + }); +}); + +function preparePopup(attemptCounter: number, completionsCount: number, enabledValue: boolean, minCompletionCount: number, maxCompletionCount: number, appShell: IApplicationShell, browser: IBrowserService): LanguageServerSurveyBanner { + const myfactory: typemoq.IMock = typemoq.Mock.ofType(); + const enabledValState: typemoq.IMock> = typemoq.Mock.ofType>(); + const attemptCountState: typemoq.IMock> = typemoq.Mock.ofType>(); + const completionCountState: typemoq.IMock> = typemoq.Mock.ofType>(); + + enabledValState.setup(a => a.updateValue(typemoq.It.isValue(true))).returns(() => { + enabledValue = true; + return Promise.resolve(); + }); + enabledValState.setup(a => a.updateValue(typemoq.It.isValue(false))).returns(() => { + enabledValue = false; + return Promise.resolve(); + }); + + attemptCountState.setup(a => a.updateValue(typemoq.It.isAnyNumber())).returns(() => { + attemptCounter += 1; + return Promise.resolve(); + }); + + completionCountState.setup(a => a.updateValue(typemoq.It.isAnyNumber())).returns(() => { + completionsCount += 1; + return Promise.resolve(); + }); + + enabledValState.setup(a => a.value).returns(() => enabledValue); + attemptCountState.setup(a => a.value).returns(() => attemptCounter); + completionCountState.setup(a => a.value).returns(() => completionsCount); + + myfactory.setup(a => a.createGlobalPersistentState(typemoq.It.isValue(LSSurveyStateKeys.ShowBanner), + typemoq.It.isValue(true))).returns(() => { + return enabledValState.object; + }); + myfactory.setup(a => a.createGlobalPersistentState(typemoq.It.isValue(LSSurveyStateKeys.ShowBanner), + typemoq.It.isValue(false))).returns(() => { + return enabledValState.object; + }); + myfactory.setup(a => a.createGlobalPersistentState(typemoq.It.isValue(LSSurveyStateKeys.ShowAttemptCounter), + typemoq.It.isAnyNumber())).returns(() => { + return attemptCountState.object; + }); + myfactory.setup(a => a.createGlobalPersistentState(typemoq.It.isValue(LSSurveyStateKeys.ShowAfterCompletionCount), + typemoq.It.isAnyNumber())).returns(() => { + return completionCountState.object; + }); + return new LanguageServerSurveyBanner( + appShell, + myfactory.object, + browser, + minCompletionCount, + maxCompletionCount); +} diff --git a/src/test/unittests/banners/proposeNewLanguageServerBanner.unit.test.ts b/src/test/unittests/banners/proposeNewLanguageServerBanner.unit.test.ts new file mode 100644 index 000000000000..e11c7e75d637 --- /dev/null +++ b/src/test/unittests/banners/proposeNewLanguageServerBanner.unit.test.ts @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { IConfigurationService, IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; +import { ProposeLanguageServerBanner, ProposeLSStateKeys } from '../../../client/languageServices/proposeLanguageServerBanner'; + +suite('Propose New Language Server Banner', () => { + let config: typemoq.IMock; + let appShell: typemoq.IMock; + const message = 'Try out Preview of our new Python Language Server to get richer and faster IntelliSense completions, and syntax errors as you type.'; + const yes = 'Try it now'; + const no = 'No thanks'; + const later = 'Remind me Later'; + + setup(() => { + config = typemoq.Mock.ofType(); + appShell = typemoq.Mock.ofType(); + }); + test('Is debugger enabled upon creation?', () => { + const enabledValue: boolean = true; + const testBanner: ProposeLanguageServerBanner = preparePopup(enabledValue, 100, appShell.object, config.object); + expect(testBanner.enabled).to.be.equal(true, 'Sampling 100/100 should always enable the banner.'); + }); + test('Do not show banner when it is disabled', () => { + appShell.setup(a => a.showInformationMessage(typemoq.It.isValue(message), + typemoq.It.isValue(yes), + typemoq.It.isValue(no), + typemoq.It.isValue(later))) + .verifiable(typemoq.Times.never()); + const enabled: boolean = true; + const testBanner: ProposeLanguageServerBanner = preparePopup(enabled, 0, appShell.object, config.object); + testBanner.showBanner().ignoreErrors(); + }); + test('shouldShowBanner must return false when Banner is implicitly disabled by sampling', () => { + const enabled: boolean = true; + const testBanner: ProposeLanguageServerBanner = preparePopup(enabled, 0, appShell.object, config.object); + expect(testBanner.enabled).to.be.equal(false, 'We implicitly disabled the banner, it should never show.'); + }); + test('shouldShowBanner must return false when Banner is explicitly disabled', async () => { + const enabled: boolean = true; + const testBanner: ProposeLanguageServerBanner = preparePopup(enabled, 100, appShell.object, config.object); + + expect(await testBanner.shouldShowBanner()).to.be.equal(true, '100% sample size should always make the banner enabled.'); + await testBanner.disable(); + expect(await testBanner.shouldShowBanner()).to.be.equal(false, 'Explicitly disabled banner shouldShowBanner != false.'); + }); +}); + +function preparePopup(enabledValue: boolean, sampleValue: number, appShell: IApplicationShell, config: IConfigurationService): ProposeLanguageServerBanner { + const myfactory: typemoq.IMock = typemoq.Mock.ofType(); + const val: typemoq.IMock> = typemoq.Mock.ofType>(); + val.setup(a => a.updateValue(typemoq.It.isValue(true))).returns(() => { + enabledValue = true; + return Promise.resolve(); + }); + val.setup(a => a.updateValue(typemoq.It.isValue(false))).returns(() => { + enabledValue = false; + return Promise.resolve(); + }); + val.setup(a => a.value).returns(() => { + return enabledValue; + }); + myfactory.setup(a => a.createGlobalPersistentState(typemoq.It.isValue(ProposeLSStateKeys.ShowBanner), + typemoq.It.isValue(true))) + .returns(() => { + return val.object; + }); + myfactory.setup(a => a.createGlobalPersistentState(typemoq.It.isValue(ProposeLSStateKeys.ShowBanner), + typemoq.It.isValue(false))) + .returns(() => { + return val.object; + }); + return new ProposeLanguageServerBanner( + appShell, + myfactory.object, + config, + sampleValue); +} diff --git a/src/test/unittests/common/argsHelper.unit.test.ts b/src/test/unittests/common/argsHelper.unit.test.ts new file mode 100644 index 000000000000..29a9ac3261d1 --- /dev/null +++ b/src/test/unittests/common/argsHelper.unit.test.ts @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any no-conditional-assignment no-increment-decrement no-invalid-this no-require-imports no-var-requires +import { expect, use } from 'chai'; +import * as typeMoq from 'typemoq'; +import { ILogger } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { ArgumentsHelper } from '../../../client/unittests/common/argumentsHelper'; +import { IArgumentsHelper } from '../../../client/unittests/types'; +const assertArrays = require('chai-arrays'); +use(assertArrays); + +suite('Unit Tests - Arguments Helper', () => { + let argsHelper: IArgumentsHelper; + setup(() => { + const serviceContainer = typeMoq.Mock.ofType(); + const logger = typeMoq.Mock.ofType(); + + serviceContainer + .setup(s => s.get(typeMoq.It.isValue(ILogger), typeMoq.It.isAny())) + .returns(() => logger.object); + + argsHelper = new ArgumentsHelper(serviceContainer.object); + }); + + test('Get Option Value', () => { + const args = ['-abc', '1234', 'zys', '--root', 'value']; + const value = argsHelper.getOptionValues(args, '--root'); + expect(value).to.not.be.array(); + expect(value).to.be.deep.equal('value'); + }); + test('Get Option Value when using =', () => { + const args = ['-abc', '1234', 'zys', '--root=value']; + const value = argsHelper.getOptionValues(args, '--root'); + expect(value).to.not.be.array(); + expect(value).to.be.deep.equal('value'); + }); + test('Get Option Values', () => { + const args = ['-abc', '1234', 'zys', '--root', 'value1', '--root', 'value2']; + const values = argsHelper.getOptionValues(args, '--root'); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(2); + expect(values).to.be.deep.equal(['value1', 'value2']); + }); + test('Get Option Values when using =', () => { + const args = ['-abc', '1234', 'zys', '--root=value1', '--root=value2']; + const values = argsHelper.getOptionValues(args, '--root'); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(2); + expect(values).to.be.deep.equal(['value1', 'value2']); + }); + test('Get Positional options', () => { + const args = ['-abc', '1234', '--value-option', 'value1', '--no-value-option', 'value2']; + const values = argsHelper.getPositionalArguments(args, ['--value-option', '-abc'], ['--no-value-option']); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(1); + expect(values).to.be.deep.equal(['value2']); + }); + test('Get multiple Positional options', () => { + const args = ['-abc', '1234', '--value-option', 'value1', '--no-value-option', 'value2', 'value3']; + const values = argsHelper.getPositionalArguments(args, ['--value-option', '-abc'], ['--no-value-option']); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(2); + expect(values).to.be.deep.equal(['value2', 'value3']); + }); + test('Get multiple Positional options and ineline values', () => { + const args = ['-abc=1234', '--value-option=value1', '--no-value-option', 'value2', 'value3']; + const values = argsHelper.getPositionalArguments(args, ['--value-option', '-abc'], ['--no-value-option']); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(2); + expect(values).to.be.deep.equal(['value2', 'value3']); + }); + test('Get Positional options with trailing value option', () => { + const args = ['-abc', '1234', '--value-option', 'value1', '--value-option', 'value2', 'value3']; + const values = argsHelper.getPositionalArguments(args, ['--value-option', '-abc'], ['--no-value-option']); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(1); + expect(values).to.be.deep.equal(['value3']); + }); + test('Get multiplle Positional options with trailing value option', () => { + const args = ['-abc', '1234', '--value-option', 'value1', '--value-option', 'value2', 'value3', '4']; + const values = argsHelper.getPositionalArguments(args, ['--value-option', '-abc'], ['--no-value-option']); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(2); + expect(values).to.be.deep.equal(['value3', '4']); + }); + test('Filter to remove those with values', () => { + const args = ['-abc', '1234', '--value-option', 'value1', '--value-option', 'value2', 'value3', '4']; + const values = argsHelper.filterArguments(args, ['--value-option']); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(4); + expect(values).to.be.deep.equal(['-abc', '1234', 'value3', '4']); + }); + test('Filter to remove those without values', () => { + const args = ['-abc', '1234', '--value-option', 'value1', '--no-value-option', 'value2', 'value3', '4']; + const values = argsHelper.filterArguments(args, [], ['--no-value-option']); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(7); + expect(values).to.be.deep.equal(['-abc', '1234', '--value-option', 'value1', 'value2', 'value3', '4']); + }); + test('Filter to remove those with and without values', () => { + const args = ['-abc', '1234', '--value-option', 'value1', '--value-option', 'value2', 'value3', '4']; + const values = argsHelper.filterArguments(args, ['--value-option'], ['-abc']); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(3); + expect(values).to.be.deep.equal(['1234', 'value3', '4']); + }); +}); diff --git a/src/test/unittests/common/debugLauncher.test.ts b/src/test/unittests/common/debugLauncher.test.ts new file mode 100644 index 000000000000..f480e0b3ee80 --- /dev/null +++ b/src/test/unittests/common/debugLauncher.test.ts @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { CancellationTokenSource, Uri, WorkspaceFolder } from 'vscode'; +import { IDebugService, IWorkspaceService } from '../../../client/common/application/types'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import '../../../client/common/extensions'; +import { IConfigurationService, IPythonSettings, IUnitTestSettings } from '../../../client/common/types'; +import { DebugOptions } from '../../../client/debugger/Common/Contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { DebugLauncher } from '../../../client/unittests/common/debugLauncher'; +import { TestProvider } from '../../../client/unittests/common/types'; + +use(chaiAsPromised); + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests - Debug Launcher', () => { + let unitTestSettings: TypeMoq.IMock; + let debugLauncher: DebugLauncher; + let debugService: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + let settings: TypeMoq.IMock; + setup(async () => { + const serviceContainer = TypeMoq.Mock.ofType(); + const configService = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configService.object); + + debugService = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDebugService))).returns(() => debugService.object); + + workspaceService = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); + + settings = TypeMoq.Mock.ofType(); + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + + unitTestSettings = TypeMoq.Mock.ofType(); + settings.setup(p => p.unitTest).returns(() => unitTestSettings.object); + + debugLauncher = new DebugLauncher(serviceContainer.object); + }); + function setupDebugManager(workspaceFolder: WorkspaceFolder, name: string, type: string, + request: string, program: string, cwd: string, + args: string[], console, debugOptions: DebugOptions[], + testProvider: TestProvider, useExperimentalDebugger: boolean) { + + const envFile = __filename; + settings.setup(p => p.envFile).returns(() => envFile); + const debugArgs = testProvider === 'unittest' && useExperimentalDebugger ? args.filter(item => item !== '--debug') : args; + + debugService.setup(d => d.startDebugging(TypeMoq.It.isValue(workspaceFolder), + TypeMoq.It.isObjectWith({ name, type, request, program, cwd, args: debugArgs, console, envFile, debugOptions }))) + .returns(() => Promise.resolve(undefined as any)) + .verifiable(TypeMoq.Times.once()); + } + function createWorkspaceFolder(folderPath: string): WorkspaceFolder { + return { index: 0, name: path.basename(folderPath), uri: Uri.file(folderPath) }; + } + function getTestLauncherScript(testProvider: TestProvider, useExperimentalDebugger: boolean) { + switch (testProvider) { + case 'unittest': { + return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'PythonTools', 'visualstudio_py_testlauncher.py'); + } + case 'pytest': + case 'nosetest': { + if (useExperimentalDebugger) { + return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'testlauncher.py'); + } else { + return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'PythonTools', 'testlauncher.py'); + } + + } + default: { + throw new Error(`Unknown test provider '${testProvider}'`); + } + } + } + const testProviders: TestProvider[] = ['nosetest', 'pytest', 'unittest']; + testProviders.forEach(testProvider => { + [true, false].forEach(useExperimentalDebugger => { + const testTitleSuffix = `(Test Framework '${testProvider}', and use experimental debugger = '${useExperimentalDebugger}')`; + const testLaunchScript = getTestLauncherScript(testProvider, useExperimentalDebugger); + const debuggerType = useExperimentalDebugger ? 'pythonExperimental' : 'python'; + + test(`Must launch debugger ${testTitleSuffix}`, async () => { + unitTestSettings.setup(u => u.useExperimentalDebugger).returns(() => useExperimentalDebugger); + workspaceService.setup(u => u.hasWorkspaceFolders).returns(() => true); + const workspaceFolders = [createWorkspaceFolder('one/two/three'), createWorkspaceFolder('five/six/seven')]; + workspaceService.setup(u => u.workspaceFolders).returns(() => workspaceFolders); + workspaceService.setup(u => u.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolders[0]); + + const args = ['/one/two/three/testfile.py']; + const cwd = workspaceFolders[0].uri.fsPath; + const program = testLaunchScript; + setupDebugManager(workspaceFolders[0], 'Debug Unit Test', debuggerType, 'launch', program, cwd, args, 'none', [DebugOptions.RedirectOutput], testProvider, useExperimentalDebugger); + + debugLauncher.launchDebugger({ cwd, args, testProvider }).ignoreErrors(); + debugService.verifyAll(); + }); + test(`Must launch debugger with arguments ${testTitleSuffix}`, async () => { + unitTestSettings.setup(u => u.useExperimentalDebugger).returns(() => useExperimentalDebugger); + workspaceService.setup(u => u.hasWorkspaceFolders).returns(() => true); + const workspaceFolders = [createWorkspaceFolder('one/two/three'), createWorkspaceFolder('five/six/seven')]; + workspaceService.setup(u => u.workspaceFolders).returns(() => workspaceFolders); + workspaceService.setup(u => u.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolders[0]); + + const args = ['/one/two/three/testfile.py', '--debug', '1']; + const cwd = workspaceFolders[0].uri.fsPath; + const program = testLaunchScript; + setupDebugManager(workspaceFolders[0], 'Debug Unit Test', debuggerType, 'launch', program, cwd, args, 'none', [DebugOptions.RedirectOutput], testProvider, useExperimentalDebugger); + + debugLauncher.launchDebugger({ cwd, args, testProvider }).ignoreErrors(); + debugService.verifyAll(); + }); + test(`Must not launch debugger if cancelled ${testTitleSuffix}`, async () => { + unitTestSettings.setup(u => u.useExperimentalDebugger).returns(() => false); + workspaceService.setup(u => u.hasWorkspaceFolders).returns(() => true); + + debugService.setup(d => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined as any)) + .verifiable(TypeMoq.Times.never()); + + const cancellationToken = new CancellationTokenSource(); + cancellationToken.cancel(); + const token = cancellationToken.token; + await expect(debugLauncher.launchDebugger({ cwd: '', args: [], token, testProvider })).to.be.eventually.equal(undefined, 'not undefined'); + debugService.verifyAll(); + }); + test(`Must throw an exception if there are no workspaces ${testTitleSuffix}`, async () => { + unitTestSettings.setup(u => u.useExperimentalDebugger).returns(() => false); + workspaceService.setup(u => u.hasWorkspaceFolders).returns(() => false); + + debugService.setup(d => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined as any)) + .verifiable(TypeMoq.Times.never()); + + await expect(debugLauncher.launchDebugger({ cwd: '', args: [], testProvider })).to.eventually.rejectedWith('Please open a workspace'); + debugService.verifyAll(); + }); + }); + }); +}); diff --git a/src/test/unittests/common/managers/testConfigurationManager.unit.test.ts b/src/test/unittests/common/managers/testConfigurationManager.unit.test.ts new file mode 100644 index 000000000000..5c7e7ac54691 --- /dev/null +++ b/src/test/unittests/common/managers/testConfigurationManager.unit.test.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import * as TypeMoq from 'typemoq'; +import { OutputChannel, Uri } from 'vscode'; +import { EnumEx } from '../../../../client/common/enumUtils'; +import { IInstaller, IOutputChannel, Product } from '../../../../client/common/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { TEST_OUTPUT_CHANNEL } from '../../../../client/unittests/common/constants'; +import { TestConfigurationManager } from '../../../../client/unittests/common/managers/testConfigurationManager'; +import { ITestConfigSettingsService, UnitTestProduct } from '../../../../client/unittests/common/types'; + +class MockTestConfigurationManager extends TestConfigurationManager { + public requiresUserToConfigure(wkspace: Uri): Promise { + throw new Error('Method not implemented.'); + } + public configure(wkspace: any): Promise { + throw new Error('Method not implemented.'); + } +} + +suite('Unit Test Configuration Manager (unit)', () => { + [Product.pytest, Product.unittest, Product.nosetest].forEach(product => { + const prods = EnumEx.getNamesAndValues(Product); + const productName = prods.filter(item => item.value === product)[0]; + suite(productName.name, () => { + const workspaceUri = Uri.file(__dirname); + let manager: TestConfigurationManager; + let configService: TypeMoq.IMock; + + setup(() => { + configService = TypeMoq.Mock.ofType(); + const outputChannel = TypeMoq.Mock.ofType().object; + const installer = TypeMoq.Mock.ofType().object; + const serviceContainer = TypeMoq.Mock.ofType(); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isValue(TEST_OUTPUT_CHANNEL))).returns(() => outputChannel); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ITestConfigSettingsService))).returns(() => configService.object); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IInstaller))).returns(() => installer); + manager = new MockTestConfigurationManager(workspaceUri, product as UnitTestProduct, serviceContainer.object); + }); + + test('Enabling a test product shoud disable other products', async () => { + const testProducsToDisable = [Product.pytest, Product.unittest, Product.nosetest] + .filter(item => item !== product) as UnitTestProduct[]; + testProducsToDisable.forEach(productToDisable => { + configService.setup(c => c.disable(TypeMoq.It.isValue(workspaceUri), + TypeMoq.It.isValue(productToDisable))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + }); + configService.setup(c => c.enable(TypeMoq.It.isValue(workspaceUri), + TypeMoq.It.isValue(product as UnitTestProduct))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + + await manager.enable(); + configService.verifyAll(); + }); + }); + }); +}); diff --git a/src/test/unittests/common/services/configSettingService.unit.test.ts b/src/test/unittests/common/services/configSettingService.unit.test.ts new file mode 100644 index 000000000000..722847efce2f --- /dev/null +++ b/src/test/unittests/common/services/configSettingService.unit.test.ts @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import { expect, use } from 'chai'; +import * as chaiPromise from 'chai-as-promised'; +import * as typeMoq from 'typemoq'; +import { Uri, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { EnumEx } from '../../../../client/common/enumUtils'; +import { Product } from '../../../../client/common/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { TestConfigSettingsService } from '../../../../client/unittests/common/services/configSettingService'; +import { ITestConfigSettingsService, UnitTestProduct } from '../../../../client/unittests/common/types'; + +use(chaiPromise); + +const updateMethods: (keyof ITestConfigSettingsService)[] = ['updateTestArgs', 'disable', 'enable']; + +suite('Unit Tests - ConfigSettingsService', () => { + [Product.pytest, Product.unittest, Product.nosetest].forEach(prodItem => { + const product = prodItem as any as UnitTestProduct; + const prods = EnumEx.getNamesAndValues(Product); + const productName = prods.filter(item => item.value === product)[0]; + const workspaceUri = Uri.file(__filename); + updateMethods.forEach(updateMethod => { + suite(`Test '${updateMethod}' method with ${productName.name}`, () => { + let testConfigSettingsService: ITestConfigSettingsService; + let workspaceService: typeMoq.IMock; + setup(() => { + const serviceContainer = typeMoq.Mock.ofType(); + workspaceService = typeMoq.Mock.ofType(); + + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); + testConfigSettingsService = new TestConfigSettingsService(serviceContainer.object); + }); + function getTestArgSetting(prod: UnitTestProduct) { + switch (prod) { + 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'); + } + } + function getTestEnablingSetting(prod: UnitTestProduct) { + switch (prod) { + 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'); + } + } + function getExpectedValueAndSettings(): { configValue: any; configName: string } { + switch (updateMethod) { + case 'disable': { + return { configValue: false, configName: getTestEnablingSetting(product) }; + } + case 'enable': { + return { configValue: true, configName: getTestEnablingSetting(product) }; + } + case 'updateTestArgs': { + return { configValue: ['one', 'two', 'three'], configName: getTestArgSetting(product) }; + } + default: { + throw new Error('Invalid Method'); + } + } + } + test('Update Test Arguments with workspace Uri without workspaces', async () => { + workspaceService.setup(w => w.hasWorkspaceFolders) + .returns(() => false) + .verifiable(typeMoq.Times.atLeastOnce()); + + const pythonConfig = typeMoq.Mock.ofType(); + workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'))) + .returns(() => pythonConfig.object) + .verifiable(typeMoq.Times.once()); + + const { configValue, configName } = getExpectedValueAndSettings(); + + pythonConfig.setup(p => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + if (updateMethod === 'updateTestArgs') { + await testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); + } else { + await testConfigSettingsService[updateMethod](workspaceUri, product); + } + workspaceService.verifyAll(); + pythonConfig.verifyAll(); + }); + test('Update Test Arguments with workspace Uri with one workspace', async () => { + workspaceService.setup(w => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(typeMoq.Times.atLeastOnce()); + + const workspaceFolder = typeMoq.Mock.ofType(); + workspaceFolder.setup(w => w.uri) + .returns(() => workspaceUri) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService.setup(w => w.workspaceFolders) + .returns(() => [workspaceFolder.object]) + .verifiable(typeMoq.Times.atLeastOnce()); + + const pythonConfig = typeMoq.Mock.ofType(); + workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), typeMoq.It.isValue(workspaceUri))) + .returns(() => pythonConfig.object) + .verifiable(typeMoq.Times.once()); + + const { configValue, configName } = getExpectedValueAndSettings(); + pythonConfig.setup(p => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + if (updateMethod === 'updateTestArgs') { + await testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); + } else { + await testConfigSettingsService[updateMethod](workspaceUri, product); + } + + workspaceService.verifyAll(); + pythonConfig.verifyAll(); + }); + test('Update Test Arguments with workspace Uri with more than one workspace and uri belongs to a workspace', async () => { + workspaceService.setup(w => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(typeMoq.Times.atLeastOnce()); + + const workspaceFolder = typeMoq.Mock.ofType(); + workspaceFolder.setup(w => w.uri) + .returns(() => workspaceUri) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService.setup(w => w.workspaceFolders) + .returns(() => [workspaceFolder.object, workspaceFolder.object]) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService.setup(w => w.getWorkspaceFolder(typeMoq.It.isValue(workspaceUri))) + .returns(() => workspaceFolder.object) + .verifiable(typeMoq.Times.once()); + + const pythonConfig = typeMoq.Mock.ofType(); + workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), typeMoq.It.isValue(workspaceUri))) + .returns(() => pythonConfig.object) + .verifiable(typeMoq.Times.once()); + + const { configValue, configName } = getExpectedValueAndSettings(); + pythonConfig.setup(p => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + if (updateMethod === 'updateTestArgs') { + await testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); + } else { + await testConfigSettingsService[updateMethod](workspaceUri, product); + } + + workspaceService.verifyAll(); + pythonConfig.verifyAll(); + }); + test('Expect an exception when updating Test Arguments with workspace Uri with more than one workspace and uri does not belong to a workspace', async () => { + workspaceService.setup(w => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(typeMoq.Times.atLeastOnce()); + + const workspaceFolder = typeMoq.Mock.ofType(); + workspaceFolder.setup(w => w.uri) + .returns(() => workspaceUri) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService.setup(w => w.workspaceFolders) + .returns(() => [workspaceFolder.object, workspaceFolder.object]) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService.setup(w => w.getWorkspaceFolder(typeMoq.It.isValue(workspaceUri))) + .returns(() => undefined) + .verifiable(typeMoq.Times.once()); + + const { configValue } = getExpectedValueAndSettings(); + + const promise = testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); + expect(promise).to.eventually.rejectedWith(); + workspaceService.verifyAll(); + }); + }); + }); + }); +}); diff --git a/src/test/unittests/configuration.unit.test.ts b/src/test/unittests/configuration.unit.test.ts new file mode 100644 index 000000000000..d98da27f72dd --- /dev/null +++ b/src/test/unittests/configuration.unit.test.ts @@ -0,0 +1,340 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import { expect } from 'chai'; +import * as typeMoq from 'typemoq'; +import { OutputChannel, Uri, WorkspaceConfiguration } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; +import { EnumEx } from '../../client/common/enumUtils'; +import { IConfigurationService, IInstaller, IOutputChannel, IPythonSettings, IUnitTestSettings, Product } from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { TEST_OUTPUT_CHANNEL } from '../../client/unittests/common/constants'; +import { UnitTestProduct } from '../../client/unittests/common/types'; +import { UnitTestConfigurationService } from '../../client/unittests/configuration'; +import { ITestConfigurationManager, ITestConfigurationManagerFactory } from '../../client/unittests/types'; + +suite('Unit Tests - ConfigurationService', () => { + [Product.pytest, Product.unittest, Product.nosetest].forEach(prodItem => { + const product = prodItem as any as UnitTestProduct; + const prods = EnumEx.getNamesAndValues(Product); + const productName = prods.filter(item => item.value === product)[0]; + const workspaceUri = Uri.file(__filename); + suite(productName.name, () => { + let testConfigService: typeMoq.IMock; + let workspaceService: typeMoq.IMock; + let factory: typeMoq.IMock; + let appShell: typeMoq.IMock; + let unitTestSettings: typeMoq.IMock; + setup(() => { + const serviceContainer = typeMoq.Mock.ofType(); + const configurationService = typeMoq.Mock.ofType(); + appShell = typeMoq.Mock.ofType(); + const outputChannel = typeMoq.Mock.ofType(); + const installer = typeMoq.Mock.ofType(); + workspaceService = typeMoq.Mock.ofType(); + factory = typeMoq.Mock.ofType(); + unitTestSettings = typeMoq.Mock.ofType(); + const pythonSettings = typeMoq.Mock.ofType(); + + pythonSettings.setup(p => p.unitTest).returns(() => unitTestSettings.object); + configurationService.setup(c => c.getSettings(workspaceUri)).returns(() => pythonSettings.object); + + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IOutputChannel), typeMoq.It.isValue(TEST_OUTPUT_CHANNEL))).returns(() => outputChannel.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IConfigurationService))).returns(() => configurationService.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(ITestConfigurationManagerFactory))).returns(() => factory.object); + testConfigService = typeMoq.Mock.ofType(UnitTestConfigurationService, typeMoq.MockBehavior.Loose, true, serviceContainer.object); + }); + test('Enable Test when setting unitTest.promptToConfigure is enabled', async () => { + const configMgr = typeMoq.Mock.ofType(); + configMgr.setup(c => c.enable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + factory.setup(f => f.create(workspaceUri, product)) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + const workspaceConfig = typeMoq.Mock.ofType(); + workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + .returns(() => workspaceConfig.object) + .verifiable(typeMoq.Times.once()); + + workspaceConfig.setup(w => w.get(typeMoq.It.isValue('unitTest.promptToConfigure'))) + .returns(() => true) + .verifiable(typeMoq.Times.once()); + + await testConfigService.target.enableTest(workspaceUri, product); + + configMgr.verifyAll(); + factory.verifyAll(); + workspaceService.verifyAll(); + workspaceConfig.verifyAll(); + }); + test('Enable Test when setting unitTest.promptToConfigure is disabled', async () => { + const configMgr = typeMoq.Mock.ofType(); + configMgr.setup(c => c.enable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + factory.setup(f => f.create(workspaceUri, product)) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + const workspaceConfig = typeMoq.Mock.ofType(); + workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + .returns(() => workspaceConfig.object) + .verifiable(typeMoq.Times.once()); + + workspaceConfig.setup(w => w.get(typeMoq.It.isValue('unitTest.promptToConfigure'))) + .returns(() => false) + .verifiable(typeMoq.Times.once()); + + workspaceConfig.setup(w => w.update(typeMoq.It.isValue('unitTest.promptToConfigure'), typeMoq.It.isValue(undefined))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + await testConfigService.target.enableTest(workspaceUri, product); + + configMgr.verifyAll(); + factory.verifyAll(); + workspaceService.verifyAll(); + workspaceConfig.verifyAll(); + }); + test('Enable Test when setting unitTest.promptToConfigure is disabled and fail to update the settings', async () => { + const configMgr = typeMoq.Mock.ofType(); + configMgr.setup(c => c.enable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + factory.setup(f => f.create(workspaceUri, product)) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + const workspaceConfig = typeMoq.Mock.ofType(); + workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + .returns(() => workspaceConfig.object) + .verifiable(typeMoq.Times.once()); + + workspaceConfig.setup(w => w.get(typeMoq.It.isValue('unitTest.promptToConfigure'))) + .returns(() => false) + .verifiable(typeMoq.Times.once()); + + const errorMessage = 'Update Failed'; + const updateFailError = new Error(errorMessage); + workspaceConfig.setup(w => w.update(typeMoq.It.isValue('unitTest.promptToConfigure'), typeMoq.It.isValue(undefined))) + .returns(() => Promise.reject(updateFailError)) + .verifiable(typeMoq.Times.once()); + + const promise = testConfigService.target.enableTest(workspaceUri, product); + + await expect(promise).to.eventually.be.rejectedWith(errorMessage); + configMgr.verifyAll(); + factory.verifyAll(); + workspaceService.verifyAll(); + workspaceConfig.verifyAll(); + }); + test('Select Test runner displays 3 items', async () => { + const placeHolder = 'Some message'; + appShell.setup(s => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) + .callback(items => expect(items).be.lengthOf(3)) + .verifiable(typeMoq.Times.once()); + + await testConfigService.target.selectTestRunner(placeHolder); + appShell.verifyAll(); + }); + test('Ensure selected item is returned', async () => { + const placeHolder = 'Some message'; + const indexes = [Product.unittest, Product.pytest, Product.nosetest]; + appShell.setup(s => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) + .callback(items => expect(items).be.lengthOf(3)) + .returns((items) => items[indexes.indexOf(product)]) + .verifiable(typeMoq.Times.once()); + + const selectedItem = await testConfigService.target.selectTestRunner(placeHolder); + expect(selectedItem).to.be.equal(product); + appShell.verifyAll(); + }); + test('Ensure undefined is returned when nothing is seleted', async () => { + const placeHolder = 'Some message'; + appShell.setup(s => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.once()); + + const selectedItem = await testConfigService.target.selectTestRunner(placeHolder); + expect(selectedItem).to.be.equal(undefined, 'invalid value'); + appShell.verifyAll(); + }); + test('Prompt to enable a test if a test framework is not enabled', async () => { + unitTestSettings.setup(u => u.pyTestEnabled).returns(() => false); + unitTestSettings.setup(u => u.unittestEnabled).returns(() => false); + unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => false); + + appShell.setup(s => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.once()); + + let exceptionThrown = false; + try { + await testConfigService.target.displayTestFrameworkError(workspaceUri); + } catch { + exceptionThrown = true; + } + + expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); + appShell.verifyAll(); + }); + test('Prompt to select a test if a test framework is not enabled', async () => { + unitTestSettings.setup(u => u.pyTestEnabled).returns(() => false); + unitTestSettings.setup(u => u.unittestEnabled).returns(() => false); + unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => false); + + appShell.setup(s => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((_msg, option) => Promise.resolve(option)) + .verifiable(typeMoq.Times.once()); + + let exceptionThrown = false; + let selectTestRunnerInvoked = false; + try { + testConfigService.callBase = false; + testConfigService.setup(t => t.selectTestRunner(typeMoq.It.isAny())) + .returns(() => { + selectTestRunnerInvoked = true; + return Promise.resolve(undefined); + }); + await testConfigService.target.displayTestFrameworkError(workspaceUri); + } catch { + exceptionThrown = true; + } + + expect(selectTestRunnerInvoked).to.be.equal(true, 'Method not invoked'); + expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); + appShell.verifyAll(); + }); + test('Configure selected test framework and disable others', async () => { + unitTestSettings.setup(u => u.pyTestEnabled).returns(() => false); + unitTestSettings.setup(u => u.unittestEnabled).returns(() => false); + unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => false); + + appShell.setup(s => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((_msg, option) => Promise.resolve(option)) + .verifiable(typeMoq.Times.once()); + + let selectTestRunnerInvoked = false; + testConfigService.callBase = false; + testConfigService.setup(t => t.selectTestRunner(typeMoq.It.isAny())) + .returns(() => { + selectTestRunnerInvoked = true; + return Promise.resolve(product as any); + }); + + let enableTestInvoked = false; + testConfigService.setup(t => t.enableTest(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product))) + .returns(() => { + enableTestInvoked = true; + return Promise.resolve(); + }); + + const configMgr = typeMoq.Mock.ofType(); + factory.setup(f => f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product))) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + configMgr.setup(c => c.configure(typeMoq.It.isValue(workspaceUri))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + await testConfigService.target.displayTestFrameworkError(workspaceUri); + + expect(selectTestRunnerInvoked).to.be.equal(true, 'Select Test Runner not invoked'); + expect(enableTestInvoked).to.be.equal(true, 'Enable Test not invoked'); + appShell.verifyAll(); + factory.verifyAll(); + configMgr.verifyAll(); + }); + test('If more than one test framework is enabled, then prompt to select a test framework', async () => { + unitTestSettings.setup(u => u.pyTestEnabled).returns(() => true); + unitTestSettings.setup(u => u.unittestEnabled).returns(() => true); + unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => true); + + appShell.setup(s => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.never()); + + let exceptionThrown = false; + try { + await testConfigService.target.displayTestFrameworkError(workspaceUri); + } catch { + exceptionThrown = true; + } + + expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); + appShell.verifyAll(); + }); + test('If more than one test framework is enabled, then prompt to select a test framework and enable test, but do not configure', async () => { + unitTestSettings.setup(u => u.pyTestEnabled).returns(() => true); + unitTestSettings.setup(u => u.unittestEnabled).returns(() => true); + unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => true); + + appShell.setup(s => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((_msg, option) => Promise.resolve(option)) + .verifiable(typeMoq.Times.never()); + + let selectTestRunnerInvoked = false; + testConfigService.callBase = false; + testConfigService.setup(t => t.selectTestRunner(typeMoq.It.isAny())) + .returns(() => { + selectTestRunnerInvoked = true; + return Promise.resolve(product as any); + }); + + let enableTestInvoked = false; + testConfigService.setup(t => t.enableTest(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product))) + .returns(() => { + enableTestInvoked = true; + return Promise.resolve(); + }); + + const configMgr = typeMoq.Mock.ofType(); + factory.setup(f => f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product))) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + configMgr.setup(c => c.configure(typeMoq.It.isValue(workspaceUri))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.never()); + const configManagersToVerify: typeof configMgr[] = [configMgr]; + + [Product.unittest, Product.pytest, Product.nosetest] + .filter(prod => product !== prod) + .forEach(prod => { + const otherTestConfigMgr = typeMoq.Mock.ofType(); + factory.setup(f => f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(prod))) + .returns(() => otherTestConfigMgr.object) + .verifiable(typeMoq.Times.once()); + otherTestConfigMgr.setup(c => c.disable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + configManagersToVerify.push(otherTestConfigMgr); + }); + + await testConfigService.target.displayTestFrameworkError(workspaceUri); + + expect(selectTestRunnerInvoked).to.be.equal(true, 'Select Test Runner not invoked'); + expect(enableTestInvoked).to.be.equal(false, 'Enable Test is invoked'); + factory.verifyAll(); + appShell.verifyAll(); + for (const item of configManagersToVerify) { + item.verifyAll(); + } + }); + }); + }); +}); diff --git a/src/test/unittests/configurationFactory.unit.test.ts b/src/test/unittests/configurationFactory.unit.test.ts new file mode 100644 index 000000000000..23e316f4ef55 --- /dev/null +++ b/src/test/unittests/configurationFactory.unit.test.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as typeMoq from 'typemoq'; +import { OutputChannel, Uri } from 'vscode'; +import { IInstaller, IOutputChannel, Product } from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { TEST_OUTPUT_CHANNEL } from '../../client/unittests/common/constants'; +import { ITestConfigSettingsService } from '../../client/unittests/common/types'; +import { TestConfigurationManagerFactory } from '../../client/unittests/configurationFactory'; +import * as nose from '../../client/unittests/nosetest/testConfigurationManager'; +import * as pytest from '../../client/unittests/pytest/testConfigurationManager'; +import { ITestConfigurationManagerFactory } from '../../client/unittests/types'; +import * as unittest from '../../client/unittests/unittest/testConfigurationManager'; + +use(chaiAsPromised); + +suite('Unit Tests - ConfigurationManagerFactory', () => { + let factory: ITestConfigurationManagerFactory; + setup(() => { + const serviceContainer = typeMoq.Mock.ofType(); + const outputChannel = typeMoq.Mock.ofType(); + const installer = typeMoq.Mock.ofType(); + const testConfigService = typeMoq.Mock.ofType(); + + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IOutputChannel), typeMoq.It.isValue(TEST_OUTPUT_CHANNEL))).returns(() => outputChannel.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(ITestConfigSettingsService))).returns(() => testConfigService.object); + factory = new TestConfigurationManagerFactory(serviceContainer.object); + }); + test('Create Unit Test Configuration', async () => { + const configMgr = factory.create(Uri.file(__filename), Product.unittest); + expect(configMgr).to.be.instanceOf(unittest.ConfigurationManager); + }); + test('Create pytest Configuration', async () => { + const configMgr = factory.create(Uri.file(__filename), Product.pytest); + expect(configMgr).to.be.instanceOf(pytest.ConfigurationManager); + }); + test('Create nose Configuration', async () => { + const configMgr = factory.create(Uri.file(__filename), Product.nosetest); + expect(configMgr).to.be.instanceOf(nose.ConfigurationManager); + }); +}); diff --git a/src/test/unittests/debugger.test.ts b/src/test/unittests/debugger.test.ts index c526961527a9..d687de8b6755 100644 --- a/src/test/unittests/debugger.test.ts +++ b/src/test/unittests/debugger.test.ts @@ -3,8 +3,19 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as path from 'path'; import { ConfigurationTarget } from 'vscode'; import { createDeferred } from '../../client/common/helpers'; -import { CANCELLATION_REASON, CommandSource } from '../../client/unittests/common/constants'; -import { ITestDebugLauncher, ITestManagerFactory, TestProvider } from '../../client/unittests/common/types'; +import { TestManagerRunner as NoseTestManagerRunner } from '../../client/unittests//nosetest/runner'; +import { TestManagerRunner as PytestManagerRunner } from '../../client/unittests//pytest/runner'; +import { TestManagerRunner as UnitTestTestManagerRunner } from '../../client/unittests//unittest/runner'; +import { ArgumentsHelper } from '../../client/unittests/common/argumentsHelper'; +import { CANCELLATION_REASON, CommandSource, NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../client/unittests/common/constants'; +import { TestRunner } from '../../client/unittests/common/runner'; +import { ITestDebugLauncher, ITestManagerFactory, ITestRunner, IXUnitParser, TestProvider } from '../../client/unittests/common/types'; +import { XUnitParser } from '../../client/unittests/common/xUnitParser'; +import { ArgumentsService as NoseTestArgumentsService } from '../../client/unittests/nosetest/services/argsService'; +import { ArgumentsService as PyTestArgumentsService } from '../../client/unittests/pytest/services/argsService'; +import { IArgumentsHelper, IArgumentsService, ITestManagerRunner, IUnitTestHelper } from '../../client/unittests/types'; +import { UnitTestHelper } from '../../client/unittests/unittest/helper'; +import { ArgumentsService as UnitTestArgumentsService } from '../../client/unittests/unittest/services/argsService'; import { deleteDirectory, rootWorkspaceUri, updateSetting } from '../common'; import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; import { MockDebugLauncher } from './mocks'; @@ -60,6 +71,16 @@ suite('Unit Tests - debugging', () => { ioc.registerTestsHelper(); ioc.registerTestManagers(); ioc.registerMockUnitTestSocketServer(); + ioc.serviceManager.add(IArgumentsHelper, ArgumentsHelper); + ioc.serviceManager.add(ITestRunner, TestRunner); + ioc.serviceManager.add(IXUnitParser, XUnitParser); + ioc.serviceManager.add(IUnitTestHelper, UnitTestHelper); + ioc.serviceManager.add(IArgumentsService, NoseTestArgumentsService, NOSETEST_PROVIDER); + ioc.serviceManager.add(IArgumentsService, PyTestArgumentsService, PYTEST_PROVIDER); + ioc.serviceManager.add(IArgumentsService, UnitTestArgumentsService, UNITTEST_PROVIDER); + ioc.serviceManager.add(ITestManagerRunner, PytestManagerRunner, PYTEST_PROVIDER); + ioc.serviceManager.add(ITestManagerRunner, NoseTestManagerRunner, NOSETEST_PROVIDER); + ioc.serviceManager.add(ITestManagerRunner, UnitTestTestManagerRunner, UNITTEST_PROVIDER); ioc.serviceManager.addSingleton(ITestDebugLauncher, MockDebugLauncher); } @@ -77,8 +98,8 @@ suite('Unit Tests - debugging', () => { // This promise should never resolve nor reject. runningPromise - .then(() => deferred.reject('Debugger stopped when it shouldn\'t have')) - .catch(error => deferred.reject(error)); + .then(() => deferred.reject('Debugger stopped when it shouldn\'t have')) + .catch(error => deferred.reject(error)); mockDebugLauncher.launched .then((launched) => { @@ -87,14 +108,14 @@ suite('Unit Tests - debugging', () => { } else { deferred.reject('Debugger not launched'); } - }) .catch(error => deferred.reject(error)); + }).catch(error => deferred.reject(error)); await deferred.promise; } test('Debugger should start (unittest)', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); - await testStartingDebugger('unittest'); + await testStartingDebugger('unittest'); }); test('Debugger should start (pytest)', async () => { diff --git a/src/test/unittests/display/main.test.ts b/src/test/unittests/display/main.test.ts new file mode 100644 index 000000000000..67de45bdaaf6 --- /dev/null +++ b/src/test/unittests/display/main.test.ts @@ -0,0 +1,366 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import { expect } from 'chai'; +import * as typeMoq from 'typemoq'; +import { StatusBarItem, Uri } from 'vscode'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { Commands } from '../../../client/common/constants'; +import { noop } from '../../../client/common/core.utils'; +import { createDeferred } from '../../../client/common/helpers'; +import { IConfigurationService, IPythonSettings, IUnitTestSettings } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { CANCELLATION_REASON } from '../../../client/unittests/common/constants'; +import { ITestsHelper, Tests } from '../../../client/unittests/common/types'; +import { TestResultDisplay } from '../../../client/unittests/display/main'; +import { sleep } from '../../core'; + +suite('Unit Tests - TestResultDisplay', () => { + const workspaceUri = Uri.file(__filename); + let appShell: typeMoq.IMock; + let unitTestSettings: typeMoq.IMock; + let serviceContainer: typeMoq.IMock; + let display: TestResultDisplay; + let testsHelper: typeMoq.IMock; + let configurationService: typeMoq.IMock; + setup(() => { + serviceContainer = typeMoq.Mock.ofType(); + configurationService = typeMoq.Mock.ofType(); + appShell = typeMoq.Mock.ofType(); + unitTestSettings = typeMoq.Mock.ofType(); + const pythonSettings = typeMoq.Mock.ofType(); + testsHelper = typeMoq.Mock.ofType(); + + pythonSettings.setup(p => p.unitTest).returns(() => unitTestSettings.object); + configurationService.setup(c => c.getSettings(workspaceUri)).returns(() => pythonSettings.object); + + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IConfigurationService))).returns(() => configurationService.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(ITestsHelper))).returns(() => testsHelper.object); + }); + teardown(() => { + try { + display.dispose(); + } catch { noop(); } + }); + function createTestResultDisplay() { + display = new TestResultDisplay(serviceContainer.object); + } + test('Should create a status bar item upon instantiation', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + appShell.verifyAll(); + }); + test('Should be disabled upon instantiation', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + appShell.verifyAll(); + expect(display.enabled).to.be.equal(false, 'not disabled'); + }); + test('Enable display should show the statusbar', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + display.enabled = true; + statusBar.verifyAll(); + }); + test('Disable display should hide the statusbar', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.hide()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + display.enabled = false; + statusBar.verifyAll(); + }); + test('Ensure status bar is displayed and updated with progress with ability to stop tests', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + display.displayProgressStatus(createDeferred().promise, false); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Running Tests'), typeMoq.Times.atLeastOnce()); + }); + test('Ensure status bar is updated with success with ability to view ui without any results', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred(); + + display.displayProgressStatus(def.promise, false); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Running Tests'), typeMoq.Times.atLeastOnce()); + + const tests = typeMoq.Mock.ofType(); + tests.setup((t: any) => t.then).returns(() => undefined); + tests.setup(t => t.summary).returns(() => { + return { errors: 0, failures: 0, passed: 0, skipped: 0 }; + }).verifiable(typeMoq.Times.atLeastOnce()); + + appShell.setup(a => a.showWarningMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.once()); + + def.resolve(tests.object); + await sleep(1); + + tests.verifyAll(); + appShell.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); + }); + test('Ensure status bar is updated with success with ability to view ui with results', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred(); + + display.displayProgressStatus(def.promise, false); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Running Tests'), typeMoq.Times.atLeastOnce()); + + const tests = typeMoq.Mock.ofType(); + tests.setup((t: any) => t.then).returns(() => undefined); + tests.setup(t => t.summary).returns(() => { + return { errors: 0, failures: 0, passed: 1, skipped: 0 }; + }).verifiable(typeMoq.Times.atLeastOnce()); + + appShell.setup(a => a.showWarningMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.never()); + + def.resolve(tests.object); + await sleep(1); + + tests.verifyAll(); + appShell.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); + }); + test('Ensure status bar is updated with error when cancelled by user with ability to view ui with results', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred(); + + display.displayProgressStatus(def.promise, false); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Running Tests'), typeMoq.Times.atLeastOnce()); + + testsHelper.setup(t => t.displayTestErrorMessage(typeMoq.It.isAny())).verifiable(typeMoq.Times.never()); + + def.reject(CANCELLATION_REASON); + await sleep(1); + + appShell.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); + testsHelper.verifyAll(); + }); + test('Ensure status bar is updated, and error message display with error in running tests, with ability to view ui with results', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred(); + + display.displayProgressStatus(def.promise, false); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Running Tests'), typeMoq.Times.atLeastOnce()); + + testsHelper.setup(t => t.displayTestErrorMessage(typeMoq.It.isAny())).verifiable(typeMoq.Times.once()); + + def.reject('Some other reason'); + await sleep(1); + + appShell.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); + testsHelper.verifyAll(); + }); + + test('Ensure status bar is displayed and updated with progress with ability to stop test discovery', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + display.displayDiscoverStatus(createDeferred().promise, false).ignoreErrors(); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Discovering Tests'), typeMoq.Times.atLeastOnce()); + }); + test('Ensure status bar is displayed and updated with success and no tests, with ability to view ui to view results of test discovery', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred(); + + display.displayDiscoverStatus(def.promise, false).ignoreErrors(); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Discovering Tests'), typeMoq.Times.atLeastOnce()); + + const tests = typeMoq.Mock.ofType(); + appShell.setup(a => a.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.once()); + + def.resolve(undefined as any); + await sleep(1); + + tests.verifyAll(); + appShell.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); + }); + test('Ensure tests are disabled when there are errors and user choses to disable tests', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred(); + + display.displayDiscoverStatus(def.promise, false).ignoreErrors(); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Discovering Tests'), typeMoq.Times.atLeastOnce()); + + const tests = typeMoq.Mock.ofType(); + appShell.setup(a => a.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((msg, item) => Promise.resolve(item)) + .verifiable(typeMoq.Times.once()); + + for (const setting of ['unitTest.promptToConfigure', 'unitTest.pyTestEnabled', + 'unitTest.unittestEnabled', 'unitTest.nosetestsEnabled']) { + configurationService.setup(c => c.updateSettingAsync(typeMoq.It.isValue(setting), typeMoq.It.isValue(false))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + } + def.resolve(undefined as any); + await sleep(1); + + tests.verifyAll(); + appShell.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); + configurationService.verifyAll(); + }); + test('Ensure status bar is displayed and updated with error info when test discovery is cancelled by the user', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred(); + + display.displayDiscoverStatus(def.promise, false).ignoreErrors(); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Discovering Tests'), typeMoq.Times.atLeastOnce()); + + appShell.setup(a => a.showErrorMessage(typeMoq.It.isAny())) + .verifiable(typeMoq.Times.never()); + + def.reject(CANCELLATION_REASON); + await sleep(1); + + appShell.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Discover), typeMoq.Times.atLeastOnce()); + configurationService.verifyAll(); + }); + test('Ensure status bar is displayed and updated with error info, and message is displayed when test discovery is fails due to errors', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred(); + + display.displayDiscoverStatus(def.promise, false).ignoreErrors(); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Discovering Tests'), typeMoq.Times.atLeastOnce()); + + appShell.setup(a => a.showErrorMessage(typeMoq.It.isAny())) + .verifiable(typeMoq.Times.once()); + + def.reject('some weird error'); + await sleep(1); + + appShell.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Discover), typeMoq.Times.atLeastOnce()); + configurationService.verifyAll(); + }); +}); diff --git a/src/test/unittests/mocks.ts b/src/test/unittests/mocks.ts index 3e2cdc91d710..1ff69155137f 100644 --- a/src/test/unittests/mocks.ts +++ b/src/test/unittests/mocks.ts @@ -6,7 +6,7 @@ import { Product } from '../../client/common/types'; import { IServiceContainer } from '../../client/ioc/types'; import { CANCELLATION_REASON } from '../../client/unittests/common/constants'; import { BaseTestManager } from '../../client/unittests/common/managers/baseTestManager'; -import { ITestDebugLauncher, ITestDiscoveryService, IUnitTestSocketServer, launchOptions, TestDiscoveryOptions, TestProvider, Tests, TestsToRun } from '../../client/unittests/common/types'; +import { ITestDebugLauncher, ITestDiscoveryService, IUnitTestSocketServer, LaunchOptions, TestDiscoveryOptions, TestProvider, Tests, TestsToRun } from '../../client/unittests/common/types'; @injectable() export class MockDebugLauncher implements ITestDebugLauncher, Disposable { @@ -32,7 +32,7 @@ export class MockDebugLauncher implements ITestDebugLauncher, Disposable { public async getLaunchOptions(resource?: Uri): Promise<{ port: number, host: string }> { return { port: 0, host: 'localhost' }; } - public async launchDebugger(options: launchOptions): Promise { + public async launchDebugger(options: LaunchOptions): Promise { this._launched.resolve(true); // tslint:disable-next-line:no-non-null-assertion this._token = options.token!; diff --git a/src/test/unittests/nosetest/nosetest.discovery.unit.test.ts b/src/test/unittests/nosetest/nosetest.discovery.unit.test.ts new file mode 100644 index 000000000000..4ea9b9d54556 --- /dev/null +++ b/src/test/unittests/nosetest/nosetest.discovery.unit.test.ts @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable-next-line:max-func-body-length + +import { expect, use } from 'chai'; +import * as chaipromise from 'chai-as-promised'; +import * as typeMoq from 'typemoq'; +import { CancellationToken } from 'vscode'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { NOSETEST_PROVIDER } from '../../../client/unittests/common/constants'; +import { ITestDiscoveryService, ITestRunner, ITestsParser, Options, TestDiscoveryOptions, Tests } from '../../../client/unittests/common/types'; +import { TestDiscoveryService } from '../../../client/unittests/nosetest/services/discoveryService'; +import { IArgumentsService, TestFilter } from '../../../client/unittests/types'; + +use(chaipromise); + +suite('Unit Tests - nose - Discovery', () => { + let discoveryService: ITestDiscoveryService; + let argsService: typeMoq.IMock; + let testParser: typeMoq.IMock; + let runner: typeMoq.IMock; + setup(() => { + const serviceContainer = typeMoq.Mock.ofType(); + argsService = typeMoq.Mock.ofType(); + testParser = typeMoq.Mock.ofType(); + runner = typeMoq.Mock.ofType(); + + serviceContainer.setup(s => s.get(typeMoq.It.isValue(IArgumentsService), typeMoq.It.isAny())) + .returns(() => argsService.object); + serviceContainer.setup(s => s.get(typeMoq.It.isValue(ITestRunner), typeMoq.It.isAny())) + .returns(() => runner.object); + + discoveryService = new TestDiscoveryService(serviceContainer.object, testParser.object); + }); + test('Ensure discovery is invoked with the right args', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], testFunctions: [], testSuites: [], + rootTestFolders: [], testFolders: [] + }; + argsService.setup(a => a.filterArguments(typeMoq.It.isValue(args), typeMoq.It.isValue(TestFilter.discovery))) + .returns(() => []) + .verifiable(typeMoq.Times.once()); + runner.setup(r => r.run(typeMoq.It.isValue(NOSETEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('--collect-only'); + expect(opts.args).to.include('-vvv'); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser.setup(t => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + + const options = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + options.setup(o => o.args).returns(() => args); + options.setup(o => o.token).returns(() => token.object); + token.setup(t => t.isCancellationRequested) + .returns(() => false); + + const result = await discoveryService.discoverTests(options.object); + + expect(result).to.be.equal(tests); + argsService.verifyAll(); + runner.verifyAll(); + testParser.verifyAll(); + }); + test('Ensure discovery is cancelled', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], testFunctions: [], testSuites: [], + rootTestFolders: [], testFolders: [] + }; + argsService.setup(a => a.filterArguments(typeMoq.It.isValue(args), typeMoq.It.isValue(TestFilter.discovery))) + .returns(() => []) + .verifiable(typeMoq.Times.once()); + runner.setup(r => r.run(typeMoq.It.isValue(NOSETEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('--collect-only'); + expect(opts.args).to.include('-vvv'); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser.setup(t => t.parse(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.never()); + + const options = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + token.setup(t => t.isCancellationRequested) + .returns(() => true) + .verifiable(typeMoq.Times.once()); + + options.setup(o => o.args).returns(() => args); + options.setup(o => o.token).returns(() => token.object); + const promise = discoveryService.discoverTests(options.object); + + await expect(promise).to.eventually.be.rejectedWith('cancelled'); + argsService.verifyAll(); + runner.verifyAll(); + testParser.verifyAll(); + }); +}); diff --git a/src/test/unittests/nosetest.disovery.test.ts b/src/test/unittests/nosetest/nosetest.disovery.test.ts similarity index 82% rename from src/test/unittests/nosetest.disovery.test.ts rename to src/test/unittests/nosetest/nosetest.disovery.test.ts index d1acbecf149b..5bda99d7c657 100644 --- a/src/test/unittests/nosetest.disovery.test.ts +++ b/src/test/unittests/nosetest/nosetest.disovery.test.ts @@ -5,18 +5,19 @@ import * as assert from 'assert'; import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; -import { IProcessService } from '../../client/common/process/types'; -import { CommandSource } from '../../client/unittests/common/constants'; -import { ITestManagerFactory } from '../../client/unittests/common/types'; -import { rootWorkspaceUri, updateSetting } from '../common'; -import { MockProcessService } from '../mocks/proc'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; -import { lookForTestFile } from './helper'; -import { UnitTestIocContainer } from './serviceRegistry'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { IProcessServiceFactory } from '../../../client/common/process/types'; +import { CommandSource } from '../../../client/unittests/common/constants'; +import { ITestManagerFactory } from '../../../client/unittests/common/types'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { MockProcessService } from '../../mocks/proc'; +import { lookForTestFile } from '../helper'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; -const PYTHON_FILES_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles'); -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 PYTHON_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles'); +const UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'noseFiles'); +const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'single'); const filesToDelete = [ path.join(UNITTEST_TEST_FILES_PATH, '.noseids'), path.join(UNITTEST_SINGLE_TEST_FILE_PATH, '.noseids') @@ -62,8 +63,8 @@ suite('Unit Tests - nose - discovery with mocked process output', () => { ioc.registerMockProcessTypes(); } - function injectTestDiscoveryOutput(outputFileName: string) { - const procService = ioc.serviceContainer.get(IProcessService); + async function injectTestDiscoveryOutput(outputFileName: string) { + const procService = await ioc.serviceContainer.get(IProcessServiceFactory).create() as MockProcessService; procService.onExecObservable((file, args, options, callback) => { if (args.indexOf('--collect-only') >= 0) { let out = fs.readFileSync(path.join(UNITTEST_TEST_FILES_PATH, outputFileName), 'utf8'); @@ -78,7 +79,7 @@ suite('Unit Tests - nose - discovery with mocked process output', () => { } test('Discover Tests (single test file)', async () => { - injectTestDiscoveryOutput('one.output'); + await injectTestDiscoveryOutput('one.output'); const factory = ioc.serviceContainer.get(ITestManagerFactory); const testManager = factory('nosetest', rootWorkspaceUri, UNITTEST_SINGLE_TEST_FILE_PATH); const tests = await testManager.discoverTests(CommandSource.ui, true, true); @@ -89,7 +90,7 @@ suite('Unit Tests - nose - discovery with mocked process output', () => { }); test('Check that nameToRun in testSuites has class name after : (single test file)', async () => { - injectTestDiscoveryOutput('two.output'); + await injectTestDiscoveryOutput('two.output'); const factory = ioc.serviceContainer.get(ITestManagerFactory); const testManager = factory('nosetest', rootWorkspaceUri, UNITTEST_SINGLE_TEST_FILE_PATH); const tests = await testManager.discoverTests(CommandSource.ui, true, true); @@ -99,7 +100,7 @@ suite('Unit Tests - nose - discovery with mocked process output', () => { 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 (-m=test)', async () => { - injectTestDiscoveryOutput('three.output'); + await injectTestDiscoveryOutput('three.output'); await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); const testManager = factory('nosetest', rootWorkspaceUri, UNITTEST_TEST_FILES_PATH); @@ -115,7 +116,7 @@ suite('Unit Tests - nose - discovery with mocked process output', () => { }); test('Discover Tests (-w=specific -m=tst)', async () => { - injectTestDiscoveryOutput('four.output'); + await injectTestDiscoveryOutput('four.output'); await updateSetting('unitTest.nosetestArgs', ['-w', 'specific', '-m', 'tst'], rootWorkspaceUri, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); const testManager = factory('nosetest', rootWorkspaceUri, UNITTEST_TEST_FILES_PATH); @@ -128,7 +129,7 @@ suite('Unit Tests - nose - discovery with mocked process output', () => { }); test('Discover Tests (-m=test_)', async () => { - injectTestDiscoveryOutput('five.output'); + await injectTestDiscoveryOutput('five.output'); await updateSetting('unitTest.nosetestArgs', ['-m', 'test_'], rootWorkspaceUri, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); const testManager = factory('nosetest', rootWorkspaceUri, UNITTEST_TEST_FILES_PATH); diff --git a/src/test/unittests/nosetest.run.test.ts b/src/test/unittests/nosetest/nosetest.run.test.ts similarity index 79% rename from src/test/unittests/nosetest.run.test.ts rename to src/test/unittests/nosetest/nosetest.run.test.ts index 1fc34a684c34..88e2adc63b8e 100644 --- a/src/test/unittests/nosetest.run.test.ts +++ b/src/test/unittests/nosetest/nosetest.run.test.ts @@ -5,16 +5,17 @@ import * as assert from 'assert'; import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; -import { IProcessService } from '../../client/common/process/types'; -import { CommandSource } from '../../client/unittests/common/constants'; -import { ITestManagerFactory, TestsToRun } from '../../client/unittests/common/types'; -import { rootWorkspaceUri, updateSetting } from '../common'; -import { MockProcessService } from '../mocks/proc'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; -import { UnitTestIocContainer } from './serviceRegistry'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { IProcessServiceFactory } from '../../../client/common/process/types'; +import { CommandSource } from '../../../client/unittests/common/constants'; +import { ITestManagerFactory, TestsToRun } from '../../../client/unittests/common/types'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { MockProcessService } from '../../mocks/proc'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; -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 UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'noseFiles'); +const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'single'); const filesToDelete = [ path.join(UNITTEST_TEST_FILES_PATH, '.noseids'), path.join(UNITTEST_SINGLE_TEST_FILE_PATH, '.noseids') @@ -60,20 +61,20 @@ suite('Unit Tests - nose - run against actual python process', () => { ioc.registerMockProcessTypes(); } - function injectTestDiscoveryOutput(outputFileName: string) { - const procService = ioc.serviceContainer.get(IProcessService); + async function injectTestDiscoveryOutput(outputFileName: string) { + const procService = await ioc.serviceContainer.get(IProcessServiceFactory).create() as MockProcessService; procService.onExecObservable((file, args, options, callback) => { if (args.indexOf('--collect-only') >= 0) { callback({ - out: fs.readFileSync(path.join(UNITTEST_TEST_FILES_PATH, outputFileName), 'utf8'), + out: fs.readFileSync(path.join(UNITTEST_TEST_FILES_PATH, outputFileName), 'utf8').replace(/\/Users\/donjayamanne\/.vscode\/extensions\/pythonVSCode\/src\/test\/pythonFiles\/testFiles\/noseFiles/g, UNITTEST_TEST_FILES_PATH), source: 'stdout' }); } }); } - function injectTestRunOutput(outputFileName: string, failedOutput: boolean = false) { - const procService = ioc.serviceContainer.get(IProcessService); + async function injectTestRunOutput(outputFileName: string, failedOutput: boolean = false) { + const procService = await ioc.serviceContainer.get(IProcessServiceFactory).create() as MockProcessService; procService.onExecObservable((file, args, options, callback) => { if (failedOutput && args.indexOf('--failed') === -1) { return; @@ -90,8 +91,8 @@ suite('Unit Tests - nose - run against actual python process', () => { } test('Run Tests', async () => { - injectTestDiscoveryOutput('run.one.output'); - injectTestRunOutput('run.one.result'); + await injectTestDiscoveryOutput('run.one.output'); + await injectTestRunOutput('run.one.result'); await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); const testManager = factory('nosetest', rootWorkspaceUri, UNITTEST_TEST_FILES_PATH); @@ -103,9 +104,9 @@ suite('Unit Tests - nose - run against actual python process', () => { }); test('Run Failed Tests', async () => { - injectTestDiscoveryOutput('run.two.output'); - injectTestRunOutput('run.two.result'); - injectTestRunOutput('run.two.again.result', true); + await injectTestDiscoveryOutput('run.two.output'); + await injectTestRunOutput('run.two.result'); + await injectTestRunOutput('run.two.again.result', true); await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); const testManager = factory('nosetest', rootWorkspaceUri, UNITTEST_TEST_FILES_PATH); @@ -123,8 +124,8 @@ suite('Unit Tests - nose - run against actual python process', () => { }); test('Run Specific Test File', async () => { - injectTestDiscoveryOutput('run.three.output'); - injectTestRunOutput('run.three.result'); + await injectTestDiscoveryOutput('run.three.output'); + await injectTestRunOutput('run.three.result'); await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); const testManager = factory('nosetest', rootWorkspaceUri, UNITTEST_TEST_FILES_PATH); @@ -141,8 +142,8 @@ suite('Unit Tests - nose - run against actual python process', () => { }); test('Run Specific Test Suite', async () => { - injectTestDiscoveryOutput('run.four.output'); - injectTestRunOutput('run.four.result'); + await injectTestDiscoveryOutput('run.four.output'); + await injectTestRunOutput('run.four.result'); await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); const testManager = factory('nosetest', rootWorkspaceUri, UNITTEST_TEST_FILES_PATH); @@ -159,8 +160,8 @@ suite('Unit Tests - nose - run against actual python process', () => { }); test('Run Specific Test Function', async () => { - injectTestDiscoveryOutput('run.five.output'); - injectTestRunOutput('run.five.result'); + await injectTestDiscoveryOutput('run.five.output'); + await injectTestRunOutput('run.five.result'); await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); const testManager = factory('nosetest', rootWorkspaceUri, UNITTEST_TEST_FILES_PATH); diff --git a/src/test/unittests/nosetest.test.ts b/src/test/unittests/nosetest/nosetest.test.ts similarity index 77% rename from src/test/unittests/nosetest.test.ts rename to src/test/unittests/nosetest/nosetest.test.ts index 4ef31a0b1266..2f3581a0b682 100644 --- a/src/test/unittests/nosetest.test.ts +++ b/src/test/unittests/nosetest/nosetest.test.ts @@ -2,15 +2,16 @@ import * as assert from 'assert'; import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; -import { CommandSource } from '../../client/unittests/common/constants'; -import { ITestManagerFactory } from '../../client/unittests/common/types'; -import { rootWorkspaceUri, updateSetting } from '../common'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; -import { lookForTestFile } from './helper'; -import { UnitTestIocContainer } from './serviceRegistry'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { CommandSource } from '../../../client/unittests/common/constants'; +import { ITestManagerFactory } from '../../../client/unittests/common/types'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { lookForTestFile } from '../helper'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; -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 UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'noseFiles'); +const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'single'); const filesToDelete = [ path.join(UNITTEST_TEST_FILES_PATH, '.noseids'), path.join(UNITTEST_SINGLE_TEST_FILE_PATH, '.noseids') diff --git a/src/test/unittests/pytest.discovery.test.ts b/src/test/unittests/pytest/pytest.discovery.test.ts similarity index 90% rename from src/test/unittests/pytest.discovery.test.ts rename to src/test/unittests/pytest/pytest.discovery.test.ts index 9f073f8440bc..d2e0ef7ca93b 100644 --- a/src/test/unittests/pytest.discovery.test.ts +++ b/src/test/unittests/pytest/pytest.discovery.test.ts @@ -4,18 +4,19 @@ import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; -import { IProcessService } from '../../client/common/process/types'; -import { CommandSource } from '../../client/unittests/common/constants'; -import { ITestManagerFactory } from '../../client/unittests/common/types'; -import { rootWorkspaceUri, updateSetting } from '../common'; -import { MockProcessService } from '../mocks/proc'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; -import { UnitTestIocContainer } from './serviceRegistry'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { IProcessServiceFactory } from '../../../client/common/process/types'; +import { CommandSource } from '../../../client/unittests/common/constants'; +import { ITestManagerFactory } from '../../../client/unittests/common/types'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { MockProcessService } from '../../mocks/proc'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; -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'); +const UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); +const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'single'); +const UNITTEST_TEST_FILES_PATH_WITH_CONFIGS = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'unitestsWithConfigs'); +const unitTestTestFilesCwdPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'cwd', 'src'); // tslint:disable-next-line:max-func-body-length suite('Unit Tests - pytest - discovery with mocked process output', () => { @@ -44,8 +45,8 @@ suite('Unit Tests - pytest - discovery with mocked process output', () => { ioc.registerMockProcessTypes(); } - function injectTestDiscoveryOutput(output: string) { - const procService = ioc.serviceContainer.get(IProcessService); + async function injectTestDiscoveryOutput(output: string) { + const procService = await ioc.serviceContainer.get(IProcessServiceFactory).create() as MockProcessService; procService.onExecObservable((file, args, options, callback) => { if (args.indexOf('--collect-only') >= 0) { callback({ @@ -58,7 +59,7 @@ suite('Unit Tests - pytest - discovery with mocked process output', () => { test('Discover Tests (single test file)', async () => { // tslint:disable-next-line:no-multiline-string - injectTestDiscoveryOutput(` + await injectTestDiscoveryOutput(` ============================= test session starts ============================== platform darwin -- Python 3.6.2, pytest-3.3.0, py-1.5.2, pluggy-0.6.0 rootdir: /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single, inifile: @@ -89,7 +90,7 @@ suite('Unit Tests - pytest - discovery with mocked process output', () => { test('Discover Tests (pattern = test_)', async () => { // tslint:disable-next-line:no-multiline-string - injectTestDiscoveryOutput(` + await injectTestDiscoveryOutput(` ============================= test session starts ============================== platform darwin -- Python 3.6.2, pytest-3.3.0, py-1.5.2, pluggy-0.6.0 rootdir: /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard, inifile: @@ -172,7 +173,7 @@ suite('Unit Tests - pytest - discovery with mocked process output', () => { test('Discover Tests (pattern = _test)', async () => { // tslint:disable-next-line:no-multiline-string - injectTestDiscoveryOutput(` + await injectTestDiscoveryOutput(` ============================= test session starts ============================== platform darwin -- Python 3.6.2, pytest-3.3.0, py-1.5.2, pluggy-0.6.0 rootdir: /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard, inifile: @@ -198,7 +199,7 @@ suite('Unit Tests - pytest - discovery with mocked process output', () => { test('Discover Tests (with config)', async () => { // tslint:disable-next-line:no-multiline-string - injectTestDiscoveryOutput(` + await injectTestDiscoveryOutput(` ============================= test session starts ============================== platform darwin -- Python 3.6.2, pytest-3.3.0, py-1.5.2, pluggy-0.6.0 rootdir: /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/unitestsWithConfigs, inifile: pytest.ini @@ -243,7 +244,7 @@ suite('Unit Tests - pytest - discovery with mocked process output', () => { test('Setting cwd should return tests', async () => { // tslint:disable-next-line:no-multiline-string - injectTestDiscoveryOutput(` + await injectTestDiscoveryOutput(` ============================= test session starts ============================== platform darwin -- Python 3.6.2, pytest-3.3.0, py-1.5.2, pluggy-0.6.0 rootdir: /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/cwd/src, inifile: diff --git a/src/test/unittests/pytest/pytest.discovery.unit.test.ts b/src/test/unittests/pytest/pytest.discovery.unit.test.ts new file mode 100644 index 000000000000..a7255308739f --- /dev/null +++ b/src/test/unittests/pytest/pytest.discovery.unit.test.ts @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { expect, use } from 'chai'; +import * as chaipromise from 'chai-as-promised'; +import * as path from 'path'; +import * as typeMoq from 'typemoq'; +import { CancellationToken } from 'vscode'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { PYTEST_PROVIDER } from '../../../client/unittests/common/constants'; +import { ITestDiscoveryService, ITestRunner, ITestsHelper, ITestsParser, Options, TestDiscoveryOptions, Tests } from '../../../client/unittests/common/types'; +import { TestDiscoveryService } from '../../../client/unittests/pytest/services/discoveryService'; +import { IArgumentsService, TestFilter } from '../../../client/unittests/types'; + +use(chaipromise); + +suite('Unit Tests - PyTest - Discovery', () => { + let discoveryService: ITestDiscoveryService; + let argsService: typeMoq.IMock; + let testParser: typeMoq.IMock; + let runner: typeMoq.IMock; + let helper: typeMoq.IMock; + setup(() => { + const serviceContainer = typeMoq.Mock.ofType(); + argsService = typeMoq.Mock.ofType(); + testParser = typeMoq.Mock.ofType(); + runner = typeMoq.Mock.ofType(); + helper = typeMoq.Mock.ofType(); + + serviceContainer.setup(s => s.get(typeMoq.It.isValue(IArgumentsService), typeMoq.It.isAny())) + .returns(() => argsService.object); + serviceContainer.setup(s => s.get(typeMoq.It.isValue(ITestRunner), typeMoq.It.isAny())) + .returns(() => runner.object); + serviceContainer.setup(s => s.get(typeMoq.It.isValue(ITestsHelper), typeMoq.It.isAny())) + .returns(() => helper.object); + + discoveryService = new TestDiscoveryService(serviceContainer.object, testParser.object); + }); + test('Ensure discovery is invoked with the right args and single dir', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const dir = path.join('a', 'b', 'c'); + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], testFunctions: [], testSuites: [], + rootTestFolders: [], testFolders: [] + }; + argsService.setup(a => a.filterArguments(typeMoq.It.isValue(args), typeMoq.It.isValue(TestFilter.discovery))) + .returns(() => []) + .verifiable(typeMoq.Times.once()); + argsService.setup(a => a.getTestFolders(typeMoq.It.isValue(args))) + .returns(() => [dir]) + .verifiable(typeMoq.Times.once()); + helper.setup(a => a.mergeTests(typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + runner.setup(r => r.run(typeMoq.It.isValue(PYTEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('--cache-clear'); + expect(opts.args).to.include('-s'); + expect(opts.args).to.include('--collect-only'); + expect(opts.args[opts.args.length - 1]).to.equal(dir); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser.setup(t => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + + const options = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + options.setup(o => o.args).returns(() => args); + options.setup(o => o.token).returns(() => token.object); + token.setup(t => t.isCancellationRequested) + .returns(() => false); + + const result = await discoveryService.discoverTests(options.object); + + expect(result).to.be.equal(tests); + argsService.verifyAll(); + runner.verifyAll(); + testParser.verifyAll(); + helper.verifyAll(); + }); + test('Ensure discovery is invoked with the right args and multiple dirs', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const dirs = [path.join('a', 'b', '1'), path.join('a', 'b', '2')]; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], testFunctions: [], testSuites: [], + rootTestFolders: [], testFolders: [] + }; + argsService.setup(a => a.filterArguments(typeMoq.It.isValue(args), typeMoq.It.isValue(TestFilter.discovery))) + .returns(() => []) + .verifiable(typeMoq.Times.once()); + argsService.setup(a => a.getTestFolders(typeMoq.It.isValue(args))) + .returns(() => dirs) + .verifiable(typeMoq.Times.once()); + helper.setup(a => a.mergeTests(typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + runner.setup(r => r.run(typeMoq.It.isValue(PYTEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('--cache-clear'); + expect(opts.args).to.include('-s'); + expect(opts.args).to.include('--collect-only'); + const dir = opts.args[opts.args.length - 1]; + expect(dirs).to.include(dir); + dirs.splice(dirs.indexOf(dir) - 1, 1); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser.setup(t => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + + const options = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + options.setup(o => o.args).returns(() => args); + options.setup(o => o.token).returns(() => token.object); + token.setup(t => t.isCancellationRequested) + .returns(() => false); + + const result = await discoveryService.discoverTests(options.object); + + expect(result).to.be.equal(tests); + argsService.verifyAll(); + runner.verifyAll(); + testParser.verifyAll(); + helper.verifyAll(); + }); + test('Ensure discovery is cancelled', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], testFunctions: [], testSuites: [], + rootTestFolders: [], testFolders: [] + }; + argsService.setup(a => a.filterArguments(typeMoq.It.isValue(args), typeMoq.It.isValue(TestFilter.discovery))) + .returns(() => []) + .verifiable(typeMoq.Times.once()); + argsService.setup(a => a.getTestFolders(typeMoq.It.isValue(args))) + .returns(() => ['']) + .verifiable(typeMoq.Times.once()); + runner.setup(r => r.run(typeMoq.It.isValue(PYTEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('--cache-clear'); + expect(opts.args).to.include('-s'); + expect(opts.args).to.include('--collect-only'); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser.setup(t => t.parse(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.never()); + helper.setup(a => a.mergeTests(typeMoq.It.isAny())) + .returns(() => tests); + + const options = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + token.setup(t => t.isCancellationRequested) + .returns(() => true) + .verifiable(typeMoq.Times.once()); + + options.setup(o => o.args).returns(() => args); + options.setup(o => o.token).returns(() => token.object); + const promise = discoveryService.discoverTests(options.object); + + await expect(promise).to.eventually.be.rejectedWith('cancelled'); + argsService.verifyAll(); + runner.verifyAll(); + testParser.verifyAll(); + }); +}); diff --git a/src/test/unittests/pytest.run.test.ts b/src/test/unittests/pytest/pytest.run.test.ts similarity index 78% rename from src/test/unittests/pytest.run.test.ts rename to src/test/unittests/pytest/pytest.run.test.ts index b6695db63311..09941039ed74 100644 --- a/src/test/unittests/pytest.run.test.ts +++ b/src/test/unittests/pytest/pytest.run.test.ts @@ -5,16 +5,17 @@ import * as assert from 'assert'; import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; -import { IProcessService } from '../../client/common/process/types'; -import { CommandSource } from '../../client/unittests/common/constants'; -import { ITestManagerFactory, TestFile, TestsToRun } from '../../client/unittests/common/types'; -import { rootWorkspaceUri, updateSetting } from '../common'; -import { MockProcessService } from '../mocks/proc'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; -import { UnitTestIocContainer } from './serviceRegistry'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { IProcessServiceFactory } from '../../../client/common/process/types'; +import { CommandSource } from '../../../client/unittests/common/constants'; +import { ITestManagerFactory, TestFile, TestsToRun } from '../../../client/unittests/common/types'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { MockProcessService } from '../../mocks/proc'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; -const UNITTEST_TEST_FILES_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); -const PYTEST_RESULTS_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'pytestFiles', 'results'); +const UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); +const PYTEST_RESULTS_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'pytestFiles', 'results'); // tslint:disable-next-line:max-func-body-length suite('Unit Tests - pytest - run with mocked process output', () => { @@ -43,20 +44,20 @@ suite('Unit Tests - pytest - run with mocked process output', () => { ioc.registerMockProcessTypes(); } - function injectTestDiscoveryOutput(outputFileName: string) { - const procService = ioc.serviceContainer.get(IProcessService); + async function injectTestDiscoveryOutput(outputFileName: string) { + const procService = await ioc.serviceContainer.get(IProcessServiceFactory).create() as MockProcessService; procService.onExecObservable((file, args, options, callback) => { if (args.indexOf('--collect-only') >= 0) { callback({ - out: fs.readFileSync(path.join(PYTEST_RESULTS_PATH, outputFileName), 'utf8'), + out: fs.readFileSync(path.join(PYTEST_RESULTS_PATH, outputFileName), 'utf8').replace(/\/Users\/donjayamanne\/.vscode\/extensions\/pythonVSCode\/src\/test\/pythonFiles\/testFiles\/noseFiles/g, PYTEST_RESULTS_PATH), source: 'stdout' }); } }); } - function injectTestRunOutput(outputFileName: string, failedOutput: boolean = false) { - const procService = ioc.serviceContainer.get(IProcessService); + async function injectTestRunOutput(outputFileName: string, failedOutput: boolean = false) { + const procService = await ioc.serviceContainer.get(IProcessServiceFactory).create() as MockProcessService; procService.onExecObservable((file, args, options, callback) => { if (failedOutput && args.indexOf('--last-failed') === -1) { return; @@ -73,8 +74,8 @@ suite('Unit Tests - pytest - run with mocked process output', () => { } test('Run Tests', async () => { - injectTestDiscoveryOutput('one.output'); - injectTestRunOutput('one.xml'); + await injectTestDiscoveryOutput('one.output'); + await injectTestRunOutput('one.xml'); await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); const testManager = factory('pytest', rootWorkspaceUri, UNITTEST_TEST_FILES_PATH); @@ -86,9 +87,9 @@ suite('Unit Tests - pytest - run with mocked process output', () => { }); test('Run Failed Tests', async () => { - injectTestDiscoveryOutput('two.output'); - injectTestRunOutput('two.xml'); - injectTestRunOutput('two.again.xml', true); + await injectTestDiscoveryOutput('two.output'); + await injectTestRunOutput('two.xml'); + await injectTestRunOutput('two.again.xml', true); await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); const testManager = factory('pytest', rootWorkspaceUri, UNITTEST_TEST_FILES_PATH); @@ -106,8 +107,8 @@ suite('Unit Tests - pytest - run with mocked process output', () => { }); test('Run Specific Test File', async () => { - injectTestDiscoveryOutput('three.output'); - injectTestRunOutput('three.xml'); + await injectTestDiscoveryOutput('three.output'); + await injectTestRunOutput('three.xml'); await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); const testManager = factory('pytest', rootWorkspaceUri, UNITTEST_TEST_FILES_PATH); @@ -130,8 +131,8 @@ suite('Unit Tests - pytest - run with mocked process output', () => { }); test('Run Specific Test Suite', async () => { - injectTestDiscoveryOutput('four.output'); - injectTestRunOutput('four.xml'); + await injectTestDiscoveryOutput('four.output'); + await injectTestRunOutput('four.xml'); await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); const testManager = factory('pytest', rootWorkspaceUri, UNITTEST_TEST_FILES_PATH); @@ -145,8 +146,8 @@ suite('Unit Tests - pytest - run with mocked process output', () => { }); test('Run Specific Test Function', async () => { - injectTestDiscoveryOutput('five.output'); - injectTestRunOutput('five.xml'); + await injectTestDiscoveryOutput('five.output'); + await injectTestRunOutput('five.xml'); await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); const testManager = factory('pytest', rootWorkspaceUri, UNITTEST_TEST_FILES_PATH); diff --git a/src/test/unittests/pytest.test.ts b/src/test/unittests/pytest/pytest.test.ts similarity index 79% rename from src/test/unittests/pytest.test.ts rename to src/test/unittests/pytest/pytest.test.ts index d8cea72441d5..210c3c5d8f84 100644 --- a/src/test/unittests/pytest.test.ts +++ b/src/test/unittests/pytest/pytest.test.ts @@ -1,13 +1,14 @@ import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; -import { CommandSource } from '../../client/unittests/common/constants'; -import { ITestManagerFactory } from '../../client/unittests/common/types'; -import { rootWorkspaceUri, updateSetting } from '../common'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; -import { UnitTestIocContainer } from './serviceRegistry'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { CommandSource } from '../../../client/unittests/common/constants'; +import { ITestManagerFactory } from '../../../client/unittests/common/types'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; -const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'single'); +const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'single'); // tslint:disable-next-line:max-func-body-length suite('Unit Tests - pytest - discovery against actual python process', () => { diff --git a/src/test/unittests/serviceRegistry.ts b/src/test/unittests/serviceRegistry.ts index 725dba422e81..f73fbecad906 100644 --- a/src/test/unittests/serviceRegistry.ts +++ b/src/test/unittests/serviceRegistry.ts @@ -12,7 +12,10 @@ import { TestsHelper } from '../../client/unittests/common/testUtils'; import { TestFlatteningVisitor } from '../../client/unittests/common/testVisitors/flatteningVisitor'; import { TestFolderGenerationVisitor } from '../../client/unittests/common/testVisitors/folderGenerationVisitor'; import { TestResultResetVisitor } from '../../client/unittests/common/testVisitors/resultResetVisitor'; -import { ITestResultsService, ITestsHelper, ITestsParser, ITestVisitor, IUnitTestSocketServer, TestProvider } from '../../client/unittests/common/types'; +import { ITestResultsService, ITestsHelper, ITestsParser, + ITestVisitor, IUnitTestSocketServer, + PythonVersionInformation, TestProvider } from '../../client/unittests/common/types'; +// tslint:disable-next-line:no-duplicate-imports import { ITestCollectionStorageService, ITestDiscoveryService, ITestManager, ITestManagerFactory, ITestManagerService, ITestManagerServiceFactory } from '../../client/unittests/common/types'; import { TestManager as NoseTestManager } from '../../client/unittests/nosetest/main'; import { TestDiscoveryService as NoseTestDiscoveryService } from '../../client/unittests/nosetest/services/discoveryService'; @@ -31,10 +34,29 @@ export class UnitTestIocContainer extends IocContainer { super(); } public getPythonMajorVersion(resource: Uri) { - return this.serviceContainer.get(IPythonExecutionFactory).create(resource) + return this.serviceContainer.get(IPythonExecutionFactory).create({ resource }) .then(pythonProcess => pythonProcess.exec(['-c', 'import sys;print(sys.version_info[0])'], {})) .then(output => parseInt(output.stdout.trim(), 10)); } + + public getPythonMajorMinorVersionString(resource: Uri): Promise { + return this.serviceContainer.get(IPythonExecutionFactory).create({ resource }) + .then(pythonProcess => pythonProcess.exec(['-c', 'import sys;print("{0}.{1}".format(*sys.version_info[:2]))'], {})) + .then(output => output.stdout.trim()); + } + + public getPythonMajorMinorVersion(resource: Uri): Promise { + return this.serviceContainer.get(IPythonExecutionFactory).create({ resource }) + .then(pythonProcess => pythonProcess.exec(['-c', 'import sys;print("{0}|{1}".format(*sys.version_info[:2]))'], {})) + .then(output => { + const versionString: string = output.stdout.trim(); + const versionInfo: string[] = versionString.split('|'); + return { + major: parseInt(versionInfo[0].trim(), 10), + minor: parseInt(versionInfo[1].trim(), 10) + }; + }); + } public registerTestVisitors() { this.serviceManager.add(ITestVisitor, TestFlatteningVisitor, 'TestFlatteningVisitor'); this.serviceManager.add(ITestVisitor, TestFolderGenerationVisitor, 'TestFolderGenerationVisitor'); diff --git a/src/test/unittests/unittest.discovery.test.ts b/src/test/unittests/unittest/unittest.discovery.test.ts similarity index 82% rename from src/test/unittests/unittest.discovery.test.ts rename to src/test/unittests/unittest/unittest.discovery.test.ts index b1f95c6ed978..12427c955f5a 100644 --- a/src/test/unittests/unittest.discovery.test.ts +++ b/src/test/unittests/unittest/unittest.discovery.test.ts @@ -6,15 +6,16 @@ import * as fs from 'fs-extra'; import { EOL } from 'os'; import * as path from 'path'; import { ConfigurationTarget } from 'vscode'; -import { IProcessService } from '../../client/common/process/types'; -import { CommandSource } from '../../client/unittests/common/constants'; -import { ITestManagerFactory } from '../../client/unittests/common/types'; -import { rootWorkspaceUri, updateSetting } from '../common'; -import { MockProcessService } from '../mocks/proc'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; -import { UnitTestIocContainer } from './serviceRegistry'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { IProcessServiceFactory } from '../../../client/common/process/types'; +import { CommandSource } from '../../../client/unittests/common/constants'; +import { ITestManagerFactory } from '../../../client/unittests/common/types'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { MockProcessService } from '../../mocks/proc'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; -const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles'); +const testFilesPath = path.join(EXTENSION_ROOT_DIR, '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'); @@ -59,8 +60,8 @@ suite('Unit Tests - unittest - discovery with mocked process output', () => { ioc.registerMockProcessTypes(); } - function injectTestDiscoveryOutput(output: string) { - const procService = ioc.serviceContainer.get(IProcessService); + async function injectTestDiscoveryOutput(output: string) { + const procService = await ioc.serviceContainer.get(IProcessServiceFactory).create() as MockProcessService; procService.onExecObservable((file, args, options, callback) => { if (args.length > 1 && args[0] === '-c' && args[1].includes('import unittest') && args[1].includes('loader = unittest.TestLoader()')) { callback({ @@ -75,7 +76,7 @@ suite('Unit Tests - unittest - discovery with mocked process output', () => { test('Discover Tests (single test file)', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); // tslint:disable-next-line:no-multiline-string - injectTestDiscoveryOutput(`start + await injectTestDiscoveryOutput(`start test_one.Test_test1.test_A test_one.Test_test1.test_B test_one.Test_test1.test_c @@ -86,13 +87,13 @@ suite('Unit Tests - unittest - discovery with mocked process output', () => { 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'); - assert.equal(tests.testFiles.some(t => t.name === 'test_one.py' && t.nameToRun === 'Test_test1.test_A'), true, 'Test File not found'); + assert.equal(tests.testFiles.some(t => t.name === 'test_one.py' && t.nameToRun === 'test_one.Test_test1.test_A'), true, 'Test File not found'); }); test('Discover Tests', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); // tslint:disable-next-line:no-multiline-string - injectTestDiscoveryOutput(`start + await injectTestDiscoveryOutput(`start test_unittest_one.Test_test1.test_A test_unittest_one.Test_test1.test_B test_unittest_one.Test_test1.test_c @@ -109,14 +110,14 @@ suite('Unit Tests - unittest - discovery with mocked process output', () => { 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.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'); + assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_one.py' && t.nameToRun === 'test_unittest_one.Test_test1.test_A'), true, 'Test File not found'); + assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_two.py' && t.nameToRun === 'test_unittest_two.Test_test2.test_A2'), true, 'Test File not found'); }); test('Discover Tests (pattern = *_test_*.py)', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=*_test*.py'], rootWorkspaceUri, configTarget); // tslint:disable-next-line:no-multiline-string - injectTestDiscoveryOutput(`start + await injectTestDiscoveryOutput(`start unittest_three_test.Test_test3.test_A unittest_three_test.Test_test3.test_B `); @@ -126,13 +127,13 @@ suite('Unit Tests - unittest - discovery with mocked process output', () => { 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.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'); + assert.equal(tests.testFiles.some(t => t.name === 'unittest_three_test.py' && t.nameToRun === 'unittest_three_test.Test_test3.test_A'), true, 'Test File not found'); }); test('Setting cwd should return tests', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); // tslint:disable-next-line:no-multiline-string - injectTestDiscoveryOutput(`start + await injectTestDiscoveryOutput(`start test_cwd.Test_Current_Working_Directory.test_cwd `); const factory = ioc.serviceContainer.get(ITestManagerFactory); diff --git a/src/test/unittests/unittest/unittest.discovery.unit.test.ts b/src/test/unittests/unittest/unittest.discovery.unit.test.ts new file mode 100644 index 000000000000..fe54b33f7b1b --- /dev/null +++ b/src/test/unittests/unittest/unittest.discovery.unit.test.ts @@ -0,0 +1,550 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { expect, use } from 'chai'; +import * as chaipromise from 'chai-as-promised'; +import * as path from 'path'; +import * as typeMoq from 'typemoq'; +import { CancellationToken, Uri } from 'vscode'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { UNITTEST_PROVIDER } from '../../../client/unittests/common/constants'; +import { TestsHelper } from '../../../client/unittests/common/testUtils'; +import { TestFlatteningVisitor } from '../../../client/unittests/common/testVisitors/flatteningVisitor'; +import { ITestDiscoveryService, ITestRunner, ITestsParser, + Options, TestDiscoveryOptions, Tests, UnitTestParserOptions } from '../../../client/unittests/common/types'; +import { IArgumentsHelper } from '../../../client/unittests/types'; +import { TestDiscoveryService } from '../../../client/unittests/unittest/services/discoveryService'; +import { TestsParser } from '../../../client/unittests/unittest/services/parserService'; + +use(chaipromise); + +suite('Unit Tests - Unittest - Discovery', () => { + let discoveryService: ITestDiscoveryService; + let argsHelper: typeMoq.IMock; + let testParser: typeMoq.IMock; + let runner: typeMoq.IMock; + let serviceContainer: typeMoq.IMock; + const dir = path.join('a', 'b', 'c'); + const pattern = 'Pattern_To_Search_For'; + setup(() => { + serviceContainer = typeMoq.Mock.ofType(); + argsHelper = typeMoq.Mock.ofType(); + testParser = typeMoq.Mock.ofType(); + runner = typeMoq.Mock.ofType(); + + serviceContainer.setup(s => s.get(typeMoq.It.isValue(IArgumentsHelper), typeMoq.It.isAny())) + .returns(() => argsHelper.object); + serviceContainer.setup(s => s.get(typeMoq.It.isValue(ITestRunner), typeMoq.It.isAny())) + .returns(() => runner.object); + + discoveryService = new TestDiscoveryService(serviceContainer.object, testParser.object); + }); + test('Ensure discovery is invoked with the right args with start directory defined with -s', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], testFunctions: [], testSuites: [], + rootTestFolders: [], testFolders: [] + }; + argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('-s'))) + .returns(() => dir) + .verifiable(typeMoq.Times.once()); + runner.setup(r => r.run(typeMoq.It.isValue(UNITTEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('-c'); + expect(opts.args[1]).to.contain(dir); + expect(opts.args[1]).to.not.contain('loader.discover("."'); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser.setup(t => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + + const options = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + options.setup(o => o.args).returns(() => args); + options.setup(o => o.token).returns(() => token.object); + token.setup(t => t.isCancellationRequested) + .returns(() => false); + + const result = await discoveryService.discoverTests(options.object); + + expect(result).to.be.equal(tests); + runner.verifyAll(); + testParser.verifyAll(); + }); + test('Ensure discovery is invoked with the right args with start directory defined with --start-directory', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], testFunctions: [], testSuites: [], + rootTestFolders: [], testFolders: [] + }; + argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('-s'))) + .returns(() => undefined) + .verifiable(typeMoq.Times.once()); + argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('--start-directory'))) + .returns(() => dir) + .verifiable(typeMoq.Times.once()); + runner.setup(r => r.run(typeMoq.It.isValue(UNITTEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('-c'); + expect(opts.args[1]).to.contain(dir); + expect(opts.args[1]).to.not.contain('loader.discover("."'); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser.setup(t => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + + const options = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + options.setup(o => o.args).returns(() => args); + options.setup(o => o.token).returns(() => token.object); + token.setup(t => t.isCancellationRequested) + .returns(() => false); + + const result = await discoveryService.discoverTests(options.object); + + expect(result).to.be.equal(tests); + runner.verifyAll(); + testParser.verifyAll(); + }); + test('Ensure discovery is invoked with the right args without a start directory', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], testFunctions: [], testSuites: [], + rootTestFolders: [], testFolders: [] + }; + argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('-s'))) + .returns(() => undefined) + .verifiable(typeMoq.Times.once()); + argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('--start-directory'))) + .returns(() => undefined) + .verifiable(typeMoq.Times.once()); + runner.setup(r => r.run(typeMoq.It.isValue(UNITTEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('-c'); + expect(opts.args[1]).to.not.contain(dir); + expect(opts.args[1]).to.contain('loader.discover("."'); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser.setup(t => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + + const options = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + options.setup(o => o.args).returns(() => args); + options.setup(o => o.token).returns(() => token.object); + token.setup(t => t.isCancellationRequested) + .returns(() => false); + + const result = await discoveryService.discoverTests(options.object); + + expect(result).to.be.equal(tests); + runner.verifyAll(); + testParser.verifyAll(); + }); + test('Ensure discovery is invoked with the right args without a pattern defined with -p', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], testFunctions: [], testSuites: [], + rootTestFolders: [], testFolders: [] + }; + argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('-p'))) + .returns(() => pattern) + .verifiable(typeMoq.Times.once()); + runner.setup(r => r.run(typeMoq.It.isValue(UNITTEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('-c'); + expect(opts.args[1]).to.contain(pattern); + expect(opts.args[1]).to.not.contain('test*.py'); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser.setup(t => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + + const options = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + options.setup(o => o.args).returns(() => args); + options.setup(o => o.token).returns(() => token.object); + token.setup(t => t.isCancellationRequested) + .returns(() => false); + + const result = await discoveryService.discoverTests(options.object); + + expect(result).to.be.equal(tests); + runner.verifyAll(); + testParser.verifyAll(); + }); + test('Ensure discovery is invoked with the right args without a pattern defined with ---pattern', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], testFunctions: [], testSuites: [], + rootTestFolders: [], testFolders: [] + }; + argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('-p'))) + .returns(() => undefined) + .verifiable(typeMoq.Times.once()); + argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('--pattern'))) + .returns(() => pattern) + .verifiable(typeMoq.Times.once()); + runner.setup(r => r.run(typeMoq.It.isValue(UNITTEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('-c'); + expect(opts.args[1]).to.contain(pattern); + expect(opts.args[1]).to.not.contain('test*.py'); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser.setup(t => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + + const options = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + options.setup(o => o.args).returns(() => args); + options.setup(o => o.token).returns(() => token.object); + token.setup(t => t.isCancellationRequested) + .returns(() => false); + + const result = await discoveryService.discoverTests(options.object); + + expect(result).to.be.equal(tests); + runner.verifyAll(); + testParser.verifyAll(); + }); + test('Ensure discovery is invoked with the right args without a pattern not defined', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], testFunctions: [], testSuites: [], + rootTestFolders: [], testFolders: [] + }; + argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('-p'))) + .returns(() => undefined) + .verifiable(typeMoq.Times.once()); + argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('--pattern'))) + .returns(() => undefined) + .verifiable(typeMoq.Times.once()); + runner.setup(r => r.run(typeMoq.It.isValue(UNITTEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('-c'); + expect(opts.args[1]).to.not.contain(pattern); + expect(opts.args[1]).to.contain('test*.py'); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser.setup(t => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + + const options = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + options.setup(o => o.args).returns(() => args); + options.setup(o => o.token).returns(() => token.object); + token.setup(t => t.isCancellationRequested) + .returns(() => false); + + const result = await discoveryService.discoverTests(options.object); + + expect(result).to.be.equal(tests); + runner.verifyAll(); + testParser.verifyAll(); + }); + test('Ensure discovery is cancelled', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], testFunctions: [], testSuites: [], + rootTestFolders: [], testFolders: [] + }; + argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('-p'))) + .returns(() => undefined) + .verifiable(typeMoq.Times.once()); + argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('--pattern'))) + .returns(() => undefined) + .verifiable(typeMoq.Times.once()); + runner.setup(r => r.run(typeMoq.It.isValue(UNITTEST_PROVIDER), typeMoq.It.isAny())) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser.setup(t => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.never()); + + const options = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + options.setup(o => o.args).returns(() => args); + options.setup(o => o.token).returns(() => token.object); + token.setup(t => t.isCancellationRequested) + .returns(() => true); + + const promise = discoveryService.discoverTests(options.object); + + await expect(promise).to.eventually.be.rejectedWith('cancelled'); + runner.verifyAll(); + testParser.verifyAll(); + }); + test('Ensure discovery resolves test suites in n-depth directories', async () => { + const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); + + const testsParser: TestsParser = new TestsParser(testHelper); + + const opts = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + const wspace = typeMoq.Mock.ofType(); + opts.setup(o => o.token).returns(() => token.object); + opts.setup(o => o.workspaceFolder).returns(() => wspace.object); + token.setup(t => t.isCancellationRequested) + .returns(() => true); + opts.setup(o => o.cwd).returns(() => '/home/user/dev'); + opts.setup(o => o.startDirectory).returns(() => '/home/user/dev/tests'); + + const discoveryOutput: string = ['start', + 'apptests.debug.class_name.RootClassName.test_root', + 'apptests.debug.class_name.RootClassName.test_root_other', + 'apptests.debug.first.class_name.FirstLevelClassName.test_first', + 'apptests.debug.first.class_name.FirstLevelClassName.test_first_other', + 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second', + 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second_other', + ''].join('\n'); + + const tests: Tests = testsParser.parse(discoveryOutput, opts.object); + + expect(tests.testFiles.length).to.be.equal(3); + expect(tests.testFunctions.length).to.be.equal(6); + expect(tests.testSuites.length).to.be.equal(3); + expect(tests.testFolders.length).to.be.equal(1); + + // now ensure that each test function belongs within a single test suite... + tests.testFunctions.forEach(fn => { + if (fn.parentTestSuite) { + const testPrefix: boolean = fn.testFunction.nameToRun.startsWith(fn.parentTestSuite.nameToRun); + expect(testPrefix).to.equal(true, + [`function ${fn.testFunction.name} has a parent suite ${fn.parentTestSuite.name}, `, + `but the parent suite 'nameToRun' (${fn.parentTestSuite.nameToRun}) isn't the `, + `prefix to the functions 'nameToRun' (${fn.testFunction.nameToRun})`].join('')); + } + }); + }); + test('Ensure discovery resolves test files in n-depth directories', async () => { + const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); + + const testsParser: TestsParser = new TestsParser(testHelper); + + const opts = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + const wspace = typeMoq.Mock.ofType(); + opts.setup(o => o.token).returns(() => token.object); + opts.setup(o => o.workspaceFolder).returns(() => wspace.object); + token.setup(t => t.isCancellationRequested) + .returns(() => true); + opts.setup(o => o.cwd).returns(() => '/home/user/dev'); + opts.setup(o => o.startDirectory).returns(() => '/home/user/dev/tests'); + + const discoveryOutput: string = ['start', + 'apptests.debug.class_name.RootClassName.test_root', + 'apptests.debug.class_name.RootClassName.test_root_other', + 'apptests.debug.first.class_name.FirstLevelClassName.test_first', + 'apptests.debug.first.class_name.FirstLevelClassName.test_first_other', + 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second', + 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second_other', + ''].join('\n'); + + const tests: Tests = testsParser.parse(discoveryOutput, opts.object); + + expect(tests.testFiles.length).to.be.equal(3); + expect(tests.testFunctions.length).to.be.equal(6); + expect(tests.testSuites.length).to.be.equal(3); + expect(tests.testFolders.length).to.be.equal(1); + + // now ensure that the 'nameToRun' for each test function begins with its file's a single test suite... + tests.testFunctions.forEach(fn => { + if (fn.parentTestSuite) { + const testPrefix: boolean = fn.testFunction.nameToRun.startsWith(fn.parentTestFile.nameToRun); + expect(testPrefix).to.equal(true, + [`function ${fn.testFunction.name} was found in file ${fn.parentTestFile.name}, `, + `but the parent file 'nameToRun' (${fn.parentTestFile.nameToRun}) isn't the `, + `prefix to the functions 'nameToRun' (${fn.testFunction.nameToRun})`].join('')); + } + }); + }); + test('Ensure discovery resolves test suites in n-depth directories when no start directory is given', async () => { + const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); + + const testsParser: TestsParser = new TestsParser(testHelper); + + const opts = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + const wspace = typeMoq.Mock.ofType(); + opts.setup(o => o.token).returns(() => token.object); + opts.setup(o => o.workspaceFolder).returns(() => wspace.object); + token.setup(t => t.isCancellationRequested) + .returns(() => true); + opts.setup(o => o.cwd).returns(() => '/home/user/dev'); + opts.setup(o => o.startDirectory).returns(() => ''); + + const discoveryOutput: string = ['start', + 'apptests.debug.class_name.RootClassName.test_root', + 'apptests.debug.class_name.RootClassName.test_root_other', + 'apptests.debug.first.class_name.FirstLevelClassName.test_first', + 'apptests.debug.first.class_name.FirstLevelClassName.test_first_other', + 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second', + 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second_other', + ''].join('\n'); + + const tests: Tests = testsParser.parse(discoveryOutput, opts.object); + + expect(tests.testFiles.length).to.be.equal(3); + expect(tests.testFunctions.length).to.be.equal(6); + expect(tests.testSuites.length).to.be.equal(3); + expect(tests.testFolders.length).to.be.equal(1); + + // now ensure that each test function belongs within a single test suite... + tests.testFunctions.forEach(fn => { + if (fn.parentTestSuite) { + const testPrefix: boolean = fn.testFunction.nameToRun.startsWith(fn.parentTestSuite.nameToRun); + expect(testPrefix).to.equal(true, + [`function ${fn.testFunction.name} has a parent suite ${fn.parentTestSuite.name}, `, + `but the parent suite 'nameToRun' (${fn.parentTestSuite.nameToRun}) isn't the `, + `prefix to the functions 'nameToRun' (${fn.testFunction.nameToRun})`].join('')); + } + }); + }); + test('Ensure discovery resolves test suites in n-depth directories when a relative start directory is given', async () => { + const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); + + const testsParser: TestsParser = new TestsParser(testHelper); + + const opts = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + const wspace = typeMoq.Mock.ofType(); + opts.setup(o => o.token).returns(() => token.object); + opts.setup(o => o.workspaceFolder).returns(() => wspace.object); + token.setup(t => t.isCancellationRequested) + .returns(() => true); + opts.setup(o => o.cwd).returns(() => '/home/user/dev'); + opts.setup(o => o.startDirectory).returns(() => './tests'); + + const discoveryOutput: string = ['start', + 'apptests.debug.class_name.RootClassName.test_root', + 'apptests.debug.class_name.RootClassName.test_root_other', + 'apptests.debug.first.class_name.FirstLevelClassName.test_first', + 'apptests.debug.first.class_name.FirstLevelClassName.test_first_other', + 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second', + 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second_other', + ''].join('\n'); + + const tests: Tests = testsParser.parse(discoveryOutput, opts.object); + + expect(tests.testFiles.length).to.be.equal(3); + expect(tests.testFunctions.length).to.be.equal(6); + expect(tests.testSuites.length).to.be.equal(3); + expect(tests.testFolders.length).to.be.equal(1); + + // now ensure that each test function belongs within a single test suite... + tests.testFunctions.forEach(fn => { + if (fn.parentTestSuite) { + const testPrefix: boolean = fn.testFunction.nameToRun.startsWith(fn.parentTestSuite.nameToRun); + expect(testPrefix).to.equal(true, + [`function ${fn.testFunction.name} has a parent suite ${fn.parentTestSuite.name}, `, + `but the parent suite 'nameToRun' (${fn.parentTestSuite.nameToRun}) isn't the `, + `prefix to the functions 'nameToRun' (${fn.testFunction.nameToRun})`].join('')); + } + }); + }); + test('Ensure discovery will not fail with blank content' , async () => { + const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); + + const testsParser: TestsParser = new TestsParser(testHelper); + + const opts = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + const wspace = typeMoq.Mock.ofType(); + opts.setup(o => o.token).returns(() => token.object); + opts.setup(o => o.workspaceFolder).returns(() => wspace.object); + token.setup(t => t.isCancellationRequested) + .returns(() => true); + opts.setup(o => o.cwd).returns(() => '/home/user/dev'); + opts.setup(o => o.startDirectory).returns(() => './tests'); + + const tests: Tests = testsParser.parse('', opts.object); + + expect(tests.testFiles.length).to.be.equal(0); + expect(tests.testFunctions.length).to.be.equal(0); + expect(tests.testSuites.length).to.be.equal(0); + expect(tests.testFolders.length).to.be.equal(0); + }); + test('Ensure discovery will not fail with corrupt content', async () => { + const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); + + const testsParser: TestsParser = new TestsParser(testHelper); + + const opts = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + const wspace = typeMoq.Mock.ofType(); + opts.setup(o => o.token).returns(() => token.object); + opts.setup(o => o.workspaceFolder).returns(() => wspace.object); + token.setup(t => t.isCancellationRequested) + .returns(() => true); + opts.setup(o => o.cwd).returns(() => '/home/user/dev'); + opts.setup(o => o.startDirectory).returns(() => './tests'); + + const discoveryOutput: string = ['a;lskdjfa', + 'allikbrilkpdbfkdfbalk;nfm', + '', + ';;h,spmn,nlikmslkjls.bmnl;klkjna;jdfngad,lmvnjkldfhb', + ''].join('\n'); + + const tests: Tests = testsParser.parse(discoveryOutput, opts.object); + + expect(tests.testFiles.length).to.be.equal(0); + expect(tests.testFunctions.length).to.be.equal(0); + expect(tests.testSuites.length).to.be.equal(0); + expect(tests.testFolders.length).to.be.equal(0); + }); + test('Ensure discovery resolves when no tests are found in the given path', async () => { + const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); + + const testsParser: TestsParser = new TestsParser(testHelper); + + const opts = typeMoq.Mock.ofType(); + const token = typeMoq.Mock.ofType(); + const wspace = typeMoq.Mock.ofType(); + opts.setup(o => o.token).returns(() => token.object); + opts.setup(o => o.workspaceFolder).returns(() => wspace.object); + token.setup(t => t.isCancellationRequested) + .returns(() => true); + opts.setup(o => o.cwd).returns(() => '/home/user/dev'); + opts.setup(o => o.startDirectory).returns(() => './tests'); + + const discoveryOutput: string = 'start'; + + const tests: Tests = testsParser.parse(discoveryOutput, opts.object); + + expect(tests.testFiles.length).to.be.equal(0); + expect(tests.testFunctions.length).to.be.equal(0); + expect(tests.testSuites.length).to.be.equal(0); + expect(tests.testFolders.length).to.be.equal(0); + }); +}); diff --git a/src/test/unittests/unittest.run.test.ts b/src/test/unittests/unittest/unittest.run.test.ts similarity index 87% rename from src/test/unittests/unittest.run.test.ts rename to src/test/unittests/unittest/unittest.run.test.ts index f74ff2522acb..45ecc8a9e939 100644 --- a/src/test/unittests/unittest.run.test.ts +++ b/src/test/unittests/unittest/unittest.run.test.ts @@ -6,16 +6,23 @@ import * as fs from 'fs-extra'; import { EOL } from 'os'; import * as path from 'path'; import { ConfigurationTarget } from 'vscode'; -import { IProcessService } from '../../client/common/process/types'; -import { CommandSource } from '../../client/unittests/common/constants'; -import { ITestManagerFactory, IUnitTestSocketServer, TestsToRun } from '../../client/unittests/common/types'; -import { rootWorkspaceUri, updateSetting } from '../common'; -import { MockProcessService } from '../mocks/proc'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; -import { MockUnitTestSocketServer } from './mocks'; -import { UnitTestIocContainer } from './serviceRegistry'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { IProcessServiceFactory } from '../../../client/common/process/types'; +import { ArgumentsHelper } from '../../../client/unittests/common/argumentsHelper'; +import { CommandSource, UNITTEST_PROVIDER } from '../../../client/unittests/common/constants'; +import { TestRunner } from '../../../client/unittests/common/runner'; +import { ITestManagerFactory, ITestRunner, IUnitTestSocketServer, TestsToRun } from '../../../client/unittests/common/types'; +import { IArgumentsHelper, IArgumentsService, ITestManagerRunner, IUnitTestHelper } from '../../../client/unittests/types'; +import { UnitTestHelper } from '../../../client/unittests/unittest/helper'; +import { TestManagerRunner } from '../../../client/unittests/unittest/runner'; +import { ArgumentsService } from '../../../client/unittests/unittest/services/argsService'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { MockProcessService } from '../../mocks/proc'; +import { MockUnitTestSocketServer } from '../mocks'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; -const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles'); +const testFilesPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles'); const UNITTEST_TEST_FILES_PATH = path.join(testFilesPath, 'standard'); const unitTestSpecificTestFilesPath = path.join(testFilesPath, 'specificTest'); const defaultUnitTestArgs = [ @@ -43,7 +50,7 @@ suite('Unit Tests - unittest - run with mocked process output', () => { } await initializeTest(); initializeDI(); - ignoreTestLauncher(); + await ignoreTestLauncher(); }); teardown(async () => { ioc.dispose(); @@ -68,10 +75,15 @@ suite('Unit Tests - unittest - run with mocked process output', () => { ioc.registerTestsHelper(); ioc.registerTestStorage(); ioc.registerTestVisitors(); + ioc.serviceManager.add(IArgumentsService, ArgumentsService, UNITTEST_PROVIDER); + ioc.serviceManager.add(IArgumentsHelper, ArgumentsHelper); + ioc.serviceManager.add(ITestManagerRunner, TestManagerRunner, UNITTEST_PROVIDER); + ioc.serviceManager.add(ITestRunner, TestRunner); + ioc.serviceManager.add(IUnitTestHelper, UnitTestHelper); } - function ignoreTestLauncher() { - const procService = ioc.serviceContainer.get(IProcessService); + async function ignoreTestLauncher() { + const procService = await ioc.serviceContainer.get(IProcessServiceFactory).create() as MockProcessService; // When running the python test launcher, just return. procService.onExecObservable((file, args, options, callback) => { if (args.length > 1 && args[0].endsWith('visualstudio_py_testlauncher.py')) { @@ -79,8 +91,8 @@ suite('Unit Tests - unittest - run with mocked process output', () => { } }); } - function injectTestDiscoveryOutput(output: string) { - const procService = ioc.serviceContainer.get(IProcessService); + async function injectTestDiscoveryOutput(output: string) { + const procService = await ioc.serviceContainer.get(IProcessServiceFactory).create() as MockProcessService; procService.onExecObservable((file, args, options, callback) => { if (args.length > 1 && args[0] === '-c' && args[1].includes('import unittest') && args[1].includes('loader = unittest.TestLoader()')) { callback({ @@ -101,7 +113,7 @@ suite('Unit Tests - unittest - run with mocked process output', () => { test('Run Tests', async () => { await updateSetting('unitTest.unittestArgs', ['-v', '-s', './tests', '-p', 'test_unittest*.py'], rootWorkspaceUri, configTarget); // tslint:disable-next-line:no-multiline-string - injectTestDiscoveryOutput(`start + await injectTestDiscoveryOutput(`start test_unittest_one.Test_test1.test_A test_unittest_one.Test_test1.test_B test_unittest_one.Test_test1.test_c @@ -138,7 +150,7 @@ suite('Unit Tests - unittest - run with mocked process output', () => { test('Run Failed Tests', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); // tslint:disable-next-line:no-multiline-string - injectTestDiscoveryOutput(`start + await injectTestDiscoveryOutput(`start test_unittest_one.Test_test1.test_A test_unittest_one.Test_test1.test_B test_unittest_one.Test_test1.test_c @@ -191,7 +203,7 @@ suite('Unit Tests - unittest - run with mocked process output', () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); // tslint:disable-next-line:no-multiline-string - injectTestDiscoveryOutput(`start + await injectTestDiscoveryOutput(`start test_unittest_one.Test_test_one_1.test_1_1_1 test_unittest_one.Test_test_one_1.test_1_1_2 test_unittest_one.Test_test_one_1.test_1_1_3 @@ -228,7 +240,7 @@ suite('Unit Tests - unittest - run with mocked process output', () => { test('Run Specific Test Suite', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); // tslint:disable-next-line:no-multiline-string - injectTestDiscoveryOutput(`start + await injectTestDiscoveryOutput(`start test_unittest_one.Test_test_one_1.test_1_1_1 test_unittest_one.Test_test_one_1.test_1_1_2 test_unittest_one.Test_test_one_1.test_1_1_3 @@ -265,7 +277,7 @@ suite('Unit Tests - unittest - run with mocked process output', () => { test('Run Specific Test Function', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); // tslint:disable-next-line:no-multiline-string - injectTestDiscoveryOutput(`start + await injectTestDiscoveryOutput(`start test_unittest_one.Test_test1.test_A test_unittest_one.Test_test1.test_B test_unittest_one.Test_test1.test_c diff --git a/src/test/unittests/unittest.test.ts b/src/test/unittests/unittest/unittest.test.ts similarity index 80% rename from src/test/unittests/unittest.test.ts rename to src/test/unittests/unittest/unittest.test.ts index 8465b7bdbb92..4152ef540be9 100644 --- a/src/test/unittests/unittest.test.ts +++ b/src/test/unittests/unittest/unittest.test.ts @@ -2,13 +2,14 @@ import * as assert from 'assert'; import * as fs from 'fs-extra'; import * as path from 'path'; import { ConfigurationTarget } from 'vscode'; -import { CommandSource } from '../../client/unittests/common/constants'; -import { ITestManagerFactory } from '../../client/unittests/common/types'; -import { rootWorkspaceUri, updateSetting } from '../common'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; -import { UnitTestIocContainer } from './serviceRegistry'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { CommandSource } from '../../../client/unittests/common/constants'; +import { ITestManagerFactory } from '../../../client/unittests/common/types'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; -const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles'); +const testFilesPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles'); const UNITTEST_TEST_FILES_PATH = path.join(testFilesPath, 'standard'); const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(testFilesPath, 'single'); const defaultUnitTestArgs = [ @@ -57,6 +58,6 @@ suite('Unit Tests - unittest - discovery against actual python process', () => { 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'); - assert.equal(tests.testFiles.some(t => t.name === 'test_one.py' && t.nameToRun === 'Test_test1.test_A'), true, 'Test File not found'); + assert.equal(tests.testFiles.some(t => t.name === 'test_one.py' && t.nameToRun === 'test_one.Test_test1.test_A'), true, 'Test File not found'); }); }); diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts new file mode 100644 index 000000000000..a1a35ed406c2 --- /dev/null +++ b/src/test/vscode-mock.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-invalid-this no-require-imports no-var-requires no-any + +import * as TypeMoq from 'typemoq'; +import * as vscode from 'vscode'; +import * as vscodeMocks from './mocks/vsc'; +import { vscMockTelemetryReporter } from './mocks/vsc/telemetryReporter'; +const Module = require('module'); + +type VSCode = typeof vscode; + +const mockedVSCode: Partial = {}; +const mockedVSCodeNamespaces: { [P in keyof VSCode]?: TypeMoq.IMock } = {}; +const originalLoad = Module._load; + +function generateMock(name: K): void { + const mockedObj = TypeMoq.Mock.ofType(); + mockedVSCode[name] = mockedObj.object; + mockedVSCodeNamespaces[name] = mockedObj as any; +} + +export function initialize() { + generateMock('workspace'); + generateMock('window'); + generateMock('commands'); + generateMock('languages'); + generateMock('env'); + generateMock('debug'); + generateMock('scm'); + + // When upgrading to npm 9-10, this might have to change, as we could have explicit imports (named imports). + Module._load = function (request, parent) { + if (request === 'vscode') { + return mockedVSCode; + } + if (request === 'vscode-extension-telemetry') { + return { default: vscMockTelemetryReporter }; + } + return originalLoad.apply(this, arguments); + }; +} + +mockedVSCode.Disposable = vscodeMocks.vscMock.Disposable as any; +mockedVSCode.EventEmitter = vscodeMocks.vscMock.EventEmitter; +mockedVSCode.CancellationTokenSource = vscodeMocks.vscMock.CancellationTokenSource; +mockedVSCode.CompletionItemKind = vscodeMocks.vscMock.CompletionItemKind; +mockedVSCode.SymbolKind = vscodeMocks.vscMock.SymbolKind; +mockedVSCode.Uri = vscodeMocks.vscMock.Uri as any; +mockedVSCode.Range = vscodeMocks.vscMockExtHostedTypes.Range; +mockedVSCode.Position = vscodeMocks.vscMockExtHostedTypes.Position; +mockedVSCode.Selection = vscodeMocks.vscMockExtHostedTypes.Selection; +mockedVSCode.Location = vscodeMocks.vscMockExtHostedTypes.Location; +mockedVSCode.SymbolInformation = vscodeMocks.vscMockExtHostedTypes.SymbolInformation; +mockedVSCode.CompletionItem = vscodeMocks.vscMockExtHostedTypes.CompletionItem; +mockedVSCode.CompletionItemKind = vscodeMocks.vscMockExtHostedTypes.CompletionItemKind; +mockedVSCode.CodeLens = vscodeMocks.vscMockExtHostedTypes.CodeLens; +mockedVSCode.DiagnosticSeverity = vscodeMocks.vscMockExtHostedTypes.DiagnosticSeverity; +mockedVSCode.SnippetString = vscodeMocks.vscMockExtHostedTypes.SnippetString; +mockedVSCode.EventEmitter = vscodeMocks.vscMock.EventEmitter; +mockedVSCode.ConfigurationTarget = vscodeMocks.vscMockExtHostedTypes.ConfigurationTarget; +mockedVSCode.StatusBarAlignment = vscodeMocks.vscMockExtHostedTypes.StatusBarAlignment; +mockedVSCode.SignatureHelp = vscodeMocks.vscMockExtHostedTypes.SignatureHelp; +mockedVSCode.DocumentLink = vscodeMocks.vscMockExtHostedTypes.DocumentLink; + +// This API is used in src/client/telemetry/telemetry.ts +const extensions = TypeMoq.Mock.ofType(); +extensions.setup(e => e.all).returns(() => []); +const extension = TypeMoq.Mock.ofType>(); +const packageJson = TypeMoq.Mock.ofType(); +const contributes = TypeMoq.Mock.ofType(); +extension.setup(e => e.packageJSON).returns(() => packageJson.object); +packageJson.setup(p => p.contributes).returns(() => contributes.object); +contributes.setup(p => p.debuggers).returns(() => [{ aiKey: '' }]); +extensions.setup(e => e.getExtension(TypeMoq.It.isAny())).returns(() => extension.object); +mockedVSCode.extensions = extensions.object; diff --git a/src/test/workspaceSymbols/multiroot.test.ts b/src/test/workspaceSymbols/multiroot.test.ts index ccbf64b3adcb..e6e0f45febe3 100644 --- a/src/test/workspaceSymbols/multiroot.test.ts +++ b/src/test/workspaceSymbols/multiroot.test.ts @@ -1,7 +1,7 @@ import * as assert from 'assert'; import * as path from 'path'; import { CancellationTokenSource, ConfigurationTarget, Uri } from 'vscode'; -import { IProcessService } from '../../client/common/process/types'; +import { IProcessServiceFactory } from '../../client/common/process/types'; import { Generator } from '../../client/workspaceSymbols/generator'; import { WorkspaceSymbolProvider } from '../../client/workspaceSymbols/provider'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; @@ -13,7 +13,7 @@ const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRo suite('Multiroot Workspace Symbols', () => { let ioc: UnitTestIocContainer; - let processService: IProcessService; + let processServiceFactory: IProcessServiceFactory; suiteSetup(function () { if (!IS_MULTI_ROOT_TEST) { // tslint:disable-next-line:no-invalid-this @@ -37,7 +37,7 @@ suite('Multiroot Workspace Symbols', () => { ioc.registerCommonTypes(); ioc.registerVariableTypes(); ioc.registerProcessTypes(); - processService = ioc.serviceContainer.get(IProcessService); + processServiceFactory = ioc.serviceContainer.get(IProcessServiceFactory); } test('symbols should be returned when enabeld and vice versa', async () => { @@ -46,7 +46,7 @@ suite('Multiroot Workspace Symbols', () => { await updateSetting('workspaceSymbols.enabled', false, childWorkspaceUri, ConfigurationTarget.WorkspaceFolder); - let generator = new Generator(childWorkspaceUri, outputChannel, processService); + let generator = new Generator(childWorkspaceUri, outputChannel, processServiceFactory); 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'); @@ -54,7 +54,7 @@ suite('Multiroot Workspace Symbols', () => { await updateSetting('workspaceSymbols.enabled', true, childWorkspaceUri, ConfigurationTarget.WorkspaceFolder); - generator = new Generator(childWorkspaceUri, outputChannel, processService); + generator = new Generator(childWorkspaceUri, outputChannel, processServiceFactory); 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'); @@ -68,8 +68,8 @@ suite('Multiroot Workspace Symbols', () => { await updateSetting('workspaceSymbols.enabled', true, workspace2Uri, ConfigurationTarget.WorkspaceFolder); const generators = [ - new Generator(childWorkspaceUri, outputChannel, processService), - new Generator(workspace2Uri, outputChannel, processService)]; + new Generator(childWorkspaceUri, outputChannel, processServiceFactory), + new Generator(workspace2Uri, outputChannel, processServiceFactory)]; const provider = new WorkspaceSymbolProvider(generators, outputChannel); const symbols = await provider.provideWorkspaceSymbols('meth1Of', new CancellationTokenSource().token); diff --git a/src/test/workspaceSymbols/standard.test.ts b/src/test/workspaceSymbols/standard.test.ts index 5d264e7fd35a..46fdd9ba6102 100644 --- a/src/test/workspaceSymbols/standard.test.ts +++ b/src/test/workspaceSymbols/standard.test.ts @@ -2,7 +2,7 @@ import * as assert from 'assert'; import * as path from 'path'; import { CancellationTokenSource, ConfigurationTarget, Uri } from 'vscode'; import { PythonSettings } from '../../client/common/configSettings'; -import { IProcessService } from '../../client/common/process/types'; +import { IProcessServiceFactory } from '../../client/common/process/types'; import { Generator } from '../../client/workspaceSymbols/generator'; import { WorkspaceSymbolProvider } from '../../client/workspaceSymbols/provider'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; @@ -15,7 +15,7 @@ const configUpdateTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFol suite('Workspace Symbols', () => { let ioc: UnitTestIocContainer; - let processService: IProcessService; + let processServiceFactory: IProcessServiceFactory; suiteSetup(initialize); suiteTeardown(closeActiveWindows); setup(async () => { @@ -32,7 +32,7 @@ suite('Workspace Symbols', () => { ioc.registerCommonTypes(); ioc.registerVariableTypes(); ioc.registerProcessTypes(); - processService = ioc.serviceContainer.get(IProcessService); + processServiceFactory = ioc.serviceContainer.get(IProcessServiceFactory); } test('symbols should be returned when enabeld and vice versa', async () => { @@ -42,9 +42,9 @@ suite('Workspace Symbols', () => { // 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'); + settings.workspaceSymbols!.tagFilePath = path.join(workspaceUri.fsPath, '.vscode', 'tags'); - let generator = new Generator(workspaceUri, outputChannel, processService); + let generator = new Generator(workspaceUri, outputChannel, processServiceFactory); 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'); @@ -55,9 +55,9 @@ suite('Workspace Symbols', () => { // 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'); + settings.workspaceSymbols!.tagFilePath = path.join(workspaceUri.fsPath, '.vscode', 'tags'); - generator = new Generator(workspaceUri, outputChannel, processService); + generator = new Generator(workspaceUri, outputChannel, processServiceFactory); 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'); @@ -70,9 +70,9 @@ suite('Workspace Symbols', () => { // 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'); + settings.workspaceSymbols!.tagFilePath = path.join(workspaceUri.fsPath, '.vscode', 'tags'); - const generators = [new Generator(workspaceUri, outputChannel, processService)]; + const generators = [new Generator(workspaceUri, outputChannel, processServiceFactory)]; const provider = new WorkspaceSymbolProvider(generators, outputChannel); const symbols = await provider.provideWorkspaceSymbols('meth1Of', new CancellationTokenSource().token); diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/home/__init__.py b/src/testMultiRootWkspc/workspace5/djangoApp/home/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/home/templates/index.html b/src/testMultiRootWkspc/workspace5/djangoApp/home/templates/index.html new file mode 100644 index 000000000000..6ca5107d23d6 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/home/templates/index.html @@ -0,0 +1,9 @@ + + + + +

Hello {{ value_from_server }}!

+

Hello {{ another_value_from_server }}!

+ + + diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/home/urls.py b/src/testMultiRootWkspc/workspace5/djangoApp/home/urls.py new file mode 100644 index 000000000000..70a9606e88e6 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/home/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url + +from . import views + +urlpatterns = [ + url('', views.index, name='index'), +] diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/home/views.py b/src/testMultiRootWkspc/workspace5/djangoApp/home/views.py new file mode 100644 index 000000000000..0494f868dc6f --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/home/views.py @@ -0,0 +1,10 @@ +from django.shortcuts import render +from django.template import loader + + +def index(request): + context = { + 'value_from_server':'this_is_a_value_from_server', + 'another_value_from_server':'this_is_another_value_from_server' + } + return render(request, 'index.html', context) diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/manage.py b/src/testMultiRootWkspc/workspace5/djangoApp/manage.py new file mode 100644 index 000000000000..afbc784aafd8 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + execute_from_command_line(sys.argv) diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/mysite/__init__.py b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/mysite/settings.py b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/settings.py new file mode 100644 index 000000000000..4e182517ca2a --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/settings.py @@ -0,0 +1,93 @@ +""" +Django settings for mysite project. + +Generated by 'django-admin startproject' using Django 1.11.2. + +For more information on this file, see +https://docs.djangoproject.com/en/1.11/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.11/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '5u06*)07dvd+=kn)zqp8#b0^qt@*$8=nnjc&&0lzfc28(wns&l' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['localhost', '127.0.0.1'] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.contenttypes', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ +] + +ROOT_URLCONF = 'mysite.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': ['home/templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'mysite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.11/ref/settings/#databases + +DATABASES = { +} + + +# Password validation +# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ +] + + +# Internationalization +# https://docs.djangoproject.com/en/1.11/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.11/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/mysite/urls.py b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/urls.py new file mode 100644 index 000000000000..9db383365e3e --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/urls.py @@ -0,0 +1,23 @@ +"""mysite URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.11/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import url, include +from django.contrib import admin +from django.views.generic import RedirectView + +urlpatterns = [ + url(r'^home/', include('home.urls')), + url('', RedirectView.as_view(url='/home/')), +] diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/mysite/wsgi.py b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/wsgi.py new file mode 100644 index 000000000000..74e7daeefe76 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for mysite project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") + +application = get_wsgi_application() diff --git a/src/testMultiRootWkspc/workspace5/flaskApp/run.py b/src/testMultiRootWkspc/workspace5/flaskApp/run.py new file mode 100644 index 000000000000..9c3172c3e918 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/flaskApp/run.py @@ -0,0 +1,13 @@ +from flask import Flask, render_template +app = Flask(__name__) + + +@app.route('/') +def hello(): + return render_template('index.html', + value_from_server='this_is_a_value_from_server', + another_value_from_server='this_is_another_value_from_server') + + +if __name__ == '__main__': + app.run() diff --git a/src/testMultiRootWkspc/workspace5/flaskApp/templates/index.html b/src/testMultiRootWkspc/workspace5/flaskApp/templates/index.html new file mode 100644 index 000000000000..6ca5107d23d6 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/flaskApp/templates/index.html @@ -0,0 +1,9 @@ + + + + +

Hello {{ value_from_server }}!

+

Hello {{ another_value_from_server }}!

+ + + diff --git a/src/testMultiRootWkspc/workspace5/mymod/__init__.py b/src/testMultiRootWkspc/workspace5/mymod/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/testMultiRootWkspc/workspace5/mymod/__main__.py b/src/testMultiRootWkspc/workspace5/mymod/__main__.py new file mode 100644 index 000000000000..f1a18139c84a --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/mymod/__main__.py @@ -0,0 +1 @@ +print("Hello world!") diff --git a/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.py b/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.py new file mode 100644 index 000000000000..ad8d7003cb6d --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.py @@ -0,0 +1,14 @@ +import sys +import time +time.sleep(2) +sys.stdout.write('this is stdout') +sys.stdout.flush() +sys.stderr.write('this is stderr') +sys.stderr.flush() +# Give the debugger some time to add a breakpoint. +time.sleep(5) +for i in range(1): + time.sleep(0.5) + pass + +print('this is print') diff --git a/tslint.json b/tslint.json index 4701c1abcbbc..600e28f64075 100644 --- a/tslint.json +++ b/tslint.json @@ -53,6 +53,13 @@ "no-empty-interface": false, "no-bitwise": false, "eofline": true, - "switch-final-break": false + "switch-final-break": false, + "no-implicit-dependencies": [ + "vscode" + ], + "no-unnecessary-type-assertion": false, + "no-submodule-imports": false, + "no-redundant-jsdoc": false, + "binary-expression-operand-order": false } -} +} \ No newline at end of file diff --git a/typeshed b/typeshed new file mode 160000 index 000000000000..95eff73ab209 --- /dev/null +++ b/typeshed @@ -0,0 +1 @@ +Subproject commit 95eff73ab2092f2c3158198d404a921447172418 diff --git a/typings/extensions.d.ts b/typings/extensions.d.ts index 046525bebc23..3a5ff4282fa9 100644 --- a/typings/extensions.d.ts +++ b/typings/extensions.d.ts @@ -26,6 +26,11 @@ declare interface String { * E.g. if an argument contains a space, then it will be enclosed within double quotes. */ fileToCommandArgument(): string; + /** + * String.format() implementation. + * Tokens such as {0}, {1} will be replaced with corresponding positional arguments. + */ + format(...args: string[]): string; } // tslint:disable-next-line:interface-name diff --git a/typings/memory-stream.d.ts b/typings/memory-stream.d.ts deleted file mode 100644 index 5efd28e0d2dc..000000000000 --- a/typings/memory-stream.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -declare module 'memory-streams' { - export class ReadableStream implements NodeJS.ReadableStream { - readable: boolean; - read(size?: number): string | Buffer; - setEncoding(encoding: string): void; - pause(): this; - resume(): this; - isPaused(): boolean; - pipe(destination: T, options?: { end?: boolean; }): T; - unpipe(destination?: T): void; - unshift(chunk: string): void; - unshift(chunk: Buffer): void; - unshift(chunk: any); - wrap(oldStream: NodeJS.ReadableStream): NodeJS.ReadableStream; - addListener(event: string | symbol, listener: Function): this; - on(event: string | symbol, listener: Function): this; - once(event: string | symbol, listener: Function): this; - removeListener(event: string | symbol, listener: Function): this; - removeAllListeners(event?: string | symbol): this; - setMaxListeners(n: number): this; - getMaxListeners(): number; - listeners(event: string | symbol): Function[]; - emit(event: string | symbol, ...args: any[]): boolean; - listenerCount(type: string | symbol): number; - prependListener(event: string | symbol, listener: Function): this; - prependOnceListener(event: string | symbol, listener: Function): this; - eventNames(): (string | symbol)[]; - constructor(content: string); - } -} diff --git a/vscode-python-signing.csproj b/vscode-python-signing.csproj new file mode 100644 index 000000000000..7fb333c4b277 --- /dev/null +++ b/vscode-python-signing.csproj @@ -0,0 +1,20 @@ + + + netcoreapp2.0 + + + + + + $(OutputPath)\python-$(Branch).vsix + $(UserProfile)\AppData\Roaming\npm\vsce + + + + + + + VsixSHA2 + + + diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 85dbba812292..000000000000 --- a/yarn.lock +++ /dev/null @@ -1,4762 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@gulp-sourcemaps/identity-map@1.X": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@gulp-sourcemaps/identity-map/-/identity-map-1.0.1.tgz#cfa23bc5840f9104ce32a65e74db7e7a974bbee1" - dependencies: - acorn "^5.0.3" - css "^2.2.1" - normalize-path "^2.1.1" - source-map "^0.5.6" - through2 "^2.0.3" - -"@gulp-sourcemaps/map-sources@1.X": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz#890ae7c5d8c877f6d384860215ace9d7ec945bda" - dependencies: - normalize-path "^2.0.1" - through2 "^2.0.3" - -"@types/chai-as-promised@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.0.tgz#010b04cde78eacfb6e72bfddb3e58fe23c2e78b9" - dependencies: - "@types/chai" "*" - -"@types/chai@*": - version "4.0.10" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.0.10.tgz#0eb222c7353adde8e0980bea04165d4d3b6afef3" - -"@types/chai@4.0.6": - version "4.0.6" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.0.6.tgz#9cb5a7fb7dd83be0cfcaafdbd95a2b5dd351762f" - -"@types/commander@^2.11.0": - version "2.12.2" - resolved "https://registry.yarnpkg.com/@types/commander/-/commander-2.12.2.tgz#183041a23842d4281478fa5d23c5ca78e6fd08ae" - dependencies: - commander "*" - -"@types/del@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/del/-/del-3.0.0.tgz#1c8cd8b6e38da3b572352ca8eaf5527931426288" - dependencies: - "@types/glob" "*" - -"@types/event-stream@^3.3.33": - version "3.3.33" - resolved "https://registry.yarnpkg.com/@types/event-stream/-/event-stream-3.3.33.tgz#ca155f6e805b606175322c03e6d75bc3b940cb95" - dependencies: - "@types/node" "*" - -"@types/events@*": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@types/events/-/events-1.1.0.tgz#93b1be91f63c184450385272c47b6496fd028e02" - -"@types/fs-extra@^4.0.2": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-4.0.7.tgz#02533262386b5a6b9a49797dc82feffdf269140a" - dependencies: - "@types/node" "*" - -"@types/get-port@^3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@types/get-port/-/get-port-3.2.0.tgz#f9e0a11443cc21336470185eae3dfba4495d29bc" - -"@types/glob@*", "@types/glob@^5.0.34": - version "5.0.34" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.34.tgz#ee626c9be3da877d717911c6101eee0a9871bbf4" - dependencies: - "@types/events" "*" - "@types/minimatch" "*" - "@types/node" "*" - -"@types/iconv-lite@0.0.1": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@types/iconv-lite/-/iconv-lite-0.0.1.tgz#aa3b8bda2be512b1ae0a057b942e869c370a5569" - dependencies: - "@types/node" "*" - -"@types/istanbul@^0.4.29": - version "0.4.29" - resolved "https://registry.yarnpkg.com/@types/istanbul/-/istanbul-0.4.29.tgz#29c8cbb747ac57280965545dc58514ba0dbb99af" - -"@types/lodash@^4.14.74": - version "4.14.91" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.91.tgz#794611b28056d16b5436059c6d800b39d573cd3a" - -"@types/md5@^2.1.32": - version "2.1.32" - resolved "https://registry.yarnpkg.com/@types/md5/-/md5-2.1.32.tgz#93e23437fcd17a7b9ca98d02aa6002e835842fe8" - dependencies: - "@types/node" "*" - -"@types/minimatch@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.2.tgz#09c06877e478a5d5f32ce5017c2eb2b33006f6f5" - -"@types/mocha@^2.2.43": - version "2.2.45" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.45.tgz#816572b6e45164526a36d4faa123e8267d6d5d0a" - dependencies: - "@types/node" "*" - -"@types/node@*": - version "8.5.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.5.2.tgz#83b8103fa9a2c2e83d78f701a9aa7c9539739aa5" - -"@types/node@^6.0.40": - version "6.0.95" - resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.95.tgz#0d027612a77c55b84497ff90a4a7d597e5ac0fab" - -"@types/semver@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.4.0.tgz#f3658535af7f1f502acd6da7daf405ffeb1f7ee4" - -"@types/shortid@0.0.29": - version "0.0.29" - resolved "https://registry.yarnpkg.com/@types/shortid/-/shortid-0.0.29.tgz#8093ee0416a6e2bf2aa6338109114b3fbffa0e9b" - -"@types/sinon@^2.3.2": - version "2.3.7" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-2.3.7.tgz#e92c2fed3297eae078d78d1da032b26788b4af86" - -"@types/untildify@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/untildify/-/untildify-3.0.0.tgz#cd3e6624e46ccf292d3823fb48fa90dda0deaec0" - -"@types/uuid@^3.3.27": - version "3.4.3" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.3.tgz#121ace265f5569ce40f4f6d0ff78a338c732a754" - dependencies: - "@types/node" "*" - -"@types/winreg@^1.2.30": - version "1.2.30" - resolved "https://registry.yarnpkg.com/@types/winreg/-/winreg-1.2.30.tgz#91d6710e536d345b9c9b017c574cf6a8da64c518" - -"@types/xml2js@^0.4.0": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.4.2.tgz#a4b84b3879ffd4710953fd92cabfde9a8a4e8456" - dependencies: - "@types/node" "*" - -JSONStream@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.2.tgz#c102371b6ec3a7cf3b847ca00c20bb0fce4c6dea" - dependencies: - jsonparse "^1.2.0" - through ">=2.2.7 <3" - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - -abbrev@1.0.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" - -acorn@5.X, acorn@^5.0.3: - version "5.5.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9" - -ajv@^4.9.1: - version "4.11.8" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" - dependencies: - co "^4.6.0" - json-stable-stringify "^1.0.1" - -ajv@^5.1.0: - version "5.5.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" - dependencies: - co "^4.6.0" - fast-deep-equal "^1.0.0" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.3.0" - -align-text@^0.1.1, align-text@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" - dependencies: - kind-of "^3.0.2" - longest "^1.0.1" - repeat-string "^1.5.2" - -amdefine@>=0.0.4, amdefine@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - -ansi-cyan@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-cyan/-/ansi-cyan-0.1.1.tgz#538ae528af8982f28ae30d86f2f17456d2609873" - dependencies: - ansi-wrap "0.1.0" - -ansi-gray@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" - dependencies: - ansi-wrap "0.1.0" - -ansi-red@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-red/-/ansi-red-0.1.1.tgz#8c638f9d1080800a353c9c28c8a81ca4705d946c" - dependencies: - ansi-wrap "0.1.0" - -ansi-regex@^0.2.0, ansi-regex@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - -ansi-styles@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de" - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - -ansi-styles@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" - dependencies: - color-convert "^1.9.0" - -ansi-wrap@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" - -anymatch@^1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" - dependencies: - micromatch "^2.1.5" - normalize-path "^2.0.0" - -applicationinsights@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.1.tgz#53446b830fe8d5d619eee2a278b31d3d25030927" - dependencies: - diagnostic-channel "0.2.0" - diagnostic-channel-publishers "0.2.1" - zone.js "0.7.6" - -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - -arch@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.0.tgz#3613aa46149064b3c1f0607919bf1d4786e82889" - -archy@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" - -are-we-there-yet@~1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - -argparse@^1.0.7: - version "1.0.9" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" - dependencies: - sprintf-js "~1.0.2" - -argv@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/argv/-/argv-0.0.2.tgz#ecbd16f8949b157183711b1bda334f37840185ab" - -arr-diff@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-1.1.0.tgz#687c32758163588fef7de7b36fabe495eb1a399a" - dependencies: - arr-flatten "^1.0.1" - array-slice "^0.2.3" - -arr-diff@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" - dependencies: - arr-flatten "^1.0.1" - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - -arr-flatten@^1.0.1, arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - -arr-union@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-2.1.0.tgz#20f9eab5ec70f5c7d215b1077b1c39161d292c7d" - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - -array-differ@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" - -array-each@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" - -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - -array-slice@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" - -array-slice@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4" - -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - dependencies: - array-uniq "^1.0.1" - -array-uniq@^1.0.1, array-uniq@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - -array-unique@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - -arrify@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - -asn1@~0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - -assert-plus@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" - -assertion-error@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c" - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - -async-each@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" - -async@1.x, async@^1.4.0: - version "1.5.2" - resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - -atob@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.0.3.tgz#19c7a760473774468f20b2d2d03372ad7d4cbf5d" - -atob@~1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/atob/-/atob-1.1.3.tgz#95f13629b12c3a51a5d215abdce2aa9f32f80773" - -aws-sign2@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - -aws4@^1.2.1, aws4@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" - -azure-storage@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/azure-storage/-/azure-storage-2.7.0.tgz#106c35f0ba6c551f928bd394c026e44d0401ffcf" - dependencies: - browserify-mime "~1.2.9" - extend "~1.2.1" - json-edm-parser "0.1.2" - md5.js "1.3.4" - readable-stream "~2.0.0" - request "~2.81.0" - underscore "~1.8.3" - uuid "^3.0.0" - validator "~3.35.0" - xml2js "0.2.7" - xmlbuilder "0.4.3" - -babel-code-frame@^6.22.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -bcrypt-pbkdf@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" - dependencies: - tweetnacl "^0.14.3" - -beeper@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809" - -binary-extensions@^1.0.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" - -block-stream@*: - version "0.0.9" - resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" - dependencies: - inherits "~2.0.0" - -boom@2.x.x: - version "2.10.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" - dependencies: - hoek "2.x.x" - -boom@4.x.x: - version "4.3.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" - dependencies: - hoek "4.x.x" - -boom@5.x.x: - version "5.2.0" - resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" - dependencies: - hoek "4.x.x" - -brace-expansion@^1.0.0, brace-expansion@^1.1.7: - version "1.1.8" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^1.8.2: - version "1.8.5" - resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" - dependencies: - expand-range "^1.8.1" - preserve "^0.2.0" - repeat-element "^1.1.2" - -braces@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.0.tgz#a46941cb5fb492156b3d6a656e06c35364e3e66e" - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - define-property "^1.0.0" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -browser-stdout@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" - -browserify-mime@~1.2.9: - version "1.2.9" - resolved "https://registry.yarnpkg.com/browserify-mime/-/browserify-mime-1.2.9.tgz#aeb1af28de6c0d7a6a2ce40adb68ff18422af31f" - -buffer-crc32@~0.2.3: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - -builtin-modules@^1.0.0, builtin-modules@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -callsite@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" - -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - -camelcase@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" - -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - -caseless@~0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - -center-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" - dependencies: - align-text "^0.1.3" - lazy-cache "^1.0.3" - -chai-as-promised@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-7.1.1.tgz#08645d825deb8696ee61725dbf590c012eb00ca0" - dependencies: - check-error "^1.0.2" - -chai@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.1.2.tgz#0f64584ba642f0f2ace2806279f4f06ca23ad73c" - dependencies: - assertion-error "^1.0.1" - check-error "^1.0.1" - deep-eql "^3.0.0" - get-func-name "^2.0.0" - pathval "^1.0.0" - type-detect "^4.0.0" - -chalk@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174" - dependencies: - ansi-styles "^1.1.0" - escape-string-regexp "^1.0.0" - has-ansi "^0.1.0" - strip-ansi "^0.3.0" - supports-color "^0.2.0" - -chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" - dependencies: - ansi-styles "^3.1.0" - escape-string-regexp "^1.0.5" - supports-color "^4.0.0" - -charenc@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" - -check-error@^1.0.1, check-error@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" - -chokidar@^1.6.1: - version "1.7.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" - dependencies: - anymatch "^1.3.0" - async-each "^1.0.0" - glob-parent "^2.0.0" - inherits "^2.0.1" - is-binary-path "^1.0.0" - is-glob "^2.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.0.0" - optionalDependencies: - fsevents "^1.0.0" - -ci-info@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.2.tgz#03561259db48d0474c8bdc90f5b47b068b6bbfb4" - -circular-json@^0.3.1: - version "0.3.3" - resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" - -class-utils@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.5.tgz#17e793103750f9627b2176ea34cfd1b565903c80" - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - lazy-cache "^2.0.2" - static-extend "^0.1.1" - -cliui@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" - dependencies: - center-align "^0.1.1" - right-align "^0.1.1" - wordwrap "0.0.2" - -clone-buffer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" - -clone-stats@^0.0.1, clone-stats@~0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" - -clone-stats@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" - -clone@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/clone/-/clone-0.2.0.tgz#c6126a90ad4f72dbf5acdb243cc37724fe93fc1f" - -clone@^1.0.0, clone@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f" - -clone@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb" - -cloneable-readable@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.0.0.tgz#a6290d413f217a61232f95e458ff38418cfb0117" - dependencies: - inherits "^2.0.1" - process-nextick-args "^1.0.6" - through2 "^2.0.1" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - -codecov@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/codecov/-/codecov-3.0.0.tgz#c273b8c4f12945723e8dc9d25803d89343e5f28e" - dependencies: - argv "0.0.2" - request "2.81.0" - urlgrey "0.4.4" - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" - dependencies: - color-name "^1.1.1" - -color-name@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - -color-support@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" - -colors@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" - -combined-stream@^1.0.5, combined-stream@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" - dependencies: - delayed-stream "~1.0.0" - -commander@*, commander@^2.11.0, commander@^2.9.0: - version "2.12.2" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555" - -commander@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06" - -commander@2.11.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" - -commander@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.3.0.tgz#fd430e889832ec353b9acd1de217c11cb3eef873" - -commandpost@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/commandpost/-/commandpost-1.2.1.tgz#2e9c4c7508b9dc704afefaa91cab92ee6054cc68" - -component-emitter@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - -config-chain@~1.1.5: - version "1.1.11" - resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.11.tgz#aba09747dfbe4c3e70e766a6e41586e1859fc6f2" - dependencies: - ini "^1.3.4" - proto-list "~1.2.1" - -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - -convert-source-map@1.X, convert-source-map@^1.1.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - -core-util-is@1.0.2, core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - -crypt@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" - -cryptiles@2.x.x: - version "2.0.5" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" - dependencies: - boom "2.x.x" - -cryptiles@3.x.x: - version "3.1.2" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" - dependencies: - boom "5.x.x" - -css@2.X, css@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/css/-/css-2.2.1.tgz#73a4c81de85db664d4ee674f7d47085e3b2d55dc" - dependencies: - inherits "^2.0.1" - source-map "^0.1.38" - source-map-resolve "^0.3.0" - urix "^0.1.0" - -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - dependencies: - array-find-index "^1.0.1" - -d@1: - version "1.0.0" - resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" - dependencies: - es5-ext "^0.10.9" - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - dependencies: - assert-plus "^1.0.0" - -dateformat@^1.0.11, dateformat@^1.0.7-1.2.3: - version "1.0.12" - resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" - dependencies: - get-stdin "^4.0.1" - meow "^3.3.0" - -dateformat@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062" - -debounce-hashed@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/debounce-hashed/-/debounce-hashed-0.1.2.tgz#a0dfe307c1a7db30f6911c8cfbc0ef841f37d4af" - -debounce@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.1.0.tgz#6a1a4ee2a9dc4b7c24bb012558dbcdb05b37f408" - -debug-fabulous@1.X: - version "1.0.0" - resolved "https://registry.yarnpkg.com/debug-fabulous/-/debug-fabulous-1.0.0.tgz#57f6648646097b1b0849dcda0017362c1ec00f8b" - dependencies: - debug "3.X" - memoizee "0.4.X" - object-assign "4.X" - -debug@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" - dependencies: - ms "0.7.1" - -debug@3.1.0, debug@3.X: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - dependencies: - ms "2.0.0" - -debug@^2.2.0, debug@^2.3.3: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - dependencies: - ms "2.0.0" - -decache@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/decache/-/decache-4.3.0.tgz#a395e407095698ac8a6def01f2a6a4cb7638f635" - dependencies: - callsite "^1.0.0" - -decamelize@^1.0.0, decamelize@^1.1.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - -deep-assign@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/deep-assign/-/deep-assign-1.0.0.tgz#b092743be8427dc621ea0067cdec7e70dd19f37b" - dependencies: - is-obj "^1.0.0" - -deep-eql@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" - dependencies: - type-detect "^4.0.0" - -deep-extend@~0.4.0: - version "0.4.2" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" - -deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - -deepmerge@~0.2.7: - version "0.2.10" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-0.2.10.tgz#8906bf9e525a4fbf1b203b2afcb4640249821219" - -defaults@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" - dependencies: - clone "^1.0.2" - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - dependencies: - is-descriptor "^1.0.0" - -del@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" - dependencies: - globby "^6.1.0" - is-path-cwd "^1.0.0" - is-path-in-cwd "^1.0.0" - p-map "^1.1.1" - pify "^3.0.0" - rimraf "^2.2.8" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - -deprecated@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/deprecated/-/deprecated-0.0.1.tgz#f9c9af5464afa1e7a971458a8bdef2aa94d5bb19" - -detect-file@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" - -detect-indent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-2.0.0.tgz#720ff51e4d97b76884f6bf57292348b13dfde939" - dependencies: - get-stdin "^3.0.0" - minimist "^1.1.0" - repeating "^1.1.0" - -detect-libc@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - -diagnostic-channel-publishers@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3" - -diagnostic-channel@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17" - dependencies: - semver "^5.3.0" - -detect-newline@2.X: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" - -diff-match-patch@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.0.tgz#1cc3c83a490d67f95d91e39f6ad1f2e086b63048" - -diff@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" - -diff@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" - -diff@^3.1.0, diff@^3.2.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" - -doctrine@^0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-0.7.2.tgz#7cb860359ba3be90e040b26b729ce4bfa654c523" - dependencies: - esutils "^1.1.6" - isarray "0.0.1" - -duplexer2@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.0.2.tgz#c614dcf67e2fb14995a91711e5a617e8a60a31db" - dependencies: - readable-stream "~1.1.9" - -duplexer@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" - -duplexify@^3.2.0: - version "3.5.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.1.tgz#4e1516be68838bc90a49994f0b39a6e5960befcd" - dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" - -ecc-jsbn@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" - dependencies: - jsbn "~0.1.0" - -editorconfig@^0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.0.tgz#b6dd4a0b6b9e76ce48e066bdc15381aebb8804fd" - dependencies: - "@types/commander" "^2.11.0" - "@types/semver" "^5.4.0" - commander "^2.11.0" - lru-cache "^4.1.1" - semver "^5.4.1" - sigmund "^1.0.1" - -end-of-stream@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.0.tgz#7a90d833efda6cfa6eac0f4949dbb0fad3a63206" - dependencies: - once "^1.4.0" - -end-of-stream@~0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-0.1.5.tgz#8e177206c3c80837d85632e8b9359dfe8b2f6eaf" - dependencies: - once "~1.3.0" - -error-ex@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" - dependencies: - is-arrayish "^0.2.1" - -es5-ext@^0.10.14, es5-ext@^0.10.30, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14, es5-ext@~0.10.2: - version "0.10.40" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.40.tgz#ab3d2179b943008c5e9ef241beb25ef41424c774" - dependencies: - es6-iterator "~2.0.3" - es6-symbol "~3.1.1" - -es6-iterator@^2.0.1, es6-iterator@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" - dependencies: - d "1" - es5-ext "^0.10.35" - es6-symbol "^3.1.1" - -es6-symbol@^3.1.1, es6-symbol@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" - dependencies: - d "1" - es5-ext "~0.10.14" - -es6-weak-map@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" - dependencies: - d "1" - es5-ext "^0.10.14" - es6-iterator "^2.0.1" - es6-symbol "^3.1.1" - -escape-string-regexp@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz#4dbc2fe674e71949caf3fb2695ce7f2dc1d9a8d1" - -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - -escodegen@1.8.x: - version "1.8.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" - dependencies: - esprima "^2.7.1" - estraverse "^1.9.1" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.2.0" - -esprima@2.7.x, esprima@^2.7.1: - version "2.7.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" - -esprima@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" - -estraverse@^1.9.1: - version "1.9.3" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" - -esutils@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-1.1.6.tgz#c01ccaa9ae4b897c6d0c3e210ae52f3c7a844375" - -esutils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" - -event-emitter@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" - dependencies: - d "1" - es5-ext "~0.10.14" - -event-stream@^3.3.1, event-stream@^3.3.4, event-stream@~3.3.4: - version "3.3.4" - resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" - dependencies: - duplexer "~0.1.1" - from "~0" - map-stream "~0.1.0" - pause-stream "0.0.11" - split "0.3" - stream-combiner "~0.0.4" - through "~2.3.1" - -expand-brackets@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" - dependencies: - is-posix-bracket "^0.1.0" - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -expand-range@^1.8.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" - dependencies: - fill-range "^2.1.0" - -expand-tilde@^2.0.0, expand-tilde@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" - dependencies: - homedir-polyfill "^1.0.1" - -extend-shallow@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-1.1.4.tgz#19d6bf94dfc09d76ba711f39b872d21ff4dd9071" - dependencies: - kind-of "^1.1.0" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extend@^3.0.0, extend@~3.0.0, extend@~3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" - -extend@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/extend/-/extend-1.2.1.tgz#a0f5fd6cfc83a5fe49ef698d60ec8a624dd4576c" - -extglob@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" - dependencies: - is-extglob "^1.0.0" - -extglob@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.3.tgz#55e019d0c95bf873949c737b7e5172dba84ebb29" - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - -fancy-log@^1.1.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.2.tgz#f41125e3d84f2e7d89a43d06d958c8f78be16be1" - dependencies: - ansi-gray "^0.1.1" - color-support "^1.1.3" - time-stamp "^1.0.0" - -fast-deep-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" - -fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - -fast-levenshtein@~2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - -fd-slicer@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" - dependencies: - pend "~1.2.0" - -filename-regex@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" - -fill-range@^2.1.0: - version "2.2.3" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" - dependencies: - is-number "^2.1.0" - isobject "^2.0.0" - randomatic "^1.1.3" - repeat-element "^1.1.2" - repeat-string "^1.5.2" - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -find-index@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4" - -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - -findup-sync@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" - dependencies: - detect-file "^1.0.0" - is-glob "^3.1.0" - micromatch "^3.0.4" - resolve-dir "^1.0.1" - -fined@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fined/-/fined-1.1.0.tgz#b37dc844b76a2f5e7081e884f7c0ae344f153476" - dependencies: - expand-tilde "^2.0.2" - is-plain-object "^2.0.3" - object.defaults "^1.1.0" - object.pick "^1.2.0" - parse-filepath "^1.0.1" - -first-chunk-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz#59bfb50cd905f60d7c394cd3d9acaab4e6ad934e" - -first-chunk-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz#1bdecdb8e083c0664b91945581577a43a9f31d70" - dependencies: - readable-stream "^2.0.2" - -flagged-respawn@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.0.tgz#4e79ae9b2eb38bf86b3bb56bf3e0a56aa5fcabd7" - -for-in@^1.0.1, for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - -for-own@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" - dependencies: - for-in "^1.0.1" - -for-own@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" - dependencies: - for-in "^1.0.1" - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - -form-data@~2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.5" - mime-types "^2.1.12" - -form-data@~2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.5" - mime-types "^2.1.12" - -formatio@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb" - dependencies: - samsam "1.x" - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - dependencies: - map-cache "^0.2.2" - -from@~0: - version "0.1.7" - resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" - -fs-extra@^4.0.2: - version "4.0.3" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - -fsevents@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8" - dependencies: - nan "^2.3.0" - node-pre-gyp "^0.6.39" - -fstream-ignore@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" - dependencies: - fstream "^1.0.0" - inherits "2" - minimatch "^3.0.0" - -fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: - version "1.0.11" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" - dependencies: - graceful-fs "^4.1.2" - inherits "~2.0.0" - mkdirp ">=0.5 0" - rimraf "2" - -fuzzy@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/fuzzy/-/fuzzy-0.1.3.tgz#4c76ec2ff0ac1a36a9dccf9a00df8623078d4ed8" - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -gaze@^0.5.1: - version "0.5.2" - resolved "https://registry.yarnpkg.com/gaze/-/gaze-0.5.2.tgz#40b709537d24d1d45767db5a908689dfe69ac44f" - dependencies: - globule "~0.1.0" - -generate-function@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" - -generate-object-property@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" - dependencies: - is-property "^1.0.0" - -get-func-name@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" - -get-port@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc" - -get-stdin@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-3.0.2.tgz#c1ced24b9039b38ded85bdf161e57713b6dd4abe" - -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - dependencies: - assert-plus "^1.0.0" - -glob-base@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" - dependencies: - glob-parent "^2.0.0" - is-glob "^2.0.0" - -glob-parent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" - dependencies: - is-glob "^2.0.0" - -glob-parent@^3.0.0, glob-parent@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-stream@^3.1.5: - version "3.1.18" - resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-3.1.18.tgz#9170a5f12b790306fdfe598f313f8f7954fd143b" - dependencies: - glob "^4.3.1" - glob2base "^0.0.12" - minimatch "^2.0.1" - ordered-read-streams "^0.1.0" - through2 "^0.6.1" - unique-stream "^1.0.0" - -glob-stream@^5.3.2: - version "5.3.5" - resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-5.3.5.tgz#a55665a9a8ccdc41915a87c701e32d4e016fad22" - dependencies: - extend "^3.0.0" - glob "^5.0.3" - glob-parent "^3.0.0" - micromatch "^2.3.7" - ordered-read-streams "^0.3.0" - through2 "^0.6.0" - to-absolute-glob "^0.1.1" - unique-stream "^2.0.2" - -glob-watcher@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-0.0.6.tgz#b95b4a8df74b39c83298b0c05c978b4d9a3b710b" - dependencies: - gaze "^0.5.1" - -glob2base@^0.0.12: - version "0.0.12" - resolved "https://registry.yarnpkg.com/glob2base/-/glob2base-0.0.12.tgz#9d419b3e28f12e83a362164a277055922c9c0d56" - dependencies: - find-index "^0.1.1" - -glob@3.2.11: - version "3.2.11" - resolved "https://registry.yarnpkg.com/glob/-/glob-3.2.11.tgz#4a973f635b9190f715d10987d5c00fd2815ebe3d" - dependencies: - inherits "2" - minimatch "0.3" - -glob@7.1.2, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^4.3.1: - version "4.5.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-4.5.3.tgz#c6cb73d3226c1efef04de3c56d012f03377ee15f" - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "^2.0.1" - once "^1.3.0" - -glob@^5.0.15, glob@^5.0.3: - version "5.0.15" - resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@~3.1.21: - version "3.1.21" - resolved "https://registry.yarnpkg.com/glob/-/glob-3.1.21.tgz#d29e0a055dea5138f4d07ed40e8982e83c2066cd" - dependencies: - graceful-fs "~1.2.0" - inherits "1" - minimatch "~0.2.11" - -global-modules@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" - dependencies: - global-prefix "^1.0.1" - is-windows "^1.0.1" - resolve-dir "^1.0.0" - -global-prefix@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" - dependencies: - expand-tilde "^2.0.2" - homedir-polyfill "^1.0.1" - ini "^1.3.4" - is-windows "^1.0.1" - which "^1.2.14" - -globby@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" - dependencies: - array-union "^1.0.1" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -globule@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/globule/-/globule-0.1.0.tgz#d9c8edde1da79d125a151b79533b978676346ae5" - dependencies: - glob "~3.1.21" - lodash "~1.0.1" - minimatch "~0.2.11" - -glogg@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.0.tgz#7fe0f199f57ac906cf512feead8f90ee4a284fc5" - dependencies: - sparkles "^1.0.0" - -graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.2, graceful-fs@^4.1.6: - version "4.1.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" - -graceful-fs@^3.0.0: - version "3.0.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-3.0.11.tgz#7613c778a1afea62f25c630a086d7f3acbbdd818" - dependencies: - natives "^1.1.0" - -graceful-fs@~1.2.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-1.2.3.tgz#15a4806a57547cb2d2dbf27f42e89a8c3451b364" - -growl@1.10.3: - version "1.10.3" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" - -growl@1.9.2: - version "1.9.2" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" - -gulp-chmod@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/gulp-chmod/-/gulp-chmod-2.0.0.tgz#00c390b928a0799b251accf631aa09e01cc6299c" - dependencies: - deep-assign "^1.0.0" - stat-mode "^0.2.0" - through2 "^2.0.0" - -gulp-debounced-watch@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/gulp-debounced-watch/-/gulp-debounced-watch-1.0.4.tgz#5a47d4e24ce46365ce82ecac32a2a303e432b12a" - dependencies: - debounce-hashed "^0.1.1" - gulp-watch "^4.3.4" - object-assign "^3.0.0" - -gulp-filter@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/gulp-filter/-/gulp-filter-5.1.0.tgz#a05e11affb07cf7dcf41a7de1cb7b63ac3783e73" - dependencies: - multimatch "^2.0.0" - plugin-error "^0.1.2" - streamfilter "^1.0.5" - -gulp-gitmodified@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/gulp-gitmodified/-/gulp-gitmodified-1.1.1.tgz#85f3675915c1d51b66807f28de0ebb591e7e9df4" - dependencies: - gulp-util "~2.2.12" - lodash.find "^3.2.1" - through2 "^2.0.0" - vinyl "^0.4.3" - which "~1.0.5" - -gulp-gunzip@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/gulp-gunzip/-/gulp-gunzip-1.0.0.tgz#15b741145e83a9c6f50886241b57cc5871f151a9" - dependencies: - through2 "~0.6.5" - vinyl "~0.4.6" - -gulp-json-editor@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/gulp-json-editor/-/gulp-json-editor-2.2.1.tgz#7c4dd7477e8d06dc5dc49c0b81e745cdb04f97bb" - dependencies: - deepmerge "~0.2.7" - detect-indent "^2.0.0" - gulp-util "~3.0.0" - js-beautify "~1.5.4" - through2 "~0.5.0" - -gulp-remote-src@^0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/gulp-remote-src/-/gulp-remote-src-0.4.3.tgz#5728cfd643433dd4845ddef0969f0f971a2ab4a1" - dependencies: - event-stream "~3.3.4" - node.extend "~1.1.2" - request "~2.79.0" - through2 "~2.0.3" - vinyl "~2.0.1" - -gulp-sourcemaps@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz#b86ff349d801ceb56e1d9e7dc7bbcb4b7dee600c" - dependencies: - convert-source-map "^1.1.1" - graceful-fs "^4.1.2" - strip-bom "^2.0.0" - through2 "^2.0.0" - vinyl "^1.0.0" - -gulp-sourcemaps@^2.6.4: - version "2.6.4" - resolved "https://registry.yarnpkg.com/gulp-sourcemaps/-/gulp-sourcemaps-2.6.4.tgz#cbb2008450b1bcce6cd23bf98337be751bf6e30a" - dependencies: - "@gulp-sourcemaps/identity-map" "1.X" - "@gulp-sourcemaps/map-sources" "1.X" - acorn "5.X" - convert-source-map "1.X" - css "2.X" - debug-fabulous "1.X" - detect-newline "2.X" - graceful-fs "4.X" - source-map "~0.6.0" - strip-bom-string "1.X" - through2 "2.X" - -gulp-symdest@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/gulp-symdest/-/gulp-symdest-1.1.0.tgz#c165320732d192ce56fd94271ffa123234bf2ae0" - dependencies: - event-stream "^3.3.1" - mkdirp "^0.5.1" - queue "^3.1.0" - vinyl-fs "^2.4.3" - -gulp-typescript@^3.2.2: - version "3.2.3" - resolved "https://registry.yarnpkg.com/gulp-typescript/-/gulp-typescript-3.2.3.tgz#32d52ab97b97c4ce070c0419db08ea3af514d720" - dependencies: - gulp-util "~3.0.7" - source-map "~0.5.3" - through2 "~2.0.1" - vinyl-fs "~2.4.3" - -gulp-untar@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/gulp-untar/-/gulp-untar-0.0.6.tgz#d6bdefde7e9a8e054c9f162385a0782c4be74000" - dependencies: - event-stream "~3.3.4" - gulp-util "~3.0.8" - streamifier "~0.1.1" - tar "^2.2.1" - through2 "~2.0.3" - -gulp-util@3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.7.tgz#78925c4b8f8b49005ac01a011c557e6218941cbb" - dependencies: - array-differ "^1.0.0" - array-uniq "^1.0.2" - beeper "^1.0.0" - chalk "^1.0.0" - dateformat "^1.0.11" - fancy-log "^1.1.0" - gulplog "^1.0.0" - has-gulplog "^0.1.0" - lodash._reescape "^3.0.0" - lodash._reevaluate "^3.0.0" - lodash._reinterpolate "^3.0.0" - lodash.template "^3.0.0" - minimist "^1.1.0" - multipipe "^0.1.2" - object-assign "^3.0.0" - replace-ext "0.0.1" - through2 "^2.0.0" - vinyl "^0.5.0" - -gulp-util@^3.0.0, gulp-util@^3.0.7, gulp-util@~3.0.0, gulp-util@~3.0.7, gulp-util@~3.0.8: - version "3.0.8" - resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" - dependencies: - array-differ "^1.0.0" - array-uniq "^1.0.2" - beeper "^1.0.0" - chalk "^1.0.0" - dateformat "^2.0.0" - fancy-log "^1.1.0" - gulplog "^1.0.0" - has-gulplog "^0.1.0" - lodash._reescape "^3.0.0" - lodash._reevaluate "^3.0.0" - lodash._reinterpolate "^3.0.0" - lodash.template "^3.0.0" - minimist "^1.1.0" - multipipe "^0.1.2" - object-assign "^3.0.0" - replace-ext "0.0.1" - through2 "^2.0.0" - vinyl "^0.5.0" - -gulp-util@~2.2.12: - version "2.2.20" - resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-2.2.20.tgz#d7146e5728910bd8f047a6b0b1e549bc22dbd64c" - dependencies: - chalk "^0.5.0" - dateformat "^1.0.7-1.2.3" - lodash._reinterpolate "^2.4.1" - lodash.template "^2.4.1" - minimist "^0.2.0" - multipipe "^0.1.0" - through2 "^0.5.0" - vinyl "^0.2.1" - -gulp-vinyl-zip@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/gulp-vinyl-zip/-/gulp-vinyl-zip-2.1.0.tgz#24e40685dc05b7149995245099e0590263be8dad" - dependencies: - event-stream "^3.3.1" - queue "^4.2.1" - through2 "^2.0.3" - vinyl "^2.0.2" - vinyl-fs "^2.0.0" - yauzl "^2.2.1" - yazl "^2.2.1" - -gulp-watch@^4.3.11, gulp-watch@^4.3.4: - version "4.3.11" - resolved "https://registry.yarnpkg.com/gulp-watch/-/gulp-watch-4.3.11.tgz#162fc563de9fc770e91f9a7ce3955513a9a118c0" - dependencies: - anymatch "^1.3.0" - chokidar "^1.6.1" - glob-parent "^3.0.1" - gulp-util "^3.0.7" - object-assign "^4.1.0" - path-is-absolute "^1.0.1" - readable-stream "^2.2.2" - slash "^1.0.0" - vinyl "^1.2.0" - vinyl-file "^2.0.0" - -gulp@^3.9.1: - version "3.9.1" - resolved "https://registry.yarnpkg.com/gulp/-/gulp-3.9.1.tgz#571ce45928dd40af6514fc4011866016c13845b4" - dependencies: - archy "^1.0.0" - chalk "^1.0.0" - deprecated "^0.0.1" - gulp-util "^3.0.0" - interpret "^1.0.0" - liftoff "^2.1.0" - minimist "^1.1.0" - orchestrator "^0.3.0" - pretty-hrtime "^1.0.0" - semver "^4.1.0" - tildify "^1.0.0" - v8flags "^2.0.2" - vinyl-fs "^0.3.0" - -gulplog@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/gulplog/-/gulplog-1.0.0.tgz#e28c4d45d05ecbbed818363ce8f9c5926229ffe5" - dependencies: - glogg "^1.0.0" - -handlebars@^4.0.1: - version "4.0.11" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc" - dependencies: - async "^1.4.0" - optimist "^0.6.1" - source-map "^0.4.4" - optionalDependencies: - uglify-js "^2.6" - -har-schema@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - -har-validator@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" - dependencies: - chalk "^1.1.1" - commander "^2.9.0" - is-my-json-valid "^2.12.4" - pinkie-promise "^2.0.0" - -har-validator@~4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" - dependencies: - ajv "^4.9.1" - har-schema "^1.0.5" - -har-validator@~5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" - dependencies: - ajv "^5.1.0" - har-schema "^2.0.0" - -has-ansi@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e" - dependencies: - ansi-regex "^0.2.0" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - dependencies: - ansi-regex "^2.0.0" - -has-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" - -has-flag@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" - -has-gulplog@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/has-gulplog/-/has-gulplog-0.1.0.tgz#6414c82913697da51590397dafb12f22967811ce" - dependencies: - sparkles "^1.0.0" - -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -hash-base@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -hawk@3.1.3, hawk@~3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" - dependencies: - boom "2.x.x" - cryptiles "2.x.x" - hoek "2.x.x" - sntp "1.x.x" - -hawk@~6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" - dependencies: - boom "4.x.x" - cryptiles "3.x.x" - hoek "4.x.x" - sntp "2.x.x" - -he@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" - -hoek@2.x.x: - version "2.16.3" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" - -hoek@4.x.x: - version "4.2.0" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" - -homedir-polyfill@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" - dependencies: - parse-passwd "^1.0.0" - -hosted-git-info@^2.1.4: - version "2.5.0" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" - -http-signature@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" - dependencies: - assert-plus "^0.2.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -husky@^0.14.3: - version "0.14.3" - resolved "https://registry.yarnpkg.com/husky/-/husky-0.14.3.tgz#c69ed74e2d2779769a17ba8399b54ce0b63c12c3" - dependencies: - is-ci "^1.0.10" - normalize-path "^1.0.0" - strip-indent "^2.0.0" - -iconv-lite@^0.4.19: - version "0.4.19" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" - -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - dependencies: - repeating "^2.0.0" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-1.0.2.tgz#ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b" - -inherits@2, inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - -ini@^1.3.4, ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - -interpret@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" - -inversify@^4.5.2: - version "4.9.0" - resolved "https://registry.yarnpkg.com/inversify/-/inversify-4.9.0.tgz#caa01f5856cfa0499aaaed9b1a635ef0c4a9f4d3" - -is-absolute@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" - dependencies: - is-relative "^1.0.0" - is-windows "^1.0.1" - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - dependencies: - kind-of "^6.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - dependencies: - binary-extensions "^1.0.0" - -is-buffer@^1.1.5, is-buffer@~1.1.1: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - -is-builtin-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" - dependencies: - builtin-modules "^1.0.0" - -is-ci@^1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.0.10.tgz#f739336b2632365061a9d48270cd56ae3369318e" - dependencies: - ci-info "^1.0.0" - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - dependencies: - kind-of "^6.0.0" - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-dotfile@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" - -is-equal-shallow@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" - dependencies: - is-primitive "^2.0.0" - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" - -is-extglob@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - -is-finite@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - dependencies: - number-is-nan "^1.0.0" - -is-glob@^2.0.0, is-glob@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" - dependencies: - is-extglob "^1.0.0" - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - dependencies: - is-extglob "^2.1.0" - -is-my-json-valid@^2.12.4: - version "2.17.1" - resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.1.tgz#3da98914a70a22f0a8563ef1511a246c6fc55471" - dependencies: - generate-function "^2.0.0" - generate-object-property "^1.1.0" - jsonpointer "^4.0.0" - xtend "^4.0.0" - -is-number@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" - dependencies: - kind-of "^3.0.2" - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - dependencies: - kind-of "^3.0.2" - -is-obj@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" - -is-odd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-1.0.0.tgz#3b8a932eb028b3775c39bb09e91767accdb69088" - dependencies: - is-number "^3.0.0" - -is-path-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" - -is-path-in-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" - dependencies: - is-path-inside "^1.0.0" - -is-path-inside@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" - dependencies: - path-is-inside "^1.0.1" - -is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - dependencies: - isobject "^3.0.1" - -is-posix-bracket@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" - -is-primitive@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" - -is-promise@^2.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" - -is-property@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" - -is-relative@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" - dependencies: - is-unc-path "^1.0.0" - -is-running@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-running/-/is-running-2.1.0.tgz#30a73ff5cc3854e4fc25490809e9f5abf8de09e0" - -is-stream@^1.0.1, is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - -is-unc-path@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" - dependencies: - unc-path-regex "^0.1.2" - -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - -is-valid-glob@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-0.3.0.tgz#d4b55c69f51886f9b65c70d6c2622d37e29f48fe" - -is-windows@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.1.tgz#310db70f742d259a16a369202b51af84233310d9" - -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - -is@^3.1.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/is/-/is-3.2.1.tgz#d0ac2ad55eb7b0bec926a5266f6c662aaa83dca5" - -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - -isarray@1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - -istanbul@0.4.5, istanbul@^0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b" - dependencies: - abbrev "1.0.x" - async "1.x" - escodegen "1.8.x" - esprima "2.7.x" - glob "^5.0.15" - handlebars "^4.0.1" - js-yaml "3.x" - mkdirp "0.5.x" - nopt "3.x" - once "1.x" - resolve "1.1.x" - supports-color "^3.1.0" - which "^1.1.1" - wordwrap "^1.0.0" - -jade@0.26.3: - version "0.26.3" - resolved "https://registry.yarnpkg.com/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c" - dependencies: - commander "0.6.1" - mkdirp "0.3.0" - -js-beautify@~1.5.4: - version "1.5.10" - resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.5.10.tgz#4d95371702699344a516ca26bf59f0a27bb75719" - dependencies: - config-chain "~1.1.5" - mkdirp "~0.5.0" - nopt "~3.0.1" - -js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - -js-yaml@3.x: - version "3.10.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - -json-edm-parser@0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/json-edm-parser/-/json-edm-parser-0.1.2.tgz#1e60b0fef1bc0af67bc0d146dfdde5486cd615b4" - dependencies: - jsonparse "~1.2.0" - -json-schema-traverse@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - -json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" - dependencies: - jsonify "~0.0.0" - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - optionalDependencies: - graceful-fs "^4.1.6" - -jsonify@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" - -jsonparse@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" - -jsonparse@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.2.0.tgz#5c0c5685107160e72fe7489bddea0b44c2bc67bd" - -jsonpointer@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" - -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - -kind-of@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-1.1.0.tgz#140a3d2d41a36d2efcfa9377b62c24f8495a5c44" - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.1.0, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0, kind-of@^5.0.2: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" - -lazy-cache@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" - -lazy-cache@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264" - dependencies: - set-getter "^0.1.0" - -lazystream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" - dependencies: - readable-stream "^2.0.5" - -levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - -liftoff@^2.1.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-2.5.0.tgz#2009291bb31cea861bbf10a7c15a28caf75c31ec" - dependencies: - extend "^3.0.0" - findup-sync "^2.0.0" - fined "^1.0.1" - flagged-respawn "^1.0.0" - is-plain-object "^2.0.4" - object.map "^1.0.0" - rechoir "^0.6.2" - resolve "^1.1.7" - -line-by-line@^0.1.5: - version "0.1.6" - resolved "https://registry.yarnpkg.com/line-by-line/-/line-by-line-0.1.6.tgz#6236edd1db2d1695addf11f0268e74a181561c30" - -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - -lodash._basecallback@^3.0.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/lodash._basecallback/-/lodash._basecallback-3.3.1.tgz#b7b2bb43dc2160424a21ccf26c57e443772a8e27" - dependencies: - lodash._baseisequal "^3.0.0" - lodash._bindcallback "^3.0.0" - lodash.isarray "^3.0.0" - lodash.pairs "^3.0.0" - -lodash._basecopy@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" - -lodash._baseeach@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash._baseeach/-/lodash._baseeach-3.0.4.tgz#cf8706572ca144e8d9d75227c990da982f932af3" - dependencies: - lodash.keys "^3.0.0" - -lodash._basefind@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._basefind/-/lodash._basefind-3.0.0.tgz#b2bba05cc645f972de2cf925fa2bf63a0f60c8ae" - -lodash._basefindindex@^3.0.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/lodash._basefindindex/-/lodash._basefindindex-3.6.0.tgz#f083360a1b022418ed81bc899beb312e21e74a4f" - -lodash._baseisequal@^3.0.0: - version "3.0.7" - resolved "https://registry.yarnpkg.com/lodash._baseisequal/-/lodash._baseisequal-3.0.7.tgz#d8025f76339d29342767dcc887ce5cb95a5b51f1" - dependencies: - lodash.isarray "^3.0.0" - lodash.istypedarray "^3.0.0" - lodash.keys "^3.0.0" - -lodash._basetostring@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz#d1861d877f824a52f669832dcaf3ee15566a07d5" - -lodash._basevalues@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz#5b775762802bde3d3297503e26300820fdf661b7" - -lodash._bindcallback@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - -lodash._escapehtmlchar@~2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/lodash._escapehtmlchar/-/lodash._escapehtmlchar-2.4.1.tgz#df67c3bb6b7e8e1e831ab48bfa0795b92afe899d" - dependencies: - lodash._htmlescapes "~2.4.1" - -lodash._escapestringchar@~2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/lodash._escapestringchar/-/lodash._escapestringchar-2.4.1.tgz#ecfe22618a2ade50bfeea43937e51df66f0edb72" - -lodash._getnative@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" - -lodash._htmlescapes@~2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/lodash._htmlescapes/-/lodash._htmlescapes-2.4.1.tgz#32d14bf0844b6de6f8b62a051b4f67c228b624cb" - -lodash._isiterateecall@^3.0.0: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" - -lodash._isnative@~2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/lodash._isnative/-/lodash._isnative-2.4.1.tgz#3ea6404b784a7be836c7b57580e1cdf79b14832c" - -lodash._objecttypes@~2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz#7c0b7f69d98a1f76529f890b0cdb1b4dfec11c11" - -lodash._reescape@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reescape/-/lodash._reescape-3.0.0.tgz#2b1d6f5dfe07c8a355753e5f27fac7f1cde1616a" - -lodash._reevaluate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz#58bc74c40664953ae0b124d806996daca431e2ed" - -lodash._reinterpolate@^2.4.1, lodash._reinterpolate@~2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-2.4.1.tgz#4f1227aa5a8711fc632f5b07a1f4607aab8b3222" - -lodash._reinterpolate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" - -lodash._reunescapedhtml@~2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/lodash._reunescapedhtml/-/lodash._reunescapedhtml-2.4.1.tgz#747c4fc40103eb3bb8a0976e571f7a2659e93ba7" - dependencies: - lodash._htmlescapes "~2.4.1" - lodash.keys "~2.4.1" - -lodash._root@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" - -lodash._shimkeys@~2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/lodash._shimkeys/-/lodash._shimkeys-2.4.1.tgz#6e9cc9666ff081f0b5a6c978b83e242e6949d203" - dependencies: - lodash._objecttypes "~2.4.1" - -lodash.defaults@~2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-2.4.1.tgz#a7e8885f05e68851144b6e12a8f3678026bc4c54" - dependencies: - lodash._objecttypes "~2.4.1" - lodash.keys "~2.4.1" - -lodash.escape@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-3.2.0.tgz#995ee0dc18c1b48cc92effae71a10aab5b487698" - dependencies: - lodash._root "^3.0.0" - -lodash.escape@~2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-2.4.1.tgz#2ce12c5e084db0a57dda5e5d1eeeb9f5d175a3b4" - dependencies: - lodash._escapehtmlchar "~2.4.1" - lodash._reunescapedhtml "~2.4.1" - lodash.keys "~2.4.1" - -lodash.find@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-3.2.1.tgz#046e319f3ace912ac6c9246c7f683c5ec07b36ad" - dependencies: - lodash._basecallback "^3.0.0" - lodash._baseeach "^3.0.0" - lodash._basefind "^3.0.0" - lodash._basefindindex "^3.0.0" - lodash.isarray "^3.0.0" - lodash.keys "^3.0.0" - -lodash.isarguments@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - -lodash.isarray@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" - -lodash.isequal@^4.0.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" - -lodash.isobject@~2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-2.4.1.tgz#5a2e47fe69953f1ee631a7eba1fe64d2d06558f5" - dependencies: - lodash._objecttypes "~2.4.1" - -lodash.istypedarray@^3.0.0: - version "3.0.6" - resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" - -lodash.keys@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" - dependencies: - lodash._getnative "^3.0.0" - lodash.isarguments "^3.0.0" - lodash.isarray "^3.0.0" - -lodash.keys@~2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-2.4.1.tgz#48dea46df8ff7632b10d706b8acb26591e2b3727" - dependencies: - lodash._isnative "~2.4.1" - lodash._shimkeys "~2.4.1" - lodash.isobject "~2.4.1" - -lodash.pairs@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash.pairs/-/lodash.pairs-3.0.1.tgz#bbe08d5786eeeaa09a15c91ebf0dcb7d2be326a9" - dependencies: - lodash.keys "^3.0.0" - -lodash.restparam@^3.0.0: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - -lodash.template@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-2.4.1.tgz#9e611007edf629129a974ab3c48b817b3e1cf20d" - dependencies: - lodash._escapestringchar "~2.4.1" - lodash._reinterpolate "~2.4.1" - lodash.defaults "~2.4.1" - lodash.escape "~2.4.1" - lodash.keys "~2.4.1" - lodash.templatesettings "~2.4.1" - lodash.values "~2.4.1" - -lodash.template@^3.0.0: - version "3.6.2" - resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-3.6.2.tgz#f8cdecc6169a255be9098ae8b0c53d378931d14f" - dependencies: - lodash._basecopy "^3.0.0" - lodash._basetostring "^3.0.0" - lodash._basevalues "^3.0.0" - lodash._isiterateecall "^3.0.0" - lodash._reinterpolate "^3.0.0" - lodash.escape "^3.0.0" - lodash.keys "^3.0.0" - lodash.restparam "^3.0.0" - lodash.templatesettings "^3.0.0" - -lodash.templatesettings@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5" - dependencies: - lodash._reinterpolate "^3.0.0" - lodash.escape "^3.0.0" - -lodash.templatesettings@~2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-2.4.1.tgz#ea76c75d11eb86d4dbe89a83893bb861929ac699" - dependencies: - lodash._reinterpolate "~2.4.1" - lodash.escape "~2.4.1" - -lodash.values@~2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-2.4.1.tgz#abf514436b3cb705001627978cbcf30b1280eea4" - dependencies: - lodash.keys "~2.4.1" - -lodash@^4.17.4: - version "4.17.4" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" - -lodash@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-1.0.2.tgz#8f57560c83b59fc270bd3d561b690043430e2551" - -lolex@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6" - -longest@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" - -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - -lru-cache@2: - version "2.7.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" - -lru-cache@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -lru-queue@0.1: - version "0.1.0" - resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" - dependencies: - es5-ext "~0.10.2" - -make-iterator@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.0.tgz#57bef5dc85d23923ba23767324d8e8f8f3d9694b" - dependencies: - kind-of "^3.1.0" - -map-cache@^0.2.0, map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - -map-obj@^1.0.0, map-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - -map-stream@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - dependencies: - object-visit "^1.0.0" - -md5.js@1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -md5@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" - dependencies: - charenc "~0.0.1" - crypt "~0.0.1" - is-buffer "~1.1.1" - -memoizee@0.4.X: - version "0.4.12" - resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.12.tgz#780e99a219c50c549be6d0fc61765080975c58fb" - dependencies: - d "1" - es5-ext "^0.10.30" - es6-weak-map "^2.0.2" - event-emitter "^0.3.5" - is-promise "^2.1" - lru-queue "0.1" - next-tick "1" - timers-ext "^0.1.2" - -meow@^3.3.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" - -merge-stream@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1" - dependencies: - readable-stream "^2.0.1" - -micromatch@^2.1.5, micromatch@^2.3.7: - version "2.3.11" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" - dependencies: - arr-diff "^2.0.0" - array-unique "^0.2.1" - braces "^1.8.2" - expand-brackets "^0.1.4" - extglob "^0.3.1" - filename-regex "^2.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.1" - kind-of "^3.0.2" - normalize-path "^2.0.1" - object.omit "^2.0.0" - parse-glob "^3.0.4" - regex-cache "^0.4.2" - -micromatch@^3.0.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.4.tgz#bb812e741a41f982c854e42b421a7eac458796f4" - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.0" - define-property "^1.0.0" - extend-shallow "^2.0.1" - extglob "^2.0.2" - fragment-cache "^0.2.1" - kind-of "^6.0.0" - nanomatch "^1.2.5" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -mime-db@~1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" - -mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.7: - version "2.1.17" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" - dependencies: - mime-db "~1.30.0" - -minimatch@0.3: - version "0.3.0" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd" - dependencies: - lru-cache "2" - sigmund "~1.0.0" - -"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - dependencies: - brace-expansion "^1.1.7" - -minimatch@^2.0.1: - version "2.0.10" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-2.0.10.tgz#8d087c39c6b38c001b97fca7ce6d0e1e80afbac7" - dependencies: - brace-expansion "^1.0.0" - -minimatch@~0.2.11: - version "0.2.14" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.2.14.tgz#c74e780574f63c6f9a090e90efbe6ef53a6a756a" - dependencies: - lru-cache "2" - sigmund "~1.0.0" - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - -minimist@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.2.0.tgz#4dffe525dae2b864c66c2e23c6271d7afdecefce" - -minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - -mixin-deep@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.0.tgz#47a8732ba97799457c8c1eca28f95132d7e8150a" - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" - -mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - dependencies: - minimist "0.0.8" - -mocha@^2.3.3: - version "2.5.3" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58" - dependencies: - commander "2.3.0" - debug "2.2.0" - diff "1.4.0" - escape-string-regexp "1.0.2" - glob "3.2.11" - growl "1.9.2" - jade "0.26.3" - mkdirp "0.5.1" - supports-color "1.2.0" - to-iso-string "0.0.2" - -mocha@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.1.0.tgz#7d86cfbcf35cb829e2754c32e17355ec05338794" - dependencies: - browser-stdout "1.3.0" - commander "2.11.0" - debug "3.1.0" - diff "3.3.1" - escape-string-regexp "1.0.5" - glob "7.1.2" - growl "1.10.3" - he "1.1.1" - mkdirp "0.5.1" - supports-color "4.4.0" - -ms@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - -multimatch@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b" - dependencies: - array-differ "^1.0.0" - array-union "^1.0.1" - arrify "^1.0.0" - minimatch "^3.0.0" - -multipipe@^0.1.0, multipipe@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-0.1.2.tgz#2a8f2ddf70eed564dff2d57f1e1a137d9f05078b" - dependencies: - duplexer2 "0.0.2" - -named-js-regexp@^1.3.1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/named-js-regexp/-/named-js-regexp-1.3.3.tgz#a2eb1655c74cb82213a4fc82777dfb67b895d8c8" - -nan@^2.3.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" - -nanomatch@^1.2.5: - version "1.2.6" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.6.tgz#f27233e97c34a8706b7e781a4bc611c957a81625" - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^1.0.0" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - is-odd "^1.0.0" - kind-of "^5.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -native-promise-only@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11" - -natives@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.1.tgz#011acce1f7cbd87f7ba6b3093d6cd9392be1c574" - -next-tick@1: - version "1.0.0" - resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" - -node-pre-gyp@^0.6.39: - version "0.6.39" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" - dependencies: - detect-libc "^1.0.2" - hawk "3.1.3" - mkdirp "^0.5.1" - nopt "^4.0.1" - npmlog "^4.0.2" - rc "^1.1.7" - request "2.81.0" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^2.2.1" - tar-pack "^3.4.0" - -node.extend@~1.1.2: - version "1.1.6" - resolved "https://registry.yarnpkg.com/node.extend/-/node.extend-1.1.6.tgz#a7b882c82d6c93a4863a5504bd5de8ec86258b96" - dependencies: - is "^3.1.0" - -nopt@3.x, nopt@~3.0.1: - version "3.0.6" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" - dependencies: - abbrev "1" - -nopt@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" - dependencies: - abbrev "1" - osenv "^0.1.4" - -normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: - version "2.4.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" - dependencies: - hosted-git-info "^2.1.4" - is-builtin-module "^1.0.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-path@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379" - -normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - dependencies: - remove-trailing-separator "^1.0.1" - -npmlog@^4.0.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - -oauth-sign@~0.8.1, oauth-sign@~0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" - -object-assign@4.X, object-assign@^4.0.0, object-assign@^4.0.1, object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - -object-assign@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - dependencies: - isobject "^3.0.0" - -object.defaults@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object.defaults/-/object.defaults-1.1.0.tgz#3a7f868334b407dea06da16d88d5cd29e435fecf" - dependencies: - array-each "^1.0.1" - array-slice "^1.0.0" - for-own "^1.0.0" - isobject "^3.0.0" - -object.map@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object.map/-/object.map-1.0.1.tgz#cf83e59dc8fcc0ad5f4250e1f78b3b81bd801d37" - dependencies: - for-own "^1.0.0" - make-iterator "^1.0.0" - -object.omit@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" - dependencies: - for-own "^0.1.4" - is-extendable "^0.1.1" - -object.pick@^1.2.0, object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - dependencies: - isobject "^3.0.1" - -once@1.x, once@^1.3.0, once@^1.3.3, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - dependencies: - wrappy "1" - -once@~1.3.0: - version "1.3.3" - resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20" - dependencies: - wrappy "1" - -opn@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/opn/-/opn-5.2.0.tgz#71fdf934d6827d676cecbea1531f95d354641225" - dependencies: - is-wsl "^1.1.0" - -optimist@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - -optionator@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.4" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - wordwrap "~1.0.0" - -orchestrator@^0.3.0: - version "0.3.8" - resolved "https://registry.yarnpkg.com/orchestrator/-/orchestrator-0.3.8.tgz#14e7e9e2764f7315fbac184e506c7aa6df94ad7e" - dependencies: - end-of-stream "~0.1.5" - sequencify "~0.0.7" - stream-consume "~0.1.0" - -ordered-read-streams@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.1.0.tgz#fd565a9af8eb4473ba69b6ed8a34352cb552f126" - -ordered-read-streams@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz#7137e69b3298bb342247a1bbee3881c80e2fd78b" - dependencies: - is-stream "^1.0.1" - readable-stream "^2.0.1" - -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - -os-tmpdir@^1.0.0, os-tmpdir@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - -osenv@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - -p-map@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" - -parse-filepath@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" - dependencies: - is-absolute "^1.0.0" - map-cache "^0.2.0" - path-root "^0.1.1" - -parse-glob@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" - dependencies: - glob-base "^0.3.0" - is-dotfile "^1.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.0" - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - dependencies: - error-ex "^1.2.0" - -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - dependencies: - pinkie-promise "^2.0.0" - -path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - -path-is-inside@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - -path-parse@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" - -path-root-regex@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" - -path-root@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7" - dependencies: - path-root-regex "^0.1.0" - -path-to-regexp@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" - dependencies: - isarray "0.0.1" - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -pathval@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" - -pause-stream@0.0.11: - version "0.0.11" - resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" - dependencies: - through "~2.3" - -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - -pidusage@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pidusage/-/pidusage-1.2.0.tgz#65ee96ace4e08a4cd3f9240996c85b367171ee92" - -pify@^2.0.0, pify@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - -plugin-error@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-0.1.2.tgz#3b9bb3335ccf00f425e07437e19276967da47ace" - dependencies: - ansi-cyan "^0.1.1" - ansi-red "^0.1.1" - arr-diff "^1.0.1" - arr-union "^2.0.1" - extend-shallow "^1.1.2" - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - -postinstall-build@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postinstall-build/-/postinstall-build-5.0.1.tgz#b917a9079b26178d9a24af5a5cd8cb4a991d11b9" - -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - -preserve@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" - -pretty-hrtime@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" - -process-nextick-args@^1.0.6, process-nextick-args@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" - -proto-list@~1.2.1: - version "1.2.4" - resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" - -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - -qs@~6.3.0: - version "6.3.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" - -qs@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" - -qs@~6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" - -querystringify@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb" - -queue@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/queue/-/queue-3.1.0.tgz#6c49d01f009e2256788789f2bffac6b8b9990585" - dependencies: - inherits "~2.0.0" - -queue@^4.2.1: - version "4.4.2" - resolved "https://registry.yarnpkg.com/queue/-/queue-4.4.2.tgz#5a9733d9a8b8bd1b36e934bc9c55ab89b28e29c7" - dependencies: - inherits "~2.0.0" - -randomatic@^1.1.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -rc@^1.1.7: - version "1.2.2" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.2.tgz#d8ce9cb57e8d64d9c7badd9876c7c34cbe3c7077" - dependencies: - deep-extend "~0.4.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - -"readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.17: - version "1.0.34" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - safe-buffer "~5.1.1" - string_decoder "~1.0.3" - util-deprecate "~1.0.1" - -readable-stream@~1.1.9: - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - -readable-stream@~2.0.0: - version "2.0.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - string_decoder "~0.10.x" - util-deprecate "~1.0.1" - -readdirp@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" - dependencies: - graceful-fs "^4.1.2" - minimatch "^3.0.2" - readable-stream "^2.0.2" - set-immediate-shim "^1.0.1" - -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - dependencies: - resolve "^1.1.6" - -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - -reflect-metadata@^0.1.12: - version "0.1.12" - resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.12.tgz#311bf0c6b63cd782f228a81abe146a2bfa9c56f2" - -regex-cache@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" - dependencies: - is-equal-shallow "^0.1.3" - -regex-not@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.0.tgz#42f83e39771622df826b02af176525d6a5f157f9" - dependencies: - extend-shallow "^2.0.1" - -relative@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/relative/-/relative-3.0.2.tgz#0dcd8ec54a5d35a3c15e104503d65375b5a5367f" - dependencies: - isobject "^2.0.0" - -remap-istanbul@^0.9.5: - version "0.9.5" - resolved "https://registry.yarnpkg.com/remap-istanbul/-/remap-istanbul-0.9.5.tgz#a18617b1f31eec5a7dbee77538298b775606aaa8" - dependencies: - amdefine "^1.0.0" - gulp-util "3.0.7" - istanbul "0.4.5" - minimatch "^3.0.3" - source-map ">=0.5.6" - through2 "2.0.1" - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - -repeat-element@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" - -repeat-string@^1.5.2, repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - -repeating@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-1.1.3.tgz#3d4114218877537494f97f77f9785fab810fa4ac" - dependencies: - is-finite "^1.0.0" - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - dependencies: - is-finite "^1.0.0" - -replace-ext@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" - -replace-ext@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" - -request@2.81.0, request@~2.81.0: - version "2.81.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~4.2.1" - hawk "~3.1.3" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - performance-now "^0.2.0" - qs "~6.4.0" - safe-buffer "^5.0.1" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "^0.6.0" - uuid "^3.0.0" - -request@^2.83.0: - version "2.83.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.6.0" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.1" - forever-agent "~0.6.1" - form-data "~2.3.1" - har-validator "~5.0.3" - hawk "~6.0.2" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.17" - oauth-sign "~0.8.2" - performance-now "^2.1.0" - qs "~6.5.1" - safe-buffer "^5.1.1" - stringstream "~0.0.5" - tough-cookie "~2.3.3" - tunnel-agent "^0.6.0" - uuid "^3.1.0" - -request@~2.79.0: - version "2.79.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - caseless "~0.11.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~2.0.6" - hawk "~3.1.3" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - qs "~6.3.0" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "~0.4.1" - uuid "^3.0.0" - -requires-port@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - -resolve-dir@^1.0.0, resolve-dir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" - dependencies: - expand-tilde "^2.0.0" - global-modules "^1.0.0" - -resolve-url@^0.2.1, resolve-url@~0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - -resolve@1.1.x: - version "1.1.7" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" - -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.3.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" - dependencies: - path-parse "^1.0.5" - -retyped-diff-match-patch-tsd-ambient@^1.0.0-0: - version "1.0.0-1" - resolved "https://registry.yarnpkg.com/retyped-diff-match-patch-tsd-ambient/-/retyped-diff-match-patch-tsd-ambient-1.0.0-1.tgz#26482bf4915c7ed9f8300bb5cbec48fd4ff5bc62" - -right-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" - dependencies: - align-text "^0.1.1" - -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1: - version "2.6.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" - dependencies: - glob "^7.0.5" - -rxjs@^5.5.2: - version "5.5.6" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.6.tgz#e31fb96d6fd2ff1fd84bcea8ae9c02d007179c02" - dependencies: - symbol-observable "1.0.1" - -safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" - -samsam@1.x, samsam@^1.1.3: - version "1.3.0" - resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50" - -sax@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/sax/-/sax-0.5.2.tgz#735ffaa39a1cff8ffb9598f0223abdb03a9fb2ea" - -sax@>=0.6.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" - -semver@^4.1.0: - version "4.3.6" - resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" - -sequencify@~0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/sequencify/-/sequencify-0.0.7.tgz#90cff19d02e07027fd767f5ead3e7b95d1e7380c" - -set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - -set-getter@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376" - dependencies: - to-object-path "^0.3.0" - -set-immediate-shim@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" - -set-value@^0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.1" - to-object-path "^0.3.0" - -set-value@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -shortid@^2.2.8: - version "2.2.8" - resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.8.tgz#033b117d6a2e975804f6f0969dbe7d3d0b355131" - -sigmund@^1.0.1, sigmund@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" - -signal-exit@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - -sinon@^2.3.6: - version "2.4.1" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-2.4.1.tgz#021fd64b54cb77d9d2fb0d43cdedfae7629c3a36" - dependencies: - diff "^3.1.0" - formatio "1.2.0" - lolex "^1.6.0" - native-promise-only "^0.8.1" - path-to-regexp "^1.7.0" - samsam "^1.1.3" - text-encoding "0.6.4" - type-detect "^4.0.0" - -slash@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.1.tgz#e12b5487faded3e3dea0ac91e9400bf75b401370" - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^2.0.0" - -sntp@1.x.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" - dependencies: - hoek "2.x.x" - -sntp@2.x.x: - version "2.1.0" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" - dependencies: - hoek "4.x.x" - -source-map-resolve@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.3.1.tgz#610f6122a445b8dd51535a2a71b783dfc1248761" - dependencies: - atob "~1.1.0" - resolve-url "~0.2.1" - source-map-url "~0.3.0" - urix "~0.1.0" - -source-map-resolve@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.1.tgz#7ad0f593f2281598e854df80f19aae4b92d7a11a" - dependencies: - atob "^2.0.0" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-support@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.0.tgz#2018a7ad2bdf8faf2691e5fddab26bed5a2bacab" - dependencies: - source-map "^0.6.0" - -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - -source-map-url@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.3.0.tgz#7ecaf13b57bcd09da8a40c5d269db33799d4aaf9" - -source-map@>=0.5.6, source-map@^0.6.0, source-map@~0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - -source-map@^0.1.38: - version "0.1.43" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" - dependencies: - amdefine ">=0.0.4" - -source-map@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" - dependencies: - amdefine ">=0.0.4" - -source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - -source-map@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" - dependencies: - amdefine ">=0.0.4" - -sparkles@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.0.tgz#1acbbfb592436d10bbe8f785b7cc6f82815012c3" - -spdx-correct@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" - dependencies: - spdx-license-ids "^1.0.2" - -spdx-expression-parse@~1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" - -spdx-license-ids@^1.0.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - dependencies: - extend-shallow "^3.0.0" - -split@0.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" - dependencies: - through "2" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - -sshpk@^1.7.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - dashdash "^1.12.0" - getpass "^0.1.1" - optionalDependencies: - bcrypt-pbkdf "^1.0.0" - ecc-jsbn "~0.1.1" - jsbn "~0.1.0" - tweetnacl "~0.14.0" - -stat-mode@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-0.2.2.tgz#e6c80b623123d7d80cf132ce538f346289072502" - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -stream-combiner@~0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" - dependencies: - duplexer "~0.1.1" - -stream-consume@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/stream-consume/-/stream-consume-0.1.0.tgz#a41ead1a6d6081ceb79f65b061901b6d8f3d1d0f" - -stream-shift@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" - -streamfilter@^1.0.5: - version "1.0.7" - resolved "https://registry.yarnpkg.com/streamfilter/-/streamfilter-1.0.7.tgz#ae3e64522aa5a35c061fd17f67620c7653c643c9" - dependencies: - readable-stream "^2.0.2" - -streamifier@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/streamifier/-/streamifier-0.1.1.tgz#97e98d8fa4d105d62a2691d1dc07e820db8dfc4f" - -string-width@^1.0.1, string-width@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - -string_decoder@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" - dependencies: - safe-buffer "~5.1.0" - -stringstream@~0.0.4, stringstream@~0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" - -strip-ansi@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.3.0.tgz#25f48ea22ca79187f3174a4db8759347bb126220" - dependencies: - ansi-regex "^0.2.1" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - dependencies: - ansi-regex "^2.0.0" - -strip-bom-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz#e7144398577d51a6bed0fa1994fa05f43fd988ee" - dependencies: - first-chunk-stream "^1.0.0" - strip-bom "^2.0.0" - -strip-bom-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz#f87db5ef2613f6968aa545abfe1ec728b6a829ca" - dependencies: - first-chunk-stream "^2.0.0" - strip-bom "^2.0.0" - -strip-bom-string@1.X: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" - -strip-bom@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-1.0.0.tgz#85b8862f3844b5a6d5ec8467a93598173a36f794" - dependencies: - first-chunk-stream "^1.0.0" - is-utf8 "^0.2.0" - -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - dependencies: - is-utf8 "^0.2.0" - -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - dependencies: - get-stdin "^4.0.1" - -strip-indent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - -sudo-prompt@^8.0.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/sudo-prompt/-/sudo-prompt-8.1.0.tgz#62dce8013b80dd242e5b6ca15d8b8cffb7c85472" - -supports-color@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.2.0.tgz#ff1ed1e61169d06b3cf2d588e188b18d8847e17e" - -supports-color@4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" - dependencies: - has-flag "^2.0.0" - -supports-color@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - -supports-color@^3.1.0: - version "3.2.3" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" - dependencies: - has-flag "^1.0.0" - -supports-color@^4.0.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" - dependencies: - has-flag "^2.0.0" - -symbol-observable@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" - -tar-pack@^3.4.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f" - dependencies: - debug "^2.2.0" - fstream "^1.0.10" - fstream-ignore "^1.0.5" - once "^1.3.3" - readable-stream "^2.1.4" - rimraf "^2.5.1" - tar "^2.2.1" - uid-number "^0.0.6" - -tar@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" - dependencies: - block-stream "*" - fstream "^1.0.2" - inherits "2" - -text-encoding@0.6.4: - version "0.6.4" - resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" - -through2-filter@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-2.0.0.tgz#60bc55a0dacb76085db1f9dae99ab43f83d622ec" - dependencies: - through2 "~2.0.0" - xtend "~4.0.0" - -through2@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.1.tgz#384e75314d49f32de12eebb8136b8eb6b5d59da9" - dependencies: - readable-stream "~2.0.0" - xtend "~4.0.0" - -through2@2.X, through2@^2.0.0, through2@^2.0.1, through2@^2.0.3, through2@~2.0.0, through2@~2.0.1, through2@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" - dependencies: - readable-stream "^2.1.5" - xtend "~4.0.1" - -through2@^0.5.0, through2@~0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/through2/-/through2-0.5.1.tgz#dfdd012eb9c700e2323fd334f38ac622ab372da7" - dependencies: - readable-stream "~1.0.17" - xtend "~3.0.0" - -through2@^0.6.0, through2@^0.6.1, through2@~0.6.5: - version "0.6.5" - resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" - dependencies: - readable-stream ">=1.0.33-1 <1.1.0-0" - xtend ">=4.0.0 <4.1.0-0" - -through@2, "through@>=2.2.7 <3", through@~2.3, through@~2.3.1: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - -tildify@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/tildify/-/tildify-1.2.0.tgz#dcec03f55dca9b7aa3e5b04f21817eb56e63588a" - dependencies: - os-homedir "^1.0.0" - -time-stamp@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" - -timers-ext@^0.1.2: - version "0.1.4" - resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.4.tgz#365781e62b458722be079379fdbfe610a5efa4ba" - dependencies: - es5-ext "~0.10.14" - next-tick "1" - -tmp@0.0.29: - version "0.0.29" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.29.tgz#f25125ff0dd9da3ccb0c2dd371ee1288bb9128c0" - dependencies: - os-tmpdir "~1.0.1" - -to-absolute-glob@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz#1cdfa472a9ef50c239ee66999b662ca0eb39937f" - dependencies: - extend-shallow "^2.0.1" - -to-iso-string@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/to-iso-string/-/to-iso-string-0.0.2.tgz#4dc19e664dfccbe25bd8db508b00c6da158255d1" - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - dependencies: - kind-of "^3.0.2" - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.1.tgz#15358bee4a2c83bd76377ba1dc049d0f18837aae" - dependencies: - define-property "^0.2.5" - extend-shallow "^2.0.1" - regex-not "^1.0.0" - -tough-cookie@~2.3.0, tough-cookie@~2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" - dependencies: - punycode "^1.4.1" - -tree-kill@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.0.tgz#5846786237b4239014f05db156b643212d4c6f36" - -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - -tslib@^1.0.0, tslib@^1.7.1, tslib@^1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.8.1.tgz#6946af2d1d651a7b1863b531d6e5afa41aa44eac" - -tslint-eslint-rules@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/tslint-eslint-rules/-/tslint-eslint-rules-4.1.1.tgz#7c30e7882f26bc276bff91d2384975c69daf88ba" - dependencies: - doctrine "^0.7.2" - tslib "^1.0.0" - tsutils "^1.4.0" - -tslint-microsoft-contrib@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/tslint-microsoft-contrib/-/tslint-microsoft-contrib-5.0.1.tgz#328ee9c28d07cdf793293204c96e2ffab9221994" - dependencies: - tsutils "^1.4.0" - -tslint@^5.7.0: - version "5.8.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.8.0.tgz#1f49ad5b2e77c76c3af4ddcae552ae4e3612eb13" - dependencies: - babel-code-frame "^6.22.0" - builtin-modules "^1.1.1" - chalk "^2.1.0" - commander "^2.9.0" - diff "^3.2.0" - glob "^7.1.1" - minimatch "^3.0.4" - resolve "^1.3.2" - semver "^5.3.0" - tslib "^1.7.1" - tsutils "^2.12.1" - -tsutils@^1.4.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.9.1.tgz#b9f9ab44e55af9681831d5f28d0aeeaf5c750cb0" - -tsutils@^2.12.1: - version "2.15.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.15.0.tgz#90831e5908cca10b28cdaf83a56dcf8156aed7c6" - dependencies: - tslib "^1.8.1" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - dependencies: - safe-buffer "^5.0.1" - -tunnel-agent@~0.4.1: - version "0.4.3" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - dependencies: - prelude-ls "~1.1.2" - -type-detect@^4.0.0: - version "4.0.5" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.5.tgz#d70e5bc81db6de2a381bcaca0c6e0cbdc7635de2" - -typemoq@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/typemoq/-/typemoq-2.1.0.tgz#4452ce360d92cf2a1a180f0c29de2803f87af1e8" - dependencies: - circular-json "^0.3.1" - lodash "^4.17.4" - postinstall-build "^5.0.1" - -typescript-char@^0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/typescript-char/-/typescript-char-0.0.0.tgz#558feda737c765a610b737eefbb1775ee9bc8dab" - -typescript-formatter@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/typescript-formatter/-/typescript-formatter-6.1.0.tgz#4425ac2bab8aaea9a04251c078f47ab7a0202f13" - dependencies: - commandpost "^1.0.0" - editorconfig "^0.15.0" - -typescript@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" - -uglify-js@^2.6: - version "2.8.29" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" - dependencies: - source-map "~0.5.1" - yargs "~3.10.0" - optionalDependencies: - uglify-to-browserify "~1.0.0" - -uglify-to-browserify@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" - -uid-number@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" - -uint64be@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uint64be/-/uint64be-1.0.1.tgz#1f7154202f2a1b8af353871dda651bf34ce93e95" - -unc-path-regex@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" - -underscore@~1.8.3: - version "1.8.3" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" - -unicode@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/unicode/-/unicode-10.0.0.tgz#e5d51c1db93b6c71a0b879e0b0c4af7e6fdf688e" - -union-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^0.4.3" - -unique-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-1.0.0.tgz#d59a4a75427447d9aa6c91e70263f8d26a4b104b" - -unique-stream@^2.0.2: - version "2.2.1" - resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.2.1.tgz#5aa003cfbe94c5ff866c4e7d668bb1c4dbadb369" - dependencies: - json-stable-stringify "^1.0.0" - through2-filter "^2.0.0" - -universalify@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -untildify@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.2.tgz#7f1f302055b3fea0f3e81dc78eb36766cb65e3f1" - -urix@^0.1.0, urix@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - -url-parse@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.2.0.tgz#3a19e8aaa6d023ddd27dcc44cb4fc8f7fec23986" - dependencies: - querystringify "~1.0.0" - requires-port "~1.0.0" - -urlgrey@0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/urlgrey/-/urlgrey-0.4.4.tgz#892fe95960805e85519f1cd4389f2cb4cbb7652f" - -use@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/use/-/use-2.0.2.tgz#ae28a0d72f93bf22422a18a2e379993112dec8e8" - dependencies: - define-property "^0.2.5" - isobject "^3.0.0" - lazy-cache "^2.0.2" - -user-home@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" - -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - -uuid@^3.0.0, uuid@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" - -v8flags@^2.0.2: - version "2.1.1" - resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" - dependencies: - user-home "^1.1.1" - -vali-date@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/vali-date/-/vali-date-1.0.0.tgz#1b904a59609fb328ef078138420934f6b86709a6" - -validate-npm-package-license@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" - dependencies: - spdx-correct "~1.0.0" - spdx-expression-parse "~1.0.0" - -validator@~3.35.0: - version "3.35.0" - resolved "https://registry.yarnpkg.com/validator/-/validator-3.35.0.tgz#3f07249402c1fc8fc093c32c6e43d72a79cca1dc" - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -vinyl-file@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-2.0.0.tgz#a7ebf5ffbefda1b7d18d140fcb07b223efb6751a" - dependencies: - graceful-fs "^4.1.2" - pify "^2.3.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - strip-bom-stream "^2.0.0" - vinyl "^1.1.0" - -vinyl-fs@^0.3.0: - version "0.3.14" - resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-0.3.14.tgz#9a6851ce1cac1c1cea5fe86c0931d620c2cfa9e6" - dependencies: - defaults "^1.0.0" - glob-stream "^3.1.5" - glob-watcher "^0.0.6" - graceful-fs "^3.0.0" - mkdirp "^0.5.0" - strip-bom "^1.0.0" - through2 "^0.6.1" - vinyl "^0.4.0" - -vinyl-fs@^2.0.0, vinyl-fs@^2.4.3, vinyl-fs@~2.4.3: - version "2.4.4" - resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-2.4.4.tgz#be6ff3270cb55dfd7d3063640de81f25d7532239" - dependencies: - duplexify "^3.2.0" - glob-stream "^5.3.2" - graceful-fs "^4.0.0" - gulp-sourcemaps "1.6.0" - is-valid-glob "^0.3.0" - lazystream "^1.0.0" - lodash.isequal "^4.0.0" - merge-stream "^1.0.0" - mkdirp "^0.5.0" - object-assign "^4.0.0" - readable-stream "^2.0.4" - strip-bom "^2.0.0" - strip-bom-stream "^1.0.0" - through2 "^2.0.0" - through2-filter "^2.0.0" - vali-date "^1.0.0" - vinyl "^1.0.0" - -vinyl-source-stream@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vinyl-source-stream/-/vinyl-source-stream-1.1.2.tgz#62b53a135610a896e98ca96bee3a87f008a8e780" - dependencies: - through2 "^2.0.3" - vinyl "^0.4.3" - -vinyl@^0.2.1: - version "0.2.3" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.2.3.tgz#bca938209582ec5a49ad538a00fa1f125e513252" - dependencies: - clone-stats "~0.0.1" - -vinyl@^0.4.0, vinyl@^0.4.3, vinyl@~0.4.6: - version "0.4.6" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" - dependencies: - clone "^0.2.0" - clone-stats "^0.0.1" - -vinyl@^0.5.0: - version "0.5.3" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.5.3.tgz#b0455b38fc5e0cf30d4325132e461970c2091cde" - dependencies: - clone "^1.0.0" - clone-stats "^0.0.1" - replace-ext "0.0.1" - -vinyl@^1.0.0, vinyl@^1.1.0, vinyl@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884" - dependencies: - clone "^1.0.0" - clone-stats "^0.0.1" - replace-ext "0.0.1" - -vinyl@^2.0.2: - version "2.1.0" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.1.0.tgz#021f9c2cf951d6b939943c89eb5ee5add4fd924c" - dependencies: - clone "^2.1.1" - clone-buffer "^1.0.0" - clone-stats "^1.0.0" - cloneable-readable "^1.0.0" - remove-trailing-separator "^1.0.1" - replace-ext "^1.0.0" - -vinyl@~2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.0.2.tgz#0a3713d8d4e9221c58f10ca16c0116c9e25eda7c" - dependencies: - clone "^1.0.0" - clone-buffer "^1.0.0" - clone-stats "^1.0.0" - cloneable-readable "^1.0.0" - is-stream "^1.1.0" - remove-trailing-separator "^1.0.1" - replace-ext "^1.0.0" - -vscode-debugadapter-testsupport@^1.25.0: - version "1.25.0" - resolved "https://registry.yarnpkg.com/vscode-debugadapter-testsupport/-/vscode-debugadapter-testsupport-1.25.0.tgz#47172dd5c7a197e9cb6c4a39acc535e5c93c3ccf" - dependencies: - vscode-debugprotocol "1.25.0" - -vscode-debugadapter@^1.0.1: - version "1.25.0" - resolved "https://registry.yarnpkg.com/vscode-debugadapter/-/vscode-debugadapter-1.25.0.tgz#8b39ab4e0f7432a94ef51e3ec67c0c3e9fc98ab8" - dependencies: - vscode-debugprotocol "1.25.0" - -vscode-debugprotocol@1.25.0, vscode-debugprotocol@^1.0.1: - version "1.25.0" - resolved "https://registry.yarnpkg.com/vscode-debugprotocol/-/vscode-debugprotocol-1.25.0.tgz#7a7e38df4cad8839e37ebcd06ed903902d97a7e3" - -vscode-extension-telemetry@0.0.14: - version "0.0.14" - resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.0.14.tgz#17454705b6bb8757351b955d812923f02ee895bf" - dependencies: - applicationinsights "1.0.1" - -vscode-jsonrpc@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-3.5.0.tgz#87239d9e166b2d7352245b8a813597804c1d63aa" - -vscode-languageclient@^3.1.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-3.5.0.tgz#36d02cc186a8365a4467719a290fb200a9ae490a" - dependencies: - vscode-languageserver-protocol "^3.5.0" - -vscode-languageserver-protocol@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.5.0.tgz#067c5cbe27709795398d119692c97ebba1452209" - dependencies: - vscode-jsonrpc "^3.5.0" - vscode-languageserver-types "^3.5.0" - -vscode-languageserver-types@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.5.0.tgz#e48d79962f0b8e02de955e3f524908e2b19c0374" - -vscode-languageserver@^3.1.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-3.5.0.tgz#d28099bc6ddda8c1dd16b707e454e1b1ddae0dba" - dependencies: - vscode-languageserver-protocol "^3.5.0" - vscode-uri "^1.0.1" - -vscode-uri@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.1.tgz#11a86befeac3c4aa3ec08623651a3c81a6d0bbc8" - -vscode@^1.1.5: - version "1.1.10" - resolved "https://registry.yarnpkg.com/vscode/-/vscode-1.1.10.tgz#d1cba378ab24f1d3ddf9cd470d242ee1472dd35b" - dependencies: - glob "^7.1.2" - gulp-chmod "^2.0.0" - gulp-filter "^5.0.1" - gulp-gunzip "1.0.0" - gulp-remote-src "^0.4.3" - gulp-symdest "^1.1.0" - gulp-untar "^0.0.6" - gulp-vinyl-zip "^2.1.0" - mocha "^4.0.1" - request "^2.83.0" - semver "^5.4.1" - source-map-support "^0.5.0" - url-parse "^1.1.9" - vinyl-source-stream "^1.1.0" - -which@^1.1.1, which@^1.2.14: - version "1.3.0" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" - dependencies: - isexe "^2.0.0" - -which@~1.0.5: - version "1.0.9" - resolved "https://registry.yarnpkg.com/which/-/which-1.0.9.tgz#460c1da0f810103d0321a9b633af9e575e64486f" - -wide-align@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" - dependencies: - string-width "^1.0.2" - -window-size@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" - -winreg@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.4.tgz#ba065629b7a925130e15779108cf540990e98d1b" - -wordwrap@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" - -wordwrap@^1.0.0, wordwrap@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - -xml2js@0.2.7: - version "0.2.7" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.2.7.tgz#1838518bb01741cae0878bab4915e494c32306af" - dependencies: - sax "0.5.2" - -xml2js@^0.4.17: - version "0.4.19" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" - dependencies: - sax ">=0.6.0" - xmlbuilder "~9.0.1" - -xmlbuilder@0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-0.4.3.tgz#c4614ba74e0ad196e609c9272cd9e1ddb28a8a58" - -xmlbuilder@~9.0.1: - version "9.0.4" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.4.tgz#519cb4ca686d005a8420d3496f3f0caeecca580f" - -"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" - -xtend@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-3.0.0.tgz#5cce7407baf642cba7becda568111c493f59665a" - -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - -yargs@~3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" - dependencies: - camelcase "^1.0.2" - cliui "^2.1.0" - decamelize "^1.0.0" - window-size "0.1.0" - -yauzl@^2.2.1: - version "2.9.1" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.9.1.tgz#a81981ea70a57946133883f029c5821a89359a7f" - dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.0.1" - -yazl@^2.2.1: - version "2.4.3" - resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.4.3.tgz#ec26e5cc87d5601b9df8432dbdd3cd2e5173a071" - dependencies: - buffer-crc32 "~0.2.3" - -zone.js@0.7.6: - version "0.7.6" - resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009"