diff --git a/.env b/.env new file mode 100644 index 000000000000..8a1d7cf7ac24 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +PYTHONPATH=./uitests diff --git a/.gitignore b/.gitignore index c50726258ef3..4ccc08dc90eb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ out node_modules *.pyc -*.vsix +# *.vsix **/.vscode/.ropeproject/** **/testFiles/**/.cache/** *.noseids @@ -15,7 +15,9 @@ npm-debug.log !yarn.lock coverage/ .vscode-test/** +.vscode-smoke/** **/.venv*/ +**/venv*/ port.txt precommit.hook pythonFiles/experimental/ptvsd/** diff --git a/.nycrc b/.nycrc new file mode 100644 index 000000000000..e2cf98abc2f6 --- /dev/null +++ b/.nycrc @@ -0,0 +1,4 @@ +{ + "extends":"@istanbuljs/nyc-config-typescript", + "all":true +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 7609fc076e7f..0a8156966f8d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,13 +2,30 @@ { "version": "0.1.0", "configurations": [ + { + "name": "Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "stopOnEntry": false, + "smartStep": true, + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/out/**/*" + ], + "preLaunchTask": "Compile" + }, { "name": "Extension inside container", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/data" + "--extensionDevelopmentPath=${workspaceFolder}", + "${workspaceFolder}/data" ], "stopOnEntry": false, "smartStep": true, @@ -24,7 +41,9 @@ "request": "launch", "module": "IPython", "console": "integratedTerminal", - "args": ["${file}"] // Additional args should be prefixed with a '--' first. + "args": [ + "${file}" + ] // Additional args should be prefixed with a '--' first. }, { "name": "Python: Current File", @@ -33,22 +52,6 @@ "program": "${file}", "console": "integratedTerminal" }, - { - "name": "Extension", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "stopOnEntry": false, - "smartStep": true, - "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/**/*" - ], - "preLaunchTask": "Compile" - }, { "name": "Debugger as debugServer", "type": "node", @@ -199,6 +202,21 @@ "args": [ "watch" ] + }, + { + "name": "UI Tests (BDD)", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/uitests/__main__.py", + "args": [ + "test", + "--", + "--format", + "progress3", + "--tags=wip" + ], + "justMyCode": false, + "console": "internalConsole" } ], "compounds": [ @@ -210,4 +228,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 3066bc76f1d7..2e87355a2ea7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,8 +7,7 @@ "obj": true, "bin": true, "**/__pycache__": true, - "node_modules": true, - ".vscode-test": true, + "node_modules": false, "**/.mypy_cache/**": true, "**/.ropeproject/**": true }, @@ -17,16 +16,22 @@ "coverage": true, "languageServer*/**": true }, + "editor.formatOnSave": true, "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version - "tslint.enable": true, - "python.linting.enabled": false, - "python.testing.promptToConfigure": false, - "python.workspaceSymbols.enabled": false, - "python.formatting.provider": "none", "typescript.preferences.quoteStyle": "single", "javascript.preferences.quoteStyle": "single", "typescriptHero.imports.stringQuoteStyle": "'", - "prettier.tslintIntegration": false, - "prettier.printWidth": 180, - "prettier.singleQuote": true + "tslint.enable": true, + "python.jediEnabled": false, + "python.analysis.logLevel": "Trace", + "python.analysis.downloadChannel": "beta", + "python.unitTest.promptToConfigure": false, + "python.workspaceSymbols.enabled": false, + "python.formatting.provider": "black", + "python.linting.enabled": true, + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": true, + "python.linting.flake8Args": [ + "--max-line-length=120" + ] } diff --git a/.vscodeignore b/.vscodeignore index 27f5660bcfd0..2f9623b77531 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -9,6 +9,7 @@ .gitmodules .huskyrc.json .npmrc +.nycrc .travis.yml CODE_OF_CONDUCT.md CODING_STANDARDS.md @@ -31,6 +32,7 @@ vscode-python-signing.* webpack.config.js webpack.datascience-*.config.js +.devcontainer/** .github/** .mocha-reporter/** .nvm/** @@ -44,6 +46,7 @@ bin/** build/** BuildOutput/** coverage/** +data/** debug_coverage*/** images/**/*.gif images/**/*.png @@ -70,3 +73,4 @@ tmp/** tpn/** typings/** types/** +uitests/** diff --git a/Pipfile b/Pipfile new file mode 100644 index 000000000000..120aea360a86 --- /dev/null +++ b/Pipfile @@ -0,0 +1,30 @@ +[[source]] + +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + + +[packages] + +jedi = "==0.12.0" +parso = "==0.2.1" +isort = "==4.3.4" +ptvsd = "==4.2.4" +behave = "*" +selenium = "*" +progress = "*" +requests = "*" +docopt = "*" +black = "*" + + +[dev-packages] + +"flake8" = "*" +black = "*" + + +[requires] + +python_version = "3.7" diff --git a/build/ci/templates/build_compile_jobs.yml b/build/ci/templates/build_compile_jobs.yml new file mode 100644 index 000000000000..65a15ad79419 --- /dev/null +++ b/build/ci/templates/build_compile_jobs.yml @@ -0,0 +1,16 @@ +jobs: +- job: Compile + variables: + build: false + pool: + vmImage: "macos-10.13" + steps: + - template: build_compile_steps.yml + +- job: Build + variables: + build: true + pool: + vmImage: "macos-10.13" + steps: + - template: build_compile_steps.yml diff --git a/build/ci/templates/build.yml b/build/ci/templates/build_compile_steps.yml similarity index 81% rename from build/ci/templates/build.yml rename to build/ci/templates/build_compile_steps.yml index 89bd3bb222ce..cfdb15e7781f 100644 --- a/build/ci/templates/build.yml +++ b/build/ci/templates/build_compile_steps.yml @@ -41,50 +41,50 @@ steps: displayName: "Compile and check for errors" inputs: targets: "prePublishNonBundle" - condition: and(succeeded(), eq(variables['Build'], 'false')) + condition: and(succeeded(), eq(variables['build'], 'false')) - bash: npx tslint ./src/**/*.ts{,x} displayName: "code hygiene" - condition: and(succeeded(), eq(variables['Build'], 'false')) + condition: and(succeeded(), eq(variables['build'], 'false')) - bash: | python -m pip install -U pip python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt failOnStderr: true displayName: "pip install requirements" - condition: and(succeeded(), eq(variables['Build'], 'true')) + condition: and(succeeded(), eq(variables['build'], 'true')) - bash: | npm install -g vsce npm run clean displayName: "Install vsce & Clean" - condition: and(succeeded(), eq(variables['Build'], 'true')) + condition: and(succeeded(), eq(variables['build'], 'true')) - bash: | npm run updateBuildNumber -- --buildNumber $BUILD_BUILDID displayName: "Update dev Version" - condition: and(succeeded(), eq(variables['Build'], 'true'), eq(variables['Build.SourceBranchName'], 'master')) + condition: and(succeeded(), eq(variables['build'], 'true'), eq(variables['Build.SourceBranchName'], 'master')) - bash: | npm run updateBuildNumber -- --buildNumber $BUILD_BUILDID --updateChangelog displayName: "Update release Version" - condition: and(succeeded(), eq(variables['Build'], 'true'), eq(variables['Build.SourceBranchName'], 'release')) + condition: and(succeeded(), eq(variables['build'], 'true'), eq(variables['Build.SourceBranchName'], 'release')) - bash: | npm run package displayName: "Build VSIX" - condition: and(succeeded(), eq(variables['Build'], 'true')) + condition: and(succeeded(), eq(variables['build'], 'true')) - task: CopyFiles@2 inputs: contents: "*.vsix" targetFolder: $(Build.ArtifactStagingDirectory) displayName: "Copy VSIX" - condition: and(succeeded(), eq(variables['Build'], 'true')) + condition: and(succeeded(), eq(variables['build'], 'true')) - task: PublishBuildArtifacts@1 inputs: pathtoPublish: $(Build.ArtifactStagingDirectory) artifactName: VSIX displayName: "Publish VSIX to Arifacts" - condition: and(succeeded(), eq(variables['Build'], 'true')) + condition: and(succeeded(), eq(variables['build'], 'true')) diff --git a/build/ci/templates/test_phases.yml b/build/ci/templates/test_phases.yml index 8451efbcdc7c..34a8886a206f 100644 --- a/build/ci/templates/test_phases.yml +++ b/build/ci/templates/test_phases.yml @@ -503,30 +503,3 @@ steps: condition: and(succeeded(), contains(variables['TestsToRun'], 'testPerformance')) env: DISPLAY: :10 - - # Run the smoke tests. - # - # This task only runs if the string 'testSmoke' exists in variable `TestsToRun`. - # - # Example command line (windows pwsh): - # > npm run clean - # > npm run updateBuildNumber -- --buildNumber 0.0.0-local - # > npm run package - # > npx gulp clean:cleanExceptTests - # > npm run testSmoke - - bash: | - npm install -g vsce - npm run clean - npm run updateBuildNumber -- --buildNumber $BUILD_BUILDID - npm run package - npx gulp clean:cleanExceptTests - npm run testSmoke - displayName: 'Run Smoke Tests' - condition: and(succeeded(), contains(variables['TestsToRun'], 'testSmoke')) - env: - DISPLAY: :10 - - - task: PublishBuildArtifacts@1 - inputs: - pathtoPublish: $(Build.ArtifactStagingDirectory) - artifactName: $(Agent.JobName) \ No newline at end of file diff --git a/build/ci/templates/uitest_jobs.yml b/build/ci/templates/uitest_jobs.yml new file mode 100644 index 000000000000..d958899bcdf7 --- /dev/null +++ b/build/ci/templates/uitest_jobs.yml @@ -0,0 +1,101 @@ +# Parameters +# 1. jobs +# Mandatory. +# Contains a list of all tests that needs to be run. +# Sample: +# ``` +# - template: templates/uitest_jobs.yml +# parameters: +# jobs: +# - test: "Smoke" +# tags: "--tags=smoke" +# - test: "Test" +# tags: "--tags=test" +# - test: "Terminal" +# tags: "--tags=terminal" +# ``` +# Based on the sample, we're running 3 tests with the names `Smoke`, `Test`, and `Terminal`. +# The tags inside each test contains the arguments that needs to be passd into behave. +# Please pass in just the `tags` arguments. +# Multiple tag values can be passed in as follows: +# tags: "--tags=debug --tags=remote" +# More information on --tags argument for behave can be found here: +# * https://behave.readthedocs.io/en/latest/tutorial.html#controlling-things-with-tags +# * https://behave.readthedocs.io/en/latest/tag_expressions.html +# 2. ignorePythonVersions +# Comma delimited list of Python versions not to be tested against. +# E.g. = 3.7,3.6 +# Possible values 3.7, 3.6, 3.5, 3.5, 2.7 +# Any OS provided in this string will not be tested against. +# 3. ignoreOperatingSystems +# Comma delimited list of OS not to be tested against. +# E.g. = win, linux +# Possible values = mac, win linux +# 4. pythonVersions +# Do not pass. +# Defines the versions of Pythons versions we run tests against. +# We use this to build a list of python versions as a list instead of having to hardcode them. +# This way we just use a simple for loop and run jobs for each OS and each Python version. + +parameters: + jobs: [] + ignorePythonVersions: "" + ignoreOperatingSystems: "" + pythonVersions: [ + { + "version": "3.7", + "displayName": "37", + "excludeTags": "--tags=~@python3.6 --tags=~@python3.5 --tags=~@python2" + }, + { + "version": "3.6", + "displayName": "36", + "excludeTags": "--tags=~@python3.7 --tags=~@python3.5 --tags=~@python2" + }, + { + "version": "3.5", + "displayName": "35", + "excludeTags": "--tags=~@python3.7 --tags=~@python3.6 --tags=~@python2" + }, + { + "version": "2.7", + "displayName": "27", + "excludeTags": "--tags=~@python3.7 --tags=~@python3.5 --tags=~@python3" + } + ] + + +jobs: +- job: UITest +# dependsOn: +# - Compile +# - Build + timeoutInMinutes: 90 + strategy: + matrix: + ${{ each job in parameters.jobs }}: + ${{ each py in parameters.pythonVersions }}: + ${{ if not(contains(coalesce(job.ignorePythonVersions, ''), py.version)) }}: + ${{ if not(contains(coalesce(job.ignoreOperatingSystems, ''), 'mac')) }}: + ${{ format('Mac{0}{1}', py.displayName, job.test) }}: + PythonVersion: ${{ py.version }} + VMImageName: "macos-10.13" + Tags: ${{ format('{0} {1} --tags=~@win --tags=~@linux', job.tags, py.excludeTags) }} + + ${{ if not(contains(coalesce(job.ignoreOperatingSystems, ''), 'win')) }}: + ${{ format('Win{0}{1}', py.displayName, job.test) }}: + PythonVersion: ${{ py.version }} + VMImageName: "vs2017-win2016" + Tags: ${{ format('{0} {1} --tags=~@mac --tags=~@linux', job.tags, py.excludeTags) }} + + ${{ if not(contains(coalesce(job.ignoreOperatingSystems, ''), 'linux')) }}: + ${{ format('Linux{0}{1}', py.displayName, job.test) }}: + PythonVersion: ${{ py.version }} + VMImageName: "ubuntu-16.04" + Tags: ${{ format('{0} {1} --tags=~@mac --tags=~@win', job.tags, py.excludeTags) }} + + pool: + vmImage: $(VMImageName) + + steps: + - template: uitest_phases.yml diff --git a/build/ci/templates/uitest_phases.yml b/build/ci/templates/uitest_phases.yml index 3757924930a0..c320a1aca750 100644 --- a/build/ci/templates/uitest_phases.yml +++ b/build/ci/templates/uitest_phases.yml @@ -1,3 +1,13 @@ +# Variables: +# Parameters: +# 1. Tags +# Mandatory. +# Contain the `--tags=....` arguments to be passed into behave to exclude certain tags. +# Multiple tags can be passed as `--tags=@smoke --tags=~@ignore1 --tags=~@another --tags=~@andMore` +# More information on --tags argument for behave can be found here: +# * https://behave.readthedocs.io/en/latest/tutorial.html#controlling-things-with-tags +# * https://behave.readthedocs.io/en/latest/tag_expressions.html + steps: - bash: | printenv @@ -14,6 +24,48 @@ steps: inputs: versionSpec: $(PythonVersion) + # Conda + - bash: echo "##vso[task.prependpath]$CONDA/bin" + displayName: Add conda to PATH + condition: and(succeeded(), not(eq(variables['agent.os'], 'Windows_NT'))) + + - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" + displayName: Add conda to PATH + condition: and(succeeded(), eq(variables['agent.os'], 'Windows_NT')) + + # On Hosted macOS, the agent user doesn't have ownership of Miniconda's installation directory/ + # We need to take ownership if we want to update conda or install packages globally + - bash: sudo chown -R $USER $CONDA + displayName: Take ownership of conda installation + condition: and(succeeded(), eq(variables['agent.os'], 'Darwin')) + + - script: | + export CI_PYTHON_PATH=`which python` + echo '##vso[task.setvariable variable=CI_PYTHON_PATH]'$CI_PYTHON_PATH + displayName: "Setup CI_PYTHON_PATH for extension" + condition: and(succeeded(), not(eq(variables['agent.os'], 'Windows_NT'))) + + - powershell: | + $CI_PYTHON_PATH = (get-command python).path + Write-Host "##vso[task.setvariable variable=CI_PYTHON_PATH]$CI_PYTHON_PATH" + Write-Host $CI_PYTHON_PATH + displayName: "Setup CI_PYTHON_PATH for extension" + condition: and(succeeded(), eq(variables['agent.os'], 'Windows_NT')) + + # Some tests need to have both 2.7 & 3.7 available. + # Also, use Python 3.7 to run the scripts that drive the ui tests. + # Order matters, currently active python version will be used to drive tests. + # Hence ensure 3.7 is setup last. + - task: UsePythonVersion@0 + displayName: "Use Python 2.7" + inputs: + versionSpec: 2.7 + + - task: UsePythonVersion@0 + displayName: "Use Python 3.7 (to drive tests)" + inputs: + versionSpec: 3.7 + - task: Npm@1 displayName: "Use NPM $(NpmVersion)" inputs: @@ -37,12 +89,6 @@ steps: condition: and(succeeded(), eq(variables['system.debug'], 'true')) displayName: Show Dependency Versions - - task: Gulp@0 - displayName: "Compile" - inputs: - targets: "prePublishNonBundle" - condition: succeeded() - # https://code.visualstudio.com/api/working-with-extensions/continuous-integration#azure-pipelines - bash: | set -e @@ -51,23 +97,80 @@ steps: displayName: "Start xvfb" condition: and(succeeded(), eq(variables['Agent.Os'], 'Linux'), not(variables['SkipXvfb'])) - - task: DownloadBuildArtifacts@0 - inputs: - buildType: "current" - artifactName: "VSIX" - downloadPath: "$(Build.SourcesDirectory)" - displayName: "Restore VSIX" - condition: succeeded() + # - task: DownloadBuildArtifacts@0 + # inputs: + # buildType: "current" + # artifactName: "VSIX" + # downloadPath: "$(Build.SourcesDirectory)" + # displayName: "Restore VSIX" + # condition: succeeded() - - task: CopyFiles@2 - inputs: - sourceFolder: "$(Build.SourcesDirectory)/VSIX" - targetFolder: $(Build.SourcesDirectory) - displayName: "Copy VSIX" + # - task: CopyFiles@2 + # inputs: + # sourceFolder: "$(Build.SourcesDirectory)/VSIX" + # targetFolder: $(Build.SourcesDirectory) + # displayName: "Copy VSIX" + # condition: succeeded() + + # Run the UI Tests. + - bash: | + python -m pip install -U pip + python -m pip install --upgrade -r ./uitests/requirements.txt + python uitests download + npm install -g vsce + python uitests install + env: + DISPLAY: :10 + AgentJobName: $(Agent.JobName) + displayName: "Download & Install UI Test Dependencies" condition: succeeded() - - bash: npm run testSmoke - displayName: "Run Smoke Tests" + - script: python uitests test --embed_screenshots -- --format=progress3 $(Tags) --tags=~@skip -D python_path=$(CI_PYTHON_PATH) + env: + DISPLAY: :10 + AgentJobName: $(Agent.JobName) + failOnStderr: true + displayName: "Run Tests" condition: succeeded() + + # Generate and publis results even if there are failures in previous steps. + - script: python uitests report env: - DISPLAY: :10 + AgentJobName: $(Agent.JobName) + displayName: "Generate Reports" + condition: always() + + - task: CopyFiles@2 + inputs: + contents: ".vscode-test/reports/**" + targetFolder: $(Build.ArtifactStagingDirectory) + displayName: "Copy Reports" + condition: always() + + - task: CopyFiles@2 + inputs: + contents: ".vscode-test/logs/**" + targetFolder: $(Build.ArtifactStagingDirectory) + displayName: "Copy Extension Logs" + condition: always() + + - task: CopyFiles@2 + inputs: + contents: ".vscode-test/user/logs/**" + targetFolder: $(Build.ArtifactStagingDirectory) + displayName: "Copy VSC Logs" + condition: always() + + - task: PublishBuildArtifacts@1 + inputs: + pathtoPublish: $(Build.ArtifactStagingDirectory) + artifactName: $(Agent.JobName) + displayName: "Upload Reports" + condition: always() + + - task: PublishTestResults@2 + displayName: "TestResults" + inputs: + testRunner: JUnit + testResultsFiles: "$(Build.SourcesDirectory)/.vscode-test/reports/*.xml" + condition: always() diff --git a/build/ci/vscode-python-ci.yaml b/build/ci/vscode-python-ci.yaml index 38164fa3e004..0f9b2a972105 100644 --- a/build/ci/vscode-python-ci.yaml +++ b/build/ci/vscode-python-ci.yaml @@ -22,55 +22,15 @@ variables: VSC_PYTHON_LOG_FILE: '$(Build.ArtifactStagingDirectory)/vsc.log' jobs: -- job: Compile - variables: - Build: false - pool: - vmImage: "macos-10.13" - steps: - - template: templates/build.yml - -- job: Build - variables: - Build: true - pool: - vmImage: "macos-10.13" - steps: - - template: templates/build.yml - -- job: Smoke - dependsOn: - - Compile - - Build - - strategy: - matrix: - 'Linux-Py3.7': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - 'Mac-Py3.7': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - 'Linux-Py2.7': - PythonVersion: '2.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - 'Mac-Py2.7': - PythonVersion: '2.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - pool: - vmImage: $(VMImageName) +- template: templates/build_compile_jobs.yml - steps: - - template: templates/uitest_phases.yml +- template: templates/uitest_jobs.yml + parameters: + jobs: + - test: "Smoke" + tags: "--tags=@smoke" + ignorePythonVersions: "3.6,3.5" - job: 'CI' diff --git a/build/ci/vscode-python-nightly-ci.yaml b/build/ci/vscode-python-nightly-ci.yaml index a43dd413c4ed..4b373dc79b23 100644 --- a/build/ci/vscode-python-nightly-ci.yaml +++ b/build/ci/vscode-python-nightly-ci.yaml @@ -21,6 +21,14 @@ variables: jobs: +- template: templates/build_compile_jobs.yml + +- template: templates/uitest_jobs.yml + parameters: + jobs: + - test: "Smoke" + tags: "--tags=@smoke" + - job: 'Nightly' strategy: @@ -342,48 +350,6 @@ jobs: TestsToRun: 'testDebugger' NeedsPythonTestReqs: true - # Smoke tests - 'Linux-Py3.7 Smoke': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - 'Mac-Py3.7 Smoke': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - 'Linux-Py3.6 Smoke': - PythonVersion: '3.6' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - 'Mac-Py3.6 Smoke': - PythonVersion: '3.6' - VMImageName: 'macos-10.13' - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - 'Linux-Py3.5 Smoke': - PythonVersion: '3.5' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - 'Mac-Py3.5 Smoke': - PythonVersion: '3.5' - VMImageName: 'macos-10.13' - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - 'Linux-Py2.7 Smoke': - PythonVersion: '2.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - 'Mac-Py2.7 Smoke': - PythonVersion: '2.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - # Functional tests (not mocked Jupyter) 'Windows-Py3.7 Functional': PythonVersion: '3.7' diff --git a/build/ci/vscode-python-nightly-uitest.yaml b/build/ci/vscode-python-nightly-uitest.yaml new file mode 100644 index 000000000000..c6219761b665 --- /dev/null +++ b/build/ci/vscode-python-nightly-uitest.yaml @@ -0,0 +1,27 @@ +name: '$(Year:yyyy).$(Month).0.$(BuildID)-nightly-uitest' + +trigger: none +pr: none + + +# Variables that are available for the entire pipeline. +variables: + PythonVersion: '3.7' + NodeVersion: '10.5.0' + NpmVersion: 'latest' + MOCHA_FILE: '$(Build.ArtifactStagingDirectory)/test-junit.xml' # All test files will write their JUnit xml output to this file, clobbering the last time it was written. + MOCHA_REPORTER_JUNIT: true # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). + VSC_PYTHON_FORCE_LOGGING: true # Enable this to turn on console output for the logger + +jobs: +- template: templates/build_compile_jobs.yml + +- template: templates/uitest_jobs.yml + parameters: + jobs: + - test: "Smoke" + tags: "--tags=@smoke" + - test: "Test" + tags: "--tags=@test" + - test: "Terminal" + tags: "--tags=@terminal --tags=~@terminal.pipenv" diff --git a/build/ci/vscode-python-pr-validation.yaml b/build/ci/vscode-python-pr-validation.yaml index 99053da62693..a9843522326b 100644 --- a/build/ci/vscode-python-pr-validation.yaml +++ b/build/ci/vscode-python-pr-validation.yaml @@ -24,78 +24,79 @@ variables: jobs: -- job: 'PR' +# - job: 'PR' - strategy: - matrix: - # Each member of this list must contain these values: - # VMImageName: '[name]' - the VM image to run the tests on. - # TestsToRun: 'testA, testB, ..., testN' - the list of tests to execute, see the list above. - # Each member of this list may contain these values: - # NeedsPythonTestReqs: [true|false] - install the test-requirements prior to running tests. False if not set. - # NeedsPythonFunctionalReqs: [true|false] - install the functional-requirements prior to running tests. False if not set. - # PythonVersion: 'M.m' - the Python version to run. DefaultPythonVersion if not set. - # NodeVersion: 'x.y.z' - Node version to use. DefaultNodeVersion if not set. - # SkipXvfb: [true|false] - skip initialization of xvfb prior to running system tests on Linux. False if not set - # UploadBinary: [true|false] - upload test binaries to Azure if true. False if not set. - 'Win-Py3.7 Unit': - VMImageName: 'vs2017-win2016' - TestsToRun: 'runHygiene, testUnitTests, pythonUnitTests, pythonInternalTools' - NeedsPythonTestReqs: true - 'Win-Py2.7 Unit': - PythonVersion: '2.7' - VMImageName: 'vs2017-win2016' - TestsToRun: 'pythonUnitTests' - NeedsPythonTestReqs: true - 'Mac-Py3.7 Unit': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools' - NeedsPythonTestReqs: true - 'Mac-Py3.7 Single Workspace': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Mac-Py3.7 Smoke': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - 'Mac-Py2.7 Unit+Single': - PythonVersion: '2.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testUnitTests, pythonUnitTests, testSingleWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py3.7 Unit': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools' - NeedsPythonTestReqs: true - 'Linux-Py3.7 Single Workspace': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py3.7 Smoke': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - 'Linux-Py2.7 Unit+Single': - PythonVersion: '2.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testUnitTests, pythonUnitTests, testSingleWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py3.7 Functional': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true +# strategy: +# matrix: +# # Each member of this list must contain these values: +# # VMImageName: '[name]' - the VM image to run the tests on. +# # TestsToRun: 'testA, testB, ..., testN' - the list of tests to execute, see the list above. +# # Each member of this list may contain these values: +# # NeedsPythonTestReqs: [true|false] - install the test-requirements prior to running tests. False if not set. +# # NeedsPythonFunctionalReqs: [true|false] - install the functional-requirements prior to running tests. False if not set. +# # PythonVersion: 'M.m' - the Python version to run. DefaultPythonVersion if not set. +# # NodeVersion: 'x.y.z' - Node version to use. DefaultNodeVersion if not set. +# # SkipXvfb: [true|false] - skip initialization of xvfb prior to running system tests on Linux. False if not set +# # UploadBinary: [true|false] - upload test binaries to Azure if true. False if not set. +# 'Win-Py3.7 Unit': +# VMImageName: 'vs2017-win2016' +# TestsToRun: 'runHygiene, testUnitTests, pythonUnitTests, pythonInternalTools' +# NeedsPythonTestReqs: true +# 'Win-Py2.7 Unit': +# PythonVersion: '2.7' +# VMImageName: 'vs2017-win2016' +# TestsToRun: 'pythonUnitTests' +# NeedsPythonTestReqs: true +# 'Mac-Py3.7 Unit': +# PythonVersion: '3.7' +# VMImageName: 'macos-10.13' +# TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools' +# NeedsPythonTestReqs: true +# 'Mac-Py3.7 Single Workspace': +# PythonVersion: '3.7' +# VMImageName: 'macos-10.13' +# TestsToRun: 'testSingleWorkspace' +# NeedsPythonTestReqs: true +# 'Mac-Py2.7 Unit+Single': +# PythonVersion: '2.7' +# VMImageName: 'macos-10.13' +# TestsToRun: 'testUnitTests, pythonUnitTests, testSingleWorkspace' +# NeedsPythonTestReqs: true +# 'Linux-Py3.7 Unit': +# PythonVersion: '3.7' +# VMImageName: 'ubuntu-16.04' +# TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools' +# NeedsPythonTestReqs: true +# 'Linux-Py3.7 Single Workspace': +# PythonVersion: '3.7' +# VMImageName: 'ubuntu-16.04' +# TestsToRun: 'testSingleWorkspace' +# NeedsPythonTestReqs: true +# 'Linux-Py2.7 Unit+Single': +# PythonVersion: '2.7' +# VMImageName: 'ubuntu-16.04' +# TestsToRun: 'testUnitTests, pythonUnitTests, testSingleWorkspace' +# NeedsPythonTestReqs: true +# 'Linux-Py3.7 Functional': +# PythonVersion: '3.7' +# VMImageName: 'ubuntu-16.04' +# TestsToRun: 'testfunctional' +# NeedsPythonTestReqs: true +# NeedsPythonFunctionalReqs: true - pool: - vmImage: $(VMImageName) +# pool: +# vmImage: $(VMImageName) - steps: - - template: templates/test_phases.yml +# steps: +# - template: templates/test_phases.yml + +- template: templates/build_compile_jobs.yml + +- template: templates/uitest_jobs.yml + parameters: + jobs: + - test: "Smoke" + tags: "--tags=@wip" + # tags: "--tags=@smoke --tags=@wip" + # ignorePythonVersions: "3.6,3.5" + ignorePythonVersions: "3.6" diff --git a/gulpfile.js b/gulpfile.js index 70f573dc28cd..2a726e862dca 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -83,10 +83,10 @@ gulp.task('compile', (done) => { const tsProject = ts.createProject("tsconfig.json"); tsProject.src() .pipe(tsProject()) - .on("error", ()=> failed = true) + .on("error", () => failed = true) .js .pipe(gulp.dest("out")) - .on("finish", ()=> failed ? done(new Error('TypeScript compilation errors')) : done()); + .on("finish", () => failed ? done(new Error('TypeScript compilation errors')) : done()); }); gulp.task('precommit', (done) => run({ exitOnError: true, mode: 'staged' }, done)); diff --git a/ms-python-insiders.vsix b/ms-python-insiders.vsix new file mode 100644 index 000000000000..d9bcac64d463 Binary files /dev/null and b/ms-python-insiders.vsix differ diff --git a/package-lock.json b/package-lock.json index a33925305276..698a54c97f11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1364,6 +1364,12 @@ "through2": "^2.0.3" } }, + "@istanbuljs/nyc-config-typescript": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-0.1.3.tgz", + "integrity": "sha512-EzRFg92bRSD1W/zeuNkeGwph0nkWf+pP2l/lYW4/5hav7RjKKBN5kV1Ix7Tvi0CMu3pC4Wi/U7rNisiJMR3ORg==", + "dev": true + }, "@jupyterlab/coreutils": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-2.2.1.tgz", @@ -1907,12 +1913,6 @@ "integrity": "sha512-ZBFR7TROLVzCkswA3Fmqq+IIJt62/T7aY/Dmz+QkU7CaW2QFqAitCE8Ups7IzmGhcN1YWMBT4Qcoc07jU9hOJQ==", "dev": true }, - "@types/q": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", - "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", - "dev": true - }, "@types/react": { "version": "16.4.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.4.14.tgz", @@ -2551,6 +2551,12 @@ "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, + "arg": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", + "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2625,6 +2631,12 @@ "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", "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-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -3361,6 +3373,14 @@ } } }, + "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=", + "requires": { + "inherits": "~2.0.0" + } + }, "bluebird": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", @@ -3768,6 +3788,24 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "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.0.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, "caniuse-lite": { "version": "1.0.30000887", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000887.tgz", @@ -4522,6 +4560,15 @@ "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", @@ -4664,8 +4711,7 @@ "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 + "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=" }, "cloneable-readable": { "version": "1.1.2", @@ -4716,51 +4762,12 @@ "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" }, "coa": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", - "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", + "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=", "dev": true, "requires": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", "q": "^1.1.2" - }, - "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.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.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.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } } }, "code-point-at": { @@ -5012,12 +5019,6 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", "dev": true }, - "cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", - "dev": true - }, "copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", @@ -5333,12 +5334,6 @@ } } }, - "css-select-base-adapter": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", - "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", - "dev": true - }, "css-selector-tokenizer": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz", @@ -5385,21 +5380,15 @@ } }, "css-tree": { - "version": "1.0.0-alpha.29", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.29.tgz", - "integrity": "sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==", + "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.0", + "mdn-data": "^1.0.0", "source-map": "^0.5.3" } }, - "css-url-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/css-url-regex/-/css-url-regex-1.1.0.tgz", - "integrity": "sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w=", - "dev": true - }, "css-what": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", @@ -5413,12 +5402,12 @@ "dev": true }, "csso": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz", - "integrity": "sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==", + "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-alpha.29" + "css-tree": "1.0.0-alpha25" } }, "cssom": { @@ -5442,6 +5431,51 @@ "integrity": "sha512-Nt5VDyOTIIV4/nRFswoCKps1R5CD1hkiyjBE9/thNaNZILLEviVw9yWQw15+O+CpNjQKB/uvdcxFFOrSflY3Yw==", "dev": true }, + "cucumber-html-reporter": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/cucumber-html-reporter/-/cucumber-html-reporter-4.0.5.tgz", + "integrity": "sha512-fhIIvna2KwxEq9XxAkbfPodY1ZBvtIos8fD5ZwaK9mrmpBBVcEfmE9CwERyWcaDpIXvJfo1nQTZanMjG9/xc8Q==", + "dev": true, + "requires": { + "find": "^0.2.7", + "fs-extra": "^3.0.1", + "js-base64": "^2.3.2", + "jsonfile": "^3.0.0", + "lodash": "^4.17.5", + "opn": "5.3.0" + }, + "dependencies": { + "fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^3.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + } + } + }, + "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.1" + } + }, "cyclist": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", @@ -5888,6 +5922,15 @@ } } }, + "deep-assign": { + "version": "1.0.0", + "resolved": "http://registry.npmjs.org/deep-assign/-/deep-assign-1.0.0.tgz", + "integrity": "sha1-sJJ0O+hCfcYh6gBnzex+cN0Z83s=", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, "deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -5897,6 +5940,12 @@ "type-detect": "^4.0.0" } }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -6265,8 +6314,7 @@ "duplexer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", - "dev": true + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" }, "duplexer3": { "version": "0.1.4", @@ -6352,6 +6400,46 @@ "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==", "dev": true }, + "electron-download": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-4.1.1.tgz", + "integrity": "sha512-FjEWG9Jb/ppK/2zToP+U5dds114fM1ZOJqMAR4aXXL5CvyPE9fiqBK/9YcwC9poIFQTEJk/EM/zyRwziziRZrg==", + "dev": true, + "requires": { + "debug": "^3.0.0", + "env-paths": "^1.0.0", + "fs-extra": "^4.0.1", + "minimist": "^1.2.0", + "nugget": "^2.0.1", + "path-exists": "^3.0.0", + "rc": "^1.2.1", + "semver": "^5.4.1", + "sumchecker": "^2.0.2" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, "electron-to-chromium": { "version": "1.3.71", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.71.tgz", @@ -6444,6 +6532,12 @@ "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", "dev": true }, + "env-paths": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz", + "integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=", + "dev": true + }, "env-variable": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz", @@ -6730,7 +6824,6 @@ "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", @@ -6995,6 +7088,29 @@ } } }, + "extract-zip": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz", + "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", + "dev": true, + "requires": { + "concat-stream": "1.6.2", + "debug": "2.6.9", + "mkdirp": "0.5.1", + "yauzl": "2.4.1" + }, + "dependencies": { + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "requires": { + "fd-slicer": "~1.0.1" + } + } + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -7190,6 +7306,15 @@ "unpipe": "~1.0.0" } }, + "find": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/find/-/find-0.2.9.tgz", + "integrity": "sha1-S3Px/55WrZG3bnFkB/5f/mVUu4w=", + "dev": true, + "requires": { + "traverse-chain": "~0.1.0" + } + }, "find-cache-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", @@ -7325,12 +7450,6 @@ "mime-types": "^2.1.12" } }, - "formidable": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", - "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", - "dev": true - }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -7355,8 +7474,7 @@ "from": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", - "dev": true + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=" }, "from2": { "version": "2.3.0", @@ -7955,6 +8073,17 @@ } } }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -8003,6 +8132,12 @@ "npm-conf": "^1.1.0" } }, + "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", @@ -8529,6 +8664,17 @@ } } }, + "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.0", + "through2": "^2.0.0" + } + }, "gulp-filter": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/gulp-filter/-/gulp-filter-5.1.0.tgz", @@ -8540,13 +8686,52 @@ "streamfilter": "^1.0.5" } }, + "gulp-gunzip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gulp-gunzip/-/gulp-gunzip-1.1.0.tgz", + "integrity": "sha512-3INeprGyz5fUtAs75k6wVslGuRZIjKAoQp39xA7Bz350ReqkrfYaLYqjZ67XyIfLytRXdzeX04f+DnBduYhQWw==", + "dev": true, + "requires": { + "through2": "~2.0.3", + "vinyl": "~2.0.1" + }, + "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": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "vinyl": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.0.2.tgz", + "integrity": "sha1-CjcT2NTpIhxY8QyhbAEWyeJe2nw=", + "dev": true, + "requires": { + "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" + } + } + } + }, "gulp-inline-source": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/gulp-inline-source/-/gulp-inline-source-4.0.0.tgz", - "integrity": "sha512-LrqCom18aYkceV2YPqJyCrDpHNyLGrHHNYCmWQXFvJUMyYIpf+xxBzmvR45cokyoq2KW9H8oxijvnDjHn9s3Pw==", + "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": "~6.1.8", + "inline-source": "~5.2.6", "plugin-error": "~1.0.1", "through2": "~2.0.0" }, @@ -8771,6 +8956,70 @@ } } }, + "gulp-untar": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/gulp-untar/-/gulp-untar-0.0.8.tgz", + "integrity": "sha512-mqW7v2uvrxd8IoCCwJ04sPYgWjR3Gsi6yfhVWBK3sFMDP7FuoT7GNmxrCMwkk4RWqQohx8DRv+cDq4SRDXATGA==", + "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": { + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "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.0", + "clone-stats": "^0.0.1", + "replace-ext": "0.0.1" + } + } + } + }, + "gulp-vinyl-zip": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/gulp-vinyl-zip/-/gulp-vinyl-zip-2.1.2.tgz", + "integrity": "sha512-wJn09jsb8PyvUeyFF7y7ImEJqJwYy40BqL9GKfJs6UGpaGW9A+N68Q+ajsIpb9AeR6lAdjMbIdDPclIGo1/b7Q==", + "dev": true, + "requires": { + "event-stream": "3.3.4", + "queue": "^4.2.1", + "through2": "^2.0.3", + "vinyl": "^2.0.2", + "vinyl-fs": "^3.0.3", + "yauzl": "^2.2.1", + "yazl": "^2.2.1" + }, + "dependencies": { + "queue": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/queue/-/queue-4.5.1.tgz", + "integrity": "sha512-AMD7w5hRXcFSb8s9u38acBZ+309u6GsiibP4/0YacJeaurRshogB7v/ZcVPxP5gD5+zIw6ixRHdutiYUJfwKHw==", + "dev": true, + "requires": { + "inherits": "~2.0.0" + } + } + } + }, "gulplog": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", @@ -9374,6 +9623,15 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "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.0" + } + }, "indexof": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", @@ -9401,56 +9659,33 @@ "dev": true }, "inline-source": { - "version": "6.1.9", - "resolved": "https://registry.npmjs.org/inline-source/-/inline-source-6.1.9.tgz", - "integrity": "sha512-uzmcBBHgUwpZnZn4axPdr0PDSUVyzAOhJRJwyxMz96fPBCqN82QcnAUm5PTCtouzDnBFBKsj+VTTCzbxtnpT2w==", + "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.5.1", - "htmlparser2": "~3.10.0", - "superagent": "~4.1.0", - "svgo": "~1.1.1", - "terser": "~3.14.1" + "csso": "3.4.x", + "htmlparser2": "3.9.x", + "is-plain-obj": "1.1.x", + "object-assign": "4.1.x", + "svgo": "0.7.x", + "uglify-js": "3.3.x" }, "dependencies": { - "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true - }, - "htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", - "dev": true, - "requires": { - "domelementtype": "^1.3.1", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.1.1" - } - }, - "readable-stream": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz", - "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } + "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.2.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", - "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "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": { - "safe-buffer": "~5.1.0" + "commander": "~2.15.0", + "source-map": "~0.6.1" } } } @@ -9736,6 +9971,15 @@ "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.0" + } + }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", @@ -9795,6 +10039,12 @@ "integrity": "sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=", "dev": true }, + "is-obj": { + "version": "1.0.1", + "resolved": "http://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", @@ -10258,9 +10508,9 @@ } }, "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -10317,9 +10567,9 @@ } }, "istanbul-reports": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.4.tgz", - "integrity": "sha512-QCHGyZEK0bfi9GR215QSm+NJwFKEShbtc7tfbUdLAEzn3kKhLDDZqvljn8rPZM9v8CEOhzL1nlYoO4r1ryl67w==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", + "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", "dev": true, "requires": { "handlebars": "^4.1.2" @@ -10335,6 +10585,12 @@ "is-object": "^1.0.1" } }, + "js-base64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", + "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", + "dev": true + }, "js-beautify": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.7.5.tgz", @@ -10652,6 +10908,12 @@ "jsonify": "~0.0.0" } }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -11179,6 +11441,16 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "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.0" + } + }, "lower-case": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", @@ -11218,6 +11490,12 @@ "pify": "^3.0.0" } }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "dev": true + }, "make-iterator": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", @@ -11242,11 +11520,16 @@ "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 + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=" }, "map-visit": { "version": "1.0.0", @@ -11333,9 +11616,9 @@ } }, "mdn-data": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz", - "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.2.0.tgz", + "integrity": "sha512-esDqNvsJB2q5V28+u7NdtdMg6Rmg4khQmAVSjUiX7BY/7haIv0K2yWM43hYp0or+3nvG7+UaTF1JHz31hgU1TA==", "dev": true }, "mdurl": { @@ -11393,6 +11676,24 @@ "readable-stream": "^2.0.1" } }, + "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.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-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -11637,6 +11938,12 @@ "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", "dev": true }, + "camelcase": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.2.0.tgz", + "integrity": "sha512-IXFsBS2pC+X0j0N/GE7Dm7j3bsEBp+oTpb7F50dwEVX7rf3IgwO9XatnegTsDtniKCUtEJH4fSU6Asw7uoVLfQ==", + "dev": true + }, "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -12294,6 +12601,29 @@ "boolbase": "~1.0.0" } }, + "nugget": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nugget/-/nugget-2.0.1.tgz", + "integrity": "sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA=", + "dev": true, + "requires": { + "debug": "^2.1.3", + "minimist": "^1.1.0", + "pretty-bytes": "^1.0.2", + "progress-stream": "^1.1.0", + "request": "^2.45.0", + "single-line-log": "^1.1.2", + "throttleit": "0.0.2" + }, + "dependencies": { + "throttleit": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", + "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=", + "dev": true + } + } + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -12313,9 +12643,9 @@ "dev": true }, "nyc": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-14.1.0.tgz", - "integrity": "sha512-iy9fEV8Emevz3z/AanIZsoGa8F4U2p0JKevZ/F0sk+/B2r9E6Qn+EPs0bpxEhnAt6UPlTL8mQZIaSJy8sK0ZFw==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-14.1.1.tgz", + "integrity": "sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw==", "dev": true, "requires": { "archy": "^1.0.0", @@ -12345,6 +12675,26 @@ "yargs-parser": "^13.0.0" }, "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.0" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, "convert-source-map": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", @@ -12405,9 +12755,9 @@ } }, "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -12518,13 +12868,24 @@ "strip-ansi": "^5.1.0" } }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, "yargs": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", - "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", + "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", "dev": true, "requires": { - "cliui": "^4.0.0", + "cliui": "^5.0.0", "find-up": "^3.0.0", "get-caller-file": "^2.0.1", "os-locale": "^3.1.0", @@ -12534,7 +12895,7 @@ "string-width": "^3.0.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^13.0.0" + "yargs-parser": "^13.1.0" } }, "yargs-parser": { @@ -12750,6 +13111,15 @@ "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==", "dev": true }, + "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==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } + }, "optimist": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", @@ -13205,7 +13575,6 @@ "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" } @@ -13488,6 +13857,16 @@ "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", "dev": true }, + "pretty-bytes": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz", + "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1", + "meow": "^3.1.0" + } + }, "pretty-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", @@ -13527,6 +13906,61 @@ "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", "dev": true }, + "progress-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-1.2.0.tgz", + "integrity": "sha1-LNPP6jO6OonJwSHsM0er6asSX3c=", + "dev": true, + "requires": { + "speedometer": "~0.1.2", + "through2": "~0.2.3" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", + "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.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", + "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", + "dev": true, + "requires": { + "readable-stream": "~1.1.9", + "xtend": "~2.1.1" + } + }, + "xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "dev": true, + "requires": { + "object-keys": "~0.4.0" + } + } + } + }, "promise": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/promise/-/promise-8.0.1.tgz", @@ -13767,6 +14201,18 @@ "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=", "dev": true }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, "react": { "version": "16.5.2", "resolved": "https://registry.npmjs.org/react/-/react-16.5.2.tgz", @@ -14100,6 +14546,16 @@ } } }, + "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", @@ -14389,6 +14845,21 @@ "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.0" + } + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + }, "replace-homedir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", @@ -14592,7 +15063,6 @@ "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.0.5" } @@ -15066,6 +15536,46 @@ } } }, + "single-line-log": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-1.1.2.tgz", + "integrity": "sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q=", + "dev": true, + "requires": { + "string-width": "^1.0.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "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.0.0" + } + } + } + }, "slash": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", @@ -15312,11 +15822,16 @@ "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", "dev": true }, + "speedometer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-0.1.4.tgz", + "integrity": "sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0=", + "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" } @@ -15359,12 +15874,6 @@ "safe-buffer": "^5.1.1" } }, - "stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true - }, "stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -15376,6 +15885,12 @@ "integrity": "sha1-Gsig2Ug4SNFpXkGLbQMaPDzmjjs=", "dev": true }, + "stat-mode": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.2.2.tgz", + "integrity": "sha1-5sgLYjEj19gM8TLOU480YokHJQI=", + "dev": true + }, "state-toggle": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.1.tgz", @@ -15429,7 +15944,6 @@ "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" } @@ -15524,8 +16038,7 @@ "streamifier": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/streamifier/-/streamifier-0.1.1.tgz", - "integrity": "sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8=", - "dev": true + "integrity": "sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8=" }, "strict-uri-encode": { "version": "1.1.0", @@ -15624,6 +16137,15 @@ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, + "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-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -15689,81 +16211,13 @@ "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-8.2.0.tgz", "integrity": "sha512-n5Nv2lIZaWfVBg10EWC8yaJCB6xV7sEsuaISAVFIS9F4fTRjy/O35A82lkweKuSqQItDlKOGQpTHK9/udQhRRw==" }, - "superagent": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-4.1.0.tgz", - "integrity": "sha512-FT3QLMasz0YyCd4uIi5HNe+3t/onxMyEho7C3PSqmti3Twgy2rXT4fmkTz6wRL6bTF4uzPcfkUCa8u4JWHw8Ag==", + "sumchecker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-2.0.2.tgz", + "integrity": "sha1-D0LBDl0F2l1C7qPlbDOZo31sWz4=", "dev": true, "requires": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.1.2", - "debug": "^4.1.0", - "form-data": "^2.3.3", - "formidable": "^1.2.0", - "methods": "^1.1.1", - "mime": "^2.4.0", - "qs": "^6.6.0", - "readable-stream": "^3.0.6" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "mime": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.2.tgz", - "integrity": "sha512-zJBfZDkwRu+j3Pdd2aHsR5GfH2jIWhmL1ZzBoc+X+3JEti2hbArWcyJ+1laC1D2/U/W1a/+Cegj0/OnEU2ybjg==", - "dev": true - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "dev": true - }, - "readable-stream": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz", - "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "string_decoder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", - "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } + "debug": "^2.2.0" } }, "supports-color": { @@ -15825,25 +16279,18 @@ } }, "svgo": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.1.1.tgz", - "integrity": "sha512-GBkJbnTuFpM4jFbiERHDWhZc/S/kpHToqmZag3aEBjPYK44JAN2QBjvrGIxLOoCyMZjuFQIfTO2eJd8uwLY/9g==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz", + "integrity": "sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=", "dev": true, "requires": { - "coa": "~2.0.1", + "coa": "~1.0.1", "colors": "~1.1.2", - "css-select": "^2.0.0", - "css-select-base-adapter": "~0.1.0", - "css-tree": "1.0.0-alpha.28", - "css-url-regex": "^1.1.0", - "csso": "^3.5.0", - "js-yaml": "^3.12.0", + "csso": "~2.3.1", + "js-yaml": "~3.7.0", "mkdirp": "~0.5.1", - "object.values": "^1.0.4", - "sax": "~1.2.4", - "stable": "~0.1.6", - "unquote": "~1.1.1", - "util.promisify": "~1.0.0" + "sax": "~1.2.1", + "whet.extend": "~0.9.9" }, "dependencies": { "colors": { @@ -15852,41 +16299,24 @@ "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", "dev": true }, - "css-select": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz", - "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^2.1.2", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "css-tree": { - "version": "1.0.0-alpha.28", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.28.tgz", - "integrity": "sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w==", + "csso": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz", + "integrity": "sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=", "dev": true, "requires": { - "mdn-data": "~1.1.0", + "clap": "^1.0.9", "source-map": "^0.5.3" } }, - "css-what": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", - "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", - "dev": true - }, - "nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "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": { - "boolbase": "~1.0.0" + "argparse": "^1.0.7", + "esprima": "^2.6.0" } } } @@ -15914,6 +16344,16 @@ "integrity": "sha512-IlqtmLVaZA2qab8epUXbVWRn3aB1imbDMJtjB3nu4X0NqPkcY/JH9ZtCBWKHWPxs8Svi9tyo8w2dBoi07qZbBA==", "dev": true }, + "tar": { + "version": "2.2.1", + "resolved": "http://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "requires": { + "block-stream": "*", + "fstream": "^1.0.2", + "inherits": "2" + } + }, "tar-stream": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz", @@ -15970,31 +16410,6 @@ } } }, - "terser": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-3.14.1.tgz", - "integrity": "sha512-NSo3E99QDbYSMeJaEk9YW2lTg3qS9V0aKGlb+PlOrei1X02r1wSBHCNX/O+yeTRFSWPKPIGj6MqvvdqV4rnVGw==", - "dev": true, - "requires": { - "commander": "~2.17.1", - "source-map": "~0.6.1", - "source-map-support": "~0.5.6" - }, - "dependencies": { - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", - "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 - } - } - }, "terser-webpack-plugin": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.3.tgz", @@ -16235,9 +16650,9 @@ } }, "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -16343,8 +16758,7 @@ "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { "version": "2.0.3", @@ -16579,6 +16993,12 @@ } } }, + "traverse-chain": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", + "integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=", + "dev": true + }, "tree-kill": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.0.tgz", @@ -16590,6 +17010,12 @@ "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", "dev": true }, + "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", @@ -16687,6 +17113,19 @@ "lodash": "^4.17.5" } }, + "ts-node": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.1.0.tgz", + "integrity": "sha512-34jpuOrxDuf+O6iW1JpgTRDFynUZ1iEqtYruBqh35gICNjN8x+LpVcPAcwzLPi9VU6mdA3ym+x233nZmZp445A==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "^3.0.0" + } + }, "tsconfig-paths": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.7.0.tgz", @@ -17320,13 +17759,25 @@ } }, "unique-stream": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", - "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", "dev": true, "requires": { - "json-stable-stringify": "^1.0.0", - "through2-filter": "^2.0.0" + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + }, + "dependencies": { + "through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "dev": true, + "requires": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + } } }, "unist-util-is": { @@ -17387,12 +17838,6 @@ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", "dev": true }, - "unquote": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", - "dev": true - }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", @@ -17675,6 +18120,40 @@ "unist-util-stringify-position": "^1.1.1" } }, + "vinyl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", + "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "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-fs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", @@ -17700,18 +18179,6 @@ "vinyl-sourcemap": "^1.1.0" }, "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "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 - }, "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", @@ -17733,12 +18200,6 @@ "util-deprecate": "~1.0.1" } }, - "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", @@ -17747,20 +18208,6 @@ "requires": { "safe-buffer": "~5.1.0" } - }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "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" - } } } }, @@ -17936,6 +18383,11 @@ "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", "dev": true }, + "clone": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=" + }, "commander": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", @@ -17975,6 +18427,77 @@ "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", "dev": true }, + "gulp-gunzip": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulp-gunzip/-/gulp-gunzip-1.0.0.tgz", + "integrity": "sha1-FbdBFF6Dqcb1CIYkG1fMWHHxUak=", + "requires": { + "through2": "~0.6.5" + } + }, + "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==", + "requires": { + "event-stream": "~3.3.4", + "streamifier": "~0.1.1", + "tar": "^2.2.1", + "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=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "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==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "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==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "requires": { + "clone": "^1.0.0", + "clone-stats": "^0.0.1" + } + } + } + }, "har-validator": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", @@ -17991,6 +18514,11 @@ "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", "dev": true }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -18079,6 +18607,14 @@ "has-flag": "^2.0.0" } }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "requires": { + "xtend": ">=4.0.0 <4.1.0-0" + } + }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", @@ -18677,6 +19213,12 @@ "webidl-conversions": "^4.0.2" } }, + "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", @@ -18947,8 +19489,7 @@ "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" }, "y18n": { "version": "4.0.0", @@ -19069,6 +19610,12 @@ "yargs": "^12.0.5" }, "dependencies": { + "camelcase": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.2.0.tgz", + "integrity": "sha512-IXFsBS2pC+X0j0N/GE7Dm7j3bsEBp+oTpb7F50dwEVX7rf3IgwO9XatnegTsDtniKCUtEJH4fSU6Asw7uoVLfQ==", + "dev": true + }, "find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -19160,6 +19707,12 @@ "buffer-crc32": "~0.2.3" } }, + "yn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.0.tgz", + "integrity": "sha512-kKfnnYkbTfrAdd0xICNFw7Atm8nKpLcLv9AZGEt+kczL/WQVai4e2V6ZN8U/O+iI6WrNuJjNNOyu4zfhl9D3Hg==", + "dev": true + }, "zone.js": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.7.6.tgz", diff --git a/package.json b/package.json index e94bfd9d7700..5c1079e765b3 100644 --- a/package.json +++ b/package.json @@ -2133,7 +2133,8 @@ "debugger-coverage": "gulp debugger-coverage", "cover:inlinesource": "gulp inlinesource", "updateBuildNumber": "gulp updateBuildNumber", - "verifyBundle": "gulp verifyBundle" + "verifyBundle": "gulp verifyBundle", + "test_coverage": "nyc -r cobertura -e .ts -x \"./src/test/**/*.unit.test.ts\" mocha --require out/test/unittests.js --ui tdd -r ts-node/register src/test/**/*.unit.test.ts --reporter mocha-multi-reporters --reporter-options configFile=build/.mocha-multi-reporters.config --colors && nyc report" }, "dependencies": { "@jupyterlab/services": "^3.2.1", @@ -2195,6 +2196,7 @@ "@babel/preset-env": "^7.1.0", "@babel/preset-react": "^7.0.0", "@babel/register": "^7.4.4", + "@istanbuljs/nyc-config-typescript": "^0.1.3", "@nteract/plotly": "^1.47.1", "@nteract/transform-dataresource": "^4.3.5", "@nteract/transform-geojson": "^3.2.3", @@ -2257,22 +2259,29 @@ "copy-webpack-plugin": "^4.6.0", "cross-spawn": "^6.0.5", "css-loader": "^1.0.1", + "cucumber-html-reporter": "^4.0.5", "decache": "^4.4.0", "del": "^3.0.0", "download": "^7.0.0", + "electron-download": "^4.1.1", "enzyme": "^3.7.0", "enzyme-adapter-react-16": "^1.6.0", "event-stream": "3.3.4", + "extract-zip": "^1.6.7", "file-loader": "^2.0.0", "flat": "^4.0.0", "gulp": "^4.0.0", "gulp-azure-storage": "^0.9.0", + "gulp-chmod": "^2.0.0", "gulp-filter": "^5.1.0", - "gulp-inline-source": "^4.0.0", + "gulp-gunzip": "^1.1.0", + "gulp-inline-source": "^3.2.0", "gulp-json-editor": "^2.2.2", "gulp-rename": "^1.4.0", "gulp-sourcemaps": "^2.6.4", "gulp-typescript": "^4.0.1", + "gulp-untar": "0.0.8", + "gulp-vinyl-zip": "^2.1.2", "html-webpack-plugin": "^3.2.0", "husky": "^1.1.2", "immutable": "^4.0.0-rc.12", @@ -2290,7 +2299,7 @@ "monaco-editor-webpack-plugin": "^1.7.0", "node-has-native-dependencies": "^1.0.2", "node-html-parser": "^1.1.13", - "nyc": "^14.1.0", + "nyc": "^14.1.1", "raw-loader": "^0.5.1", "react": "^16.5.2", "react-data-grid": "^6.0.2-0", @@ -2311,6 +2320,7 @@ "terser-webpack-plugin": "^1.2.3", "ts-loader": "^5.3.0", "ts-mockito": "^2.3.1", + "ts-node": "^8.1.0", "tsconfig-paths-webpack-plugin": "^3.2.0", "tslint-eslint-rules": "^5.1.0", "tslint-microsoft-contrib": "^5.0.3", @@ -2320,6 +2330,7 @@ "typescript-formatter": "^7.1.0", "url-loader": "^1.1.2", "uuid": "^3.3.2", + "vinyl-fs": "^3.0.3", "vsce": "^1.59.0", "vscode": "^1.1.33", "vscode-debugadapter-testsupport": "^1.27.0", diff --git a/src/client/common/logger.ts b/src/client/common/logger.ts index 0c58f15f1262..f333642e8f8e 100644 --- a/src/client/common/logger.ts +++ b/src/client/common/logger.ts @@ -25,14 +25,14 @@ const logLevelMap = { }; function log(logLevel: LogLevel, ...args: any[]) { - if (consoleLogger.transports.length > 0){ + if (consoleLogger.transports.length > 0) { const message = args.length === 0 ? '' : util.format(args[0], ...args.slice(1)); consoleLogger.log(logLevelMap[logLevel], message); } logToFile(logLevel, ...args); } function logToFile(logLevel: LogLevel, ...args: any[]) { - if (fileLogger.transports.length === 0){ + if (fileLogger.transports.length === 0) { return; } const message = args.length === 0 ? '' : util.format(args[0], ...args.slice(1)); @@ -119,7 +119,7 @@ function initializeConsoleLogger() { }; } - if (isTestExecution() && !process.env.VSC_PYTHON_FORCE_LOGGING){ + if (isTestExecution() && !process.env.VSC_PYTHON_FORCE_LOGGING) { // Do not log to console if running tests on CI and we're not asked to do so. return; } diff --git a/src/client/interpreter/locators/index.ts b/src/client/interpreter/locators/index.ts index 8111210e3d85..53cba0fecbda 100644 --- a/src/client/interpreter/locators/index.ts +++ b/src/client/interpreter/locators/index.ts @@ -1,5 +1,6 @@ import { inject, injectable } from 'inversify'; import { Disposable, Event, EventEmitter, Uri } from 'vscode'; +import { traceDecorators } from '../../common/logger'; import { IPlatformService } from '../../common/platform/types'; import { IDisposableRegistry } from '../../common/types'; import { createDeferred, Deferred } from '../../common/utils/async'; @@ -68,6 +69,7 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi * The optional resource arg may control where locators look for * interpreters. */ + @traceDecorators.verbose('Get Interpreters') public async getInterpreters(resource?: Uri): Promise { const locators = this.getLocators(); const promises = locators.map(async provider => provider.getInterpreters(resource)); diff --git a/src/client/interpreter/locators/services/cacheableLocatorService.ts b/src/client/interpreter/locators/services/cacheableLocatorService.ts index 8ddfc9fc6221..3a9ea19dba9f 100644 --- a/src/client/interpreter/locators/services/cacheableLocatorService.ts +++ b/src/client/interpreter/locators/services/cacheableLocatorService.ts @@ -8,7 +8,7 @@ import * as md5 from 'md5'; import { Disposable, Event, EventEmitter, Uri } from 'vscode'; import { IWorkspaceService } from '../../../common/application/types'; import '../../../common/extensions'; -import { Logger, traceVerbose } from '../../../common/logger'; +import { Logger, traceDecorators, traceVerbose } from '../../../common/logger'; import { IDisposableRegistry, IPersistentStateFactory } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { IServiceContainer } from '../../../ioc/types'; @@ -36,6 +36,7 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ return this._hasInterpreters.promise; } public abstract dispose(): void; + @traceDecorators.verbose('Get Interpreters in CacheableLocatorService') public async getInterpreters(resource?: Uri, ignoreCache?: boolean): Promise { const cacheKey = this.getCacheKey(resource); let deferred = this.promisesPerResource.get(cacheKey); @@ -51,6 +52,7 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ .then(async items => { await this.cacheInterpreters(items, resource); traceVerbose(`Interpreters returned by ${this.name} are of count ${Array.isArray(items) ? items.length : 0}`); + traceVerbose(`Interpreters returned by ${this.name} are ${JSON.stringify(items)}`); deferred!.resolve(items); }) .catch(ex => deferred!.reject(ex)); diff --git a/src/client/interpreter/locators/services/condaService.ts b/src/client/interpreter/locators/services/condaService.ts index 30c9cc0122cd..2dc9b312c3bd 100644 --- a/src/client/interpreter/locators/services/condaService.ts +++ b/src/client/interpreter/locators/services/condaService.ts @@ -4,7 +4,7 @@ import { compare, parse, SemVer } from 'semver'; import { ConfigurationChangeEvent, Uri } from 'vscode'; import { IWorkspaceService } from '../../../common/application/types'; -import { Logger } from '../../../common/logger'; +import { Logger, traceDecorators, traceVerbose } from '../../../common/logger'; import { IFileSystem, IPlatformService } from '../../../common/platform/types'; import { IProcessServiceFactory } from '../../../common/process/types'; import { IConfigurationService, IDisposableRegistry, ILogger, IPersistentStateFactory } from '../../../common/types'; @@ -210,6 +210,7 @@ export class CondaService implements ICondaService { /** * Return the list of conda envs (by name, interpreter filename). */ + @traceDecorators.verbose('Get Conda environments') public async getCondaEnvironments(ignoreCache: boolean): Promise<({ name: string; path: string }[]) | undefined> { // Global cache. // tslint:disable-next-line:no-any @@ -222,6 +223,7 @@ export class CondaService implements ICondaService { const condaFile = await this.getCondaFile(); const processService = await this.processServiceFactory.create(); const envInfo = await processService.exec(condaFile, ['env', 'list']).then(output => output.stdout); + traceVerbose(`Conda Env List ${envInfo}}`); const environments = this.condaHelper.parseCondaEnvironmentNames(envInfo); await globalPersistence.updateValue({ data: environments }); return environments; @@ -246,6 +248,7 @@ export class CondaService implements ICondaService { /** * Get the conda exe from the path to an interpreter's python. This might be different than the globally registered conda.exe */ + @traceDecorators.verbose('Get Conda File from interpreter') public async getCondaFileFromInterpreter(interpreterPath?: string, envName?: string): Promise { const condaExe = this.platform.isWindows ? 'conda.exe' : 'conda'; const scriptsDir = this.platform.isWindows ? 'Scripts' : 'bin'; diff --git a/src/client/telemetry/importTracker.ts b/src/client/telemetry/importTracker.ts index 9cf4af027593..37925632fd4c 100644 --- a/src/client/telemetry/importTracker.ts +++ b/src/client/telemetry/importTracker.ts @@ -8,7 +8,7 @@ import * as path from 'path'; import { TextDocument } from 'vscode'; import { sendTelemetryEvent } from '.'; -import { noop } from '../../test/core'; +import { noop } from '../common/utils/misc'; import { IDocumentManager } from '../common/application/types'; import { isTestExecution } from '../common/constants'; import { EventName } from './constants'; diff --git a/src/datascience-ui/history-react/MainPanel.tsx b/src/datascience-ui/history-react/MainPanel.tsx index f2c507118796..34b19b62a6bd 100644 --- a/src/datascience-ui/history-react/MainPanel.tsx +++ b/src/datascience-ui/history-react/MainPanel.tsx @@ -63,8 +63,12 @@ export class MainPanel extends React.Component submittedText: false, history: new InputHistory(), contentTop: 24, +<<<<<<< HEAD + editCellVM: getSettings && getSettings().allowInput ? createEditableCellVM(1) : undefined +======= editCellVM: getSettings && getSettings().allowInput ? createEditableCellVM(1) : undefined, editorOptions: this.computeEditorOptions() +>>>>>>> master }; // Add test state if necessary @@ -295,7 +299,10 @@ export class MainPanel extends React.Component
>>>>>> master history={this.state.history} maxTextSize={maxTextSize} autoFocus={document.hasFocus()} @@ -318,6 +325,8 @@ export class MainPanel extends React.Component ); } +<<<<<<< HEAD +======= private computeEditorOptions() : monacoEditor.editor.IEditorOptions { const intellisenseOptions = getSettings().intellisenseOptions; if (intellisenseOptions) { @@ -345,6 +354,7 @@ export class MainPanel extends React.Component return {}; } +>>>>>>> master // Called by the header control when size changes (such as expanding variables) private onHeaderHeightChange = (newHeight: number) => { this.setState({contentTop: newHeight}); @@ -971,6 +981,11 @@ export class MainPanel extends React.Component } } +<<<<<<< HEAD + private readOnlyCodeCreated = (text: string, file: string, id: string, monacoId: string) => { + // Pass this onto the completion provider running in the extension + this.sendMessage(HistoryMessages.AddCell, { text, file, id }); +======= private readOnlyCodeCreated = (_text: string, file: string, id: string, monacoId: string) => { const cell = this.state.cellVMs.find(c => c.cell.id === id); if (cell) { @@ -982,6 +997,7 @@ export class MainPanel extends React.Component id }); } +>>>>>>> master // Save in our map of monaco id to cell id this.monacoIdToCellId.set(monacoId, id); diff --git a/src/datascience-ui/history-react/cell.tsx b/src/datascience-ui/history-react/cell.tsx index 3d07c770d3fe..5de4aaf0bb30 100644 --- a/src/datascience-ui/history-react/cell.tsx +++ b/src/datascience-ui/history-react/cell.tsx @@ -11,10 +11,10 @@ import * as React from 'react'; import JSONTree from 'react-json-tree'; import '../../client/common/extensions'; +import { noop } from '../../client/common/utils/misc'; import { concatMultilineString, formatStreamText } from '../../client/datascience/common'; import { Identifiers } from '../../client/datascience/constants'; import { CellState, ICell } from '../../client/datascience/types'; -import { noop } from '../../test/core'; import { getLocString } from '../react-common/locReactSide'; import { getSettings } from '../react-common/settingsReactSide'; import './cell.css'; @@ -40,7 +40,10 @@ interface ICellProps { showWatermark: boolean; errorBackgroundColor: string; monacoTheme: string | undefined; +<<<<<<< HEAD +======= editorOptions: monacoEditor.editor.IEditorOptions; +>>>>>>> master gotoCode(): void; delete(): void; submitNewCode(code: string): void; @@ -64,12 +67,12 @@ export class Cell extends React.Component { constructor(prop: ICellProps) { super(prop); - this.state = {focused: this.props.autoFocus}; + this.state = { focused: this.props.autoFocus }; } public render() { if (this.props.cellVM.cell.data.cell_type === 'sys_info') { - return ; + return ; } else { return this.renderNormalCell(); } @@ -163,11 +166,11 @@ export class Cell extends React.Component { } } - private showInputs = () : boolean => { + private showInputs = (): boolean => { return (this.isCodeCell() && (this.props.cellVM.inputBlockShow || this.props.cellVM.editable)); } - private getRenderableInputCode = () : string => { + private getRenderableInputCode = (): string => { if (this.props.cellVM.editable) { return ''; } @@ -228,7 +231,11 @@ export class Cell extends React.Component { onCreated={this.onCodeCreated} outermostParentClass='cell-wrapper' monacoTheme={this.props.monacoTheme} +<<<<<<< HEAD + /> +======= /> +>>>>>>> master
); } else { @@ -244,7 +251,11 @@ export class Cell extends React.Component { this.props.onCodeCreated(code, this.props.cellVM.cell.file, this.props.cellVM.cell.id, modelId); } +<<<<<<< HEAD + private getCursorType = (): string => { +======= private getCursorType = () : string => { +>>>>>>> master if (getSettings && getSettings().extraSettings) { return getSettings().extraSettings.terminalCursor; } @@ -284,15 +295,15 @@ export class Cell extends React.Component { return []; } - private renderMarkdown = (markdown : nbformat.IMarkdownCell) => { + private renderMarkdown = (markdown: nbformat.IMarkdownCell) => { // React-markdown expects that the source is a string const source = concatMultilineString(markdown.source); const Transform = transforms['text/markdown']; - return []; + return []; } - private renderWithTransform = (mimetype: string, output : nbformat.IOutput, index : number, renderWithScrollbars: boolean, forceLightTheme: boolean) => { + private renderWithTransform = (mimetype: string, output: nbformat.IOutput, index: number, renderWithScrollbars: boolean, forceLightTheme: boolean) => { // If we found a mimetype, use the transform if (mimetype) { @@ -343,7 +354,7 @@ export class Cell extends React.Component { return
; } - private convertToLinearRgb(color: number) : number { + private convertToLinearRgb(color: number): number { let c = color / 255; if (c <= 0.03928) { c = c / 12.92; @@ -377,20 +388,20 @@ export class Cell extends React.Component { } } - private renderOutput = (output : nbformat.IOutput, index: number) => { + private renderOutput = (output: nbformat.IOutput, index: number) => { // Borrowed this from Don's Jupyter extension // First make sure we have the mime data if (!output) { - return
; + return
; } // Make a copy of our data so we don't modify our cell - const copy = {...output}; + const copy = { ...output }; // Special case for json if (copy.data && copy.data.hasOwnProperty('application/json')) { - return ; + return ; } // Only for text and error ouptut do we add scrollbars @@ -406,7 +417,7 @@ export class Cell extends React.Component { const multiline = concatMultilineString(stream.text); const formatted = formatStreamText(multiline); copy.data = { - 'text/html' : `${formatted}` + 'text/html': `${formatted}` }; // Output may have goofy ascii colorization chars in it. Try @@ -458,7 +469,7 @@ export class Cell extends React.Component { } else { mimetype = 'unknown'; } - const str : string = this.getUnknownMimeTypeFormatString().format(mimetype); + const str: string = this.getUnknownMimeTypeFormatString().format(mimetype); return
{str}
; } } diff --git a/src/datascience-ui/history-react/code.tsx b/src/datascience-ui/history-react/code.tsx index 2efb31245e41..5df96f4c22bc 100644 --- a/src/datascience-ui/history-react/code.tsx +++ b/src/datascience-ui/history-react/code.tsx @@ -22,7 +22,10 @@ export interface ICodeProps { showWatermark: boolean; monacoTheme: string | undefined; outermostParentClass: string; +<<<<<<< HEAD +======= editorOptions: monacoEditor.editor.IEditorOptions; +>>>>>>> master onSubmit(code: string): void; onCreated(code: string, modelId: string): void; onChange(changes: monacoEditor.editor.IModelContentChange[], modelId: string): void; @@ -85,9 +88,13 @@ export class Code extends React.Component { hideCursorInOverviewRuler: true, folding: false, readOnly: readOnly, +<<<<<<< HEAD + lineDecorationsWidth: 0 +======= lineDecorationsWidth: 0, contextmenu: false, ...this.props.editorOptions +>>>>>>> master }; return ( diff --git a/src/datascience-ui/history-react/contentPanel.tsx b/src/datascience-ui/history-react/contentPanel.tsx index 80ae1a05bbe5..357d7626cef8 100644 --- a/src/datascience-ui/history-react/contentPanel.tsx +++ b/src/datascience-ui/history-react/contentPanel.tsx @@ -6,7 +6,11 @@ import './contentPanel.css'; import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; import * as React from 'react'; +<<<<<<< HEAD +import { noop } from '../../client/common/utils/misc'; +======= import { noop } from '../../test/core'; +>>>>>>> master import { ErrorBoundary } from '../react-common/errorBoundary'; import { getSettings } from '../react-common/settingsReactSide'; import { Cell, ICellViewModel } from './cell'; @@ -22,7 +26,10 @@ export interface IContentPanelProps { submittedText: boolean; skipNextScroll: boolean; monacoTheme: string | undefined; +<<<<<<< HEAD +======= editorOptions: monacoEditor.editor.IEditorOptions; +>>>>>>> master gotoCellCode(index: number): void; deleteCell(index: number): void; onCodeChange(changes: monacoEditor.editor.IModelContentChange[], cellId: string, modelId: string): void; @@ -44,14 +51,18 @@ export class ContentPanel extends React.Component { } public render() { +<<<<<<< HEAD + return ( +======= return( +>>>>>>> master
{this.renderCells()}
-
+
); } @@ -65,7 +76,10 @@ export class ContentPanel extends React.Component { return this.props.cellVMs.map((cellVM: ICellViewModel, index: number) => >>>>>> master history={undefined} maxTextSize={maxTextSize} autoFocus={false} @@ -81,7 +95,11 @@ export class ContentPanel extends React.Component { onCodeChange={this.props.onCodeChange} onCodeCreated={this.props.onCodeCreated} monacoTheme={this.props.monacoTheme} +<<<<<<< HEAD + /> +======= /> +>>>>>>> master ); } @@ -92,7 +110,7 @@ export class ContentPanel extends React.Component { // yet so we need to delay. 10ms looks good from a user point of view setTimeout(() => { if (this.bottom) { - this.bottom.scrollIntoView({behavior: 'smooth', block : 'end', inline: 'end'}); + this.bottom.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'end' }); } }, 100); } diff --git a/src/datascience-ui/history-react/intellisenseProvider.ts b/src/datascience-ui/history-react/intellisenseProvider.ts index bd28d95801fd..e8c328290556 100644 --- a/src/datascience-ui/history-react/intellisenseProvider.ts +++ b/src/datascience-ui/history-react/intellisenseProvider.ts @@ -9,8 +9,12 @@ import { HistoryMessages, IHistoryMapping, IProvideCompletionItemsResponse, +<<<<<<< HEAD + IProvideHoverResponse +======= IProvideHoverResponse, IProvideSignatureHelpResponse +>>>>>>> master } from '../../client/datascience/history/historyTypes'; import { IMessageHandler, PostOffice } from '../react-common/postOffice'; @@ -19,6 +23,12 @@ interface IRequestData { cancelDisposable: monacoEditor.IDisposable; } +<<<<<<< HEAD +export class IntellisenseProvider implements monacoEditor.languages.CompletionItemProvider, monacoEditor.languages.HoverProvider, IDisposable, IMessageHandler { + public triggerCharacters?: string[] | undefined = ['.']; + private completionRequests: Map> = new Map>(); + private hoverRequests: Map> = new Map>(); +======= export class IntellisenseProvider implements monacoEditor.languages.CompletionItemProvider, monacoEditor.languages.HoverProvider, monacoEditor.languages.SignatureHelpProvider, IDisposable, IMessageHandler { public triggerCharacters?: string[] | undefined = ['.']; public readonly signatureHelpTriggerCharacters?: ReadonlyArray = ['(', ',', '<']; @@ -26,12 +36,16 @@ export class IntellisenseProvider implements monacoEditor.languages.CompletionIt private completionRequests: Map> = new Map>(); private hoverRequests: Map> = new Map>(); private signatureHelpRequests: Map> = new Map>(); +>>>>>>> master private registerDisposables: monacoEditor.IDisposable[] = []; constructor(private postOffice: PostOffice, private getCellId: (modelId: string) => string) { // Register a completion provider this.registerDisposables.push(monacoEditor.languages.registerCompletionItemProvider('python', this)); this.registerDisposables.push(monacoEditor.languages.registerHoverProvider('python', this)); +<<<<<<< HEAD +======= this.registerDisposables.push(monacoEditor.languages.registerSignatureHelpProvider('python', this)); +>>>>>>> master this.postOffice.addHandler(this); } @@ -75,6 +89,8 @@ export class IntellisenseProvider implements monacoEditor.languages.CompletionIt return promise.promise; } +<<<<<<< HEAD +======= public provideSignatureHelp( model: monacoEditor.editor.ITextModel, position: monacoEditor.Position, @@ -95,6 +111,7 @@ export class IntellisenseProvider implements monacoEditor.languages.CompletionIt return promise.promise; } +>>>>>>> master public dispose() { this.registerDisposables.forEach(r => r.dispose()); this.completionRequests.forEach(r => r.promise.resolve()); @@ -118,10 +135,13 @@ export class IntellisenseProvider implements monacoEditor.languages.CompletionIt this.handleHoverResponse(payload); return true; +<<<<<<< HEAD +======= case HistoryMessages.ProvideSignatureHelpResponse: this.handleSignatureHelpResponse(payload); return true; +>>>>>>> master default: break; } @@ -156,6 +176,8 @@ export class IntellisenseProvider implements monacoEditor.languages.CompletionIt } } +<<<<<<< HEAD +======= // Handle hover response // tslint:disable-next-line:no-any private handleSignatureHelpResponse = (payload?: any) => { @@ -170,6 +192,7 @@ export class IntellisenseProvider implements monacoEditor.languages.CompletionIt } } +>>>>>>> master private sendMessage(type: T, payload?: M[T]) { this.postOffice.sendMessage(type, payload); } diff --git a/src/datascience-ui/history-react/mainPanelState.ts b/src/datascience-ui/history-react/mainPanelState.ts index be0913cb5429..18d39e059041 100644 --- a/src/datascience-ui/history-react/mainPanelState.ts +++ b/src/datascience-ui/history-react/mainPanelState.ts @@ -6,11 +6,11 @@ import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; import * as path from 'path'; import { IDataScienceSettings } from '../../client/common/types'; +import { noop } from '../../client/common/utils/misc'; import { CellMatcher } from '../../client/datascience/cellMatcher'; import { concatMultilineString } from '../../client/datascience/common'; import { Identifiers } from '../../client/datascience/constants'; import { CellState, ICell, ISysInfo } from '../../client/datascience/types'; -import { noop } from '../../test/core'; import { ICellViewModel } from './cell'; import { InputHistory } from './inputHistory'; @@ -18,9 +18,9 @@ export interface IMainPanelState { cellVMs: ICellViewModel[]; editCellVM?: ICellViewModel; busy: boolean; - skipNextScroll? : boolean; - undoStack : ICellViewModel[][]; - redoStack : ICellViewModel[][]; + skipNextScroll?: boolean; + undoStack: ICellViewModel[][]; + redoStack: ICellViewModel[][]; submittedText: boolean; history: InputHistory; contentTop: number; @@ -29,7 +29,10 @@ export interface IMainPanelState { forceDark?: boolean; monacoTheme?: string; tokenizerLoaded?: boolean; +<<<<<<< HEAD +======= editorOptions: monacoEditor.editor.IEditorOptions; +>>>>>>> master } // tslint:disable-next-line: no-multiline-string @@ -46,24 +49,32 @@ const darkStyle = ` `; // This function generates test state when running under a browser instead of inside of -export function generateTestState(inputBlockToggled : (id: string) => void, filePath: string = '') : IMainPanelState { +export function generateTestState(inputBlockToggled: (id: string) => void, filePath: string = ''): IMainPanelState { return { +<<<<<<< HEAD + cellVMs: generateVMs(inputBlockToggled, filePath), +======= cellVMs : generateVMs(inputBlockToggled, filePath), +>>>>>>> master editCellVM: createEditableCellVM(1), busy: true, - skipNextScroll : false, - undoStack : [], - redoStack : [], + skipNextScroll: false, + undoStack: [], + redoStack: [], submittedText: false, history: new InputHistory(), contentTop: 24, rootStyle: darkStyle, +<<<<<<< HEAD + tokenizerLoaded: true +======= tokenizerLoaded: true, editorOptions: {} +>>>>>>> master }; } -export function createEditableCellVM(executionCount: number) : ICellViewModel { +export function createEditableCellVM(executionCount: number): ICellViewModel { return { cell: { @@ -89,7 +100,7 @@ export function createEditableCellVM(executionCount: number) : ICellViewModel { }; } -export function extractInputText(inputCell: ICell, settings: IDataScienceSettings | undefined) : string { +export function extractInputText(inputCell: ICell, settings: IDataScienceSettings | undefined): string { let source = inputCell.data.cell_type === 'code' ? inputCell.data.source : []; const matcher = new CellMatcher(settings); @@ -104,46 +115,46 @@ export function extractInputText(inputCell: ICell, settings: IDataScienceSetting return concatMultilineString(source); } -export function createCellVM(inputCell: ICell, settings: IDataScienceSettings | undefined, inputBlockToggled : (id: string) => void) : ICellViewModel { +export function createCellVM(inputCell: ICell, settings: IDataScienceSettings | undefined, inputBlockToggled: (id: string) => void): ICellViewModel { let inputLinesCount = 0; const inputText = inputCell.data.cell_type === 'code' ? extractInputText(inputCell, settings) : ''; if (inputText) { inputLinesCount = inputText.split('\n').length; } - return { - cell: inputCell, - editable: false, - inputBlockOpen: true, - inputBlockShow: true, - inputBlockText: inputText, - inputBlockCollapseNeeded: (inputLinesCount > 1), - inputBlockToggled: inputBlockToggled - }; + return { + cell: inputCell, + editable: false, + inputBlockOpen: true, + inputBlockShow: true, + inputBlockText: inputText, + inputBlockCollapseNeeded: (inputLinesCount > 1), + inputBlockToggled: inputBlockToggled + }; } -function generateVMs(inputBlockToggled : (id: string) => void, filePath: string) : ICellViewModel [] { +function generateVMs(inputBlockToggled: (id: string) => void, filePath: string): ICellViewModel[] { const cells = generateCells(filePath); - return cells.map((cell : ICell) => { + return cells.map((cell: ICell) => { return createCellVM(cell, undefined, inputBlockToggled); }); } -function generateCells(filePath: string) : ICell[] { +function generateCells(filePath: string): ICell[] { const cellData = generateCellData(); - return cellData.map((data : nbformat.ICodeCell | nbformat.IMarkdownCell | nbformat.IRawCell | ISysInfo, key : number) => { + return cellData.map((data: nbformat.ICodeCell | nbformat.IMarkdownCell | nbformat.IRawCell | ISysInfo, key: number) => { return { - id : key.toString(), - file : path.join(filePath, 'foo.py'), - line : 1, + id: key.toString(), + file: path.join(filePath, 'foo.py'), + line: 1, state: key === cellData.length - 1 ? CellState.executing : CellState.finished, - data : data + data: data }; }); } //tslint:disable:max-func-body-length -function generateCellData() : (nbformat.ICodeCell | nbformat.IMarkdownCell | nbformat.IRawCell | ISysInfo)[] { +function generateCellData(): (nbformat.ICodeCell | nbformat.IMarkdownCell | nbformat.IRawCell | ISysInfo)[] { // Hopefully new entries here can just be copied out of a jupyter notebook (ipynb) return [ @@ -151,7 +162,7 @@ function generateCellData() : (nbformat.ICodeCell | nbformat.IMarkdownCell | nbf // These are special. Sys_info is our own custom cell cell_type: 'sys_info', path: 'c:\\data\\python.exe', - version : '3.9.9.9 The Uber Version', + version: '3.9.9.9 The Uber Version', notebook_version: '(5, 9, 9)', source: [], metadata: {}, @@ -169,7 +180,11 @@ function generateCellData() : (nbformat.ICodeCell | nbformat.IMarkdownCell | nbf outputs: [ { data: { +<<<<<<< HEAD + // tslint:disable-next-line: no-multiline-string +======= // tslint:disable-next-line: no-multiline-string +>>>>>>> master 'text/html': [`
{ + const clampedCode = Math.min(code, 255); + let draining = 0; + + // Eagerly set the process's exit code in case stream.write doesn't + // execute its callback before the process terminates. + (process as any).exitCode = clampedCode; + + // flush output for Node.js Windows pipe bug + // https://github.com/joyent/node/issues/6247 is just one bug example + // https://github.com/visionmedia/mocha/issues/333 has a good discussion + const done = () => { +// tslint:disable-next-line: no-increment-decrement + if (!draining--) { + process.exit(clampedCode); + } + }; + + const streams = [process.stdout, process.stderr]; + + streams.forEach(stream => { + // submit empty write request and wait for completion + draining += 1; + stream.write('', done); + }); + + done(); +}; export function run(testsRoot: string, callback: TestCallback): void { // Enable source map support. @@ -126,7 +161,11 @@ export function run(testsRoot: string, callback: TestCallback): void { try { files.forEach(file => mocha.addFile(path.join(testsRoot, file))); initializationScript() - .then(() => mocha.run(failures => callback(undefined, failures))) + .then(() => mocha.run(failures => { + // Force exit, don't wait for process to die. + // callback(undefined, failures); + exitMocha(failures); + })) .catch(callback); } catch (error) { return callback(error); diff --git a/tslint.json b/tslint.json index eea5b3abf208..3560f7758468 100644 --- a/tslint.json +++ b/tslint.json @@ -10,7 +10,6 @@ "messages-must-be-localized": true, "no-unused-expression": true, "no-duplicate-variable": true, - "no-unused-variable": true, "curly": true, "non-literal-fs-path": false, "newline-per-chained-call": false, diff --git a/uitests/README.md b/uitests/README.md new file mode 100644 index 000000000000..8855ee390380 --- /dev/null +++ b/uitests/README.md @@ -0,0 +1,100 @@ +# xxx Tests for Python Extension. +## Usage + +Assuming you have created a virtual environment (for Python 3.7), +installed the `requirements.txt` dependencies, and activated the virtual environment: + +```shell +$ python uitests download +$ python uitests install +$ python uitests test +``` + +## Overview +* These are a set of UI tests for the Python Extension in VSC. +* The UI is driven using the [selenium webdriver](https://selenium-python.readthedocs.io/). +* [BDD](https://docs.cucumber.io/bdd/overview/) is used to create the tests using [Behave](https://behave.readthedocs.io/en/latest/). + + +## How does it work? +Here are the steps involved in running the tests: +* Setup environment: + * Download a completely fresh version of VS Code. + * Identify the version of [Electron](https://electronjs.org/) that VS Code is built upon. + * Download [Chrome Driver](http://chromedriver.chromium.org/) corresponding to the version of Electron used. + * When running tests, the `chrome driver` needs to be in the current path. + * Use [selenium webdriver](https://selenium-python.readthedocs.io/) to drive the VSC UI. + * Create a folder named `.vsccode-test` with the following sub-directories: +* When launching VSC, we will launch it as a completely stand alone version of VSC. + * I.e. even if it is installed on the current machine, we'll download and launch a new instance. + * This new instance will not interfere with currently installed version of VSC. + * All user settings, etc will be in a separate directory (see `user` folder). + * VSC will not have any extensions (see `extensions` folder). +* Automate VSC UI + * Launch VSC using the [Chrome Driver](http://chromedriver.chromium.org/) + * Use [selenium webdriver](https://selenium-python.readthedocs.io/) to drive the VSC UI. + * The tests are written and executed using [Behave](https://behave.readthedocs.io/en/latest/). +* Workspace folder/files + * Each [feature](https://docs.cucumber.io/gherkin/reference/#feature) can have its own set of files in the form of a github repo. + * Just add a tag with the path of the github repo url to the `feature`. + * When starting the tests for a feature, the repo is downloaded into `workspace folder`. + * At the begining of every scenario, the workspace folder is reset. + * This ensures each scenario starts with a clean workspace folder. +* Reports + * Test results are stored in the `reports` directory + * These `json` (`cucumber format`) report files are converted into HTML using an `npm` script [cucumber-html-reporter](https://www.npmjs.com/package/cucumber-html-reporter). + + +## Technology +* 99% of the code is written in `Python`. +* Downloading of `chrome driver` and generating `html reports` is done in `node.js` (using pre-existing `npm` packages). +* The tests are written using [Behave](https://behave.readthedocs.io/en/latest/) in `Python`. +* `GitHub` repos are used to provide the files to be used for testing in a workspace folder. +* The reports (`cucumber format`) are converted into HTML using an `npm` script [cucumber-html-reporter](https://www.npmjs.com/package/cucumber-html-reporter). + + +## Caveats +* **If using the `node.js` scripts from VSC itself, most of the following caveats do not apply. However as we're using Python, hence there are a few drawbacks.** +* VSC UI must be a top level window for elements to receive focus. Hence when running tests, do not do anything else. +* Screenshots are generally embedded into the HTML report, however as the number of tests grow so would the size of the generated `json` and resultant `html` file. + * If the size is too large then consider storing the `screenshots` as external files (see `--embed-screenshots` CLI arg). +* Reloading VSC will reset the connection (`driver` connection). + * Hence reloading is basically performed by re-starting VSC. +* As reloading of VSC is not a cheap operation, resettting workspaces is slow. + * Current approach is to `clone` a `git repo` directly into the workspace folder and use `git reset` for ever scenario. + * Note: Deleting workspace folder is not an option, as this would result in VSC loosing the workspace folder (things go south from there). +* `chrome driver` only supports arguments that begin with `--`. Hence arguments passed to VSC are limited to those that start with `--`. +* `Terminal` output cannot be retrieved using the `driver`. Hence output from terminal cannot be inspected. +* Sending characters to an input is slow, the `selenium driver` seems to send text one character at a time. Hence tests are slow. +* `Behave` does not generate reports that comply with the `cucumber json` report format. Hence the custom formatter in `report.py`. + * Using a `cucumber json` report format allows us to use existing tools to generate other HTML reports out of the raw `json` files. +* Sending keyboard commands to VSC is currently not possible (not known). + * `Selenium driver` can only send keyboard commands to a specific `html element`. + * But kyeboard commands such as `ctrl+p` are to be sent to the main window, and this isn't possible/not known. + * Fortunately almost everything in VSC can be driven through commands in the `command palette`. + * Hence, we have an extension that opens the `command palette`, from there, we use `selenium driver` to select commands. + * This same extension is used to `activate` the `Python extension`. + * This extension is referred to as the `bootstrap extension`. + + +## Files & Folders + +* The folder `.vsccode-test` in the root directory is where VSC is downloaded, workspace files created, etc. + * `extensions` This is where the extensions get installed for the instance of VSC used for testing. + * `reports` Location where generated reports are stored. + * `screenshots` Location where screenshots are stored. + * `stable` Loaction where stable version of VSC is downloaded and stored. + * `insiders` Loaction where insiders version of VSC is downloaded and stored. + * `user` Loaction where user information related to VSC is stored. + * `workspace folder`Workspace folder opened in VSC (this is where files used for smoke tests will be stored). +* `uitests/tests/bootstrap` This is where the source for the bootstrap extension is stored. +* `uitests/tests/features` Location where all `BDD features` are stored. +* `uitests/tests/steps` Location where all `BDD steps` are defined. +* `uitests/tests/js` Location with helper `js` files (download chrome driver and generate html reports). +* `uitests/tests/vscode` Contains all modules related to `vscode` (driving the UI, downloading, starting, etc). +* `environment.py` `enviroyment` file for `Behave`. + + +## Miscellaneous +* Use the debug configuration `Behave Smoke Tests` for debugging. +* In order to pass custom arguments to `Behave`, refer to the `CLI` (pass `behave` specific args after `--` in `python uitests test`). diff --git a/uitests/TODO.md b/uitests/TODO.md new file mode 100644 index 000000000000..373b14433ec1 --- /dev/null +++ b/uitests/TODO.md @@ -0,0 +1,6 @@ +* [ ] Dynamic detection of where `pyenv` environments are created and stored + * uitests/uitests/vscode/startup.py +* [ ] CRC compare files unzipped using python module and general unzipping. + * [ ] Identify whats wrong and file an issue upstream on Python if required. + * [ ] Use node.js as alternative +* [ ] Unzip using Python code diff --git a/uitests/__main__.py b/uitests/__main__.py new file mode 100644 index 000000000000..76b4f5720536 --- /dev/null +++ b/uitests/__main__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import runpy + +runpy.run_module("uitests", run_name="__main__", alter_sys=True) diff --git a/uitests/requirements.txt b/uitests/requirements.txt new file mode 100644 index 000000000000..fdb77ca305d7 --- /dev/null +++ b/uitests/requirements.txt @@ -0,0 +1,6 @@ +docopt~=0.6.2 +behave~=1.2.6 +requests~=2.21.0 +selenium~=3.141.0 +progress~=1.5 +junitparser~=1.3.2 diff --git a/uitests/uitests/__init__.py b/uitests/uitests/__init__.py new file mode 100644 index 000000000000..6c532b962985 --- /dev/null +++ b/uitests/uitests/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from . import vscode # noqa diff --git a/uitests/uitests/__main__.py b/uitests/uitests/__main__.py new file mode 100644 index 000000000000..0470eca57475 --- /dev/null +++ b/uitests/uitests/__main__.py @@ -0,0 +1,189 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +"""PVSC Smoke Tests. + +Usage: + uitests download [options] + uitests install [options] + uitests launch [options] + uitests test [options] [--] [ ...] + uitests report [options] + uitests (-h | --help) + +Options: + -h --help Show this screen. + -D, --destination=PATH Path for smoke tests [default: .vscode-test]. + -C, --channel=CHANNEL Defines the channel for VSC (stable or insiders) [default: stable]. + --vsix=VSIX Path to VSIX [default: ms-python-insiders.vsix]. + -O, --out=OUTPUT Output for test results (console or file) [default: file]. + --embed_screenshots Whether to embed screenshots (applicable only when using --out=file). + -L, --log=LEVEL Log Level [default: ERROR]. + --config=PATH Path to the config file [default: uitests/uitests/config.json] + --show Whether to display the report or not. + -T, --timeout=TIMEOUT Timeout for closing instance of VSC when Launched to validate instance of VSC [default: 30] + +""" +import glob +import json +import logging +import os +import os.path +import pathlib +import sys +import time + +from behave import __main__ +from docopt import docopt +from junitparser import JUnitXml + +from . import tools, vscode + + +def download(destination, channel, **kwargs): + """Download VS Code (stable/insiders) and chrome driver. + + The channel defines the channel for VSC (stable or insiders). + """ + destination = os.path.abspath(destination) + destination = os.path.join(destination, channel) + vscode.download.download_vscode(destination, channel) + vscode.download.download_chrome_driver(destination, channel) + + +def install(destination, channel, vsix, **kwargs): + """Installs the Python Extension into VS Code in preparation for the smoke tests.""" + destination = os.path.abspath(destination) + vsix = os.path.abspath(vsix) + options = vscode.application.get_options(destination, vsix=vsix, channel=channel) + vscode.application.install_extension(options) + + # Launch extension and exit (we want to ensure folders are created & extensions work). + vscode.application.setup_environment(options) + driver = vscode.application.launch_extension(options) + context = vscode.startup.Context(options, driver) + vscode.application.exit(context) + + +def launch(destination, channel, vsix, timeout=30, **kwargs): + """Launches VS Code (the same instance used for smoke tests).""" + destination = os.path.abspath(destination) + vsix = os.path.abspath(vsix) + options = vscode.application.get_options(destination, vsix=vsix, channel=channel) + logging.info(f"Launched VSC will exit in {timeout}s") + vscode.startup.start(options) + time.sleep(int(timeout)) + + +def report(destination, show=False, **kwargs): + """Generates an HTML report and displays it.""" + _update_junit_report(destination, **kwargs) + destination = os.path.abspath(destination) + report_dir = os.path.join(destination, "reports") + tools.run_command( + [ + "node", + os.path.join("uitests", "uitests", "js", "report.js"), + report_dir, + str(show), + ] + ) + + +def _update_junit_report(destination, **kwargs): + """Updates the junit reports to contain the names of the current Azdo Job.""" + destination = os.path.abspath(destination) + report_dir = os.path.join(destination, "reports") + report_name = os.getenv("AgentJobName", "") + for name in glob.glob(os.path.join(report_dir, "*.xml")): + xml = JUnitXml.fromfile(name) + xml.name = f"({report_name}): {xml.name}" + for suite in xml: + suite.classname = f"({report_name}): {suite.classname}" + xml.write() + + +def test(out, destination, channel, vsix, behave_options, embed_screenshots, **kwargs): + """Start the smoke tests.""" + destination = os.path.abspath(destination) + vsix = os.path.abspath(vsix) + embed_screenshots = False if embed_screenshots is None else embed_screenshots + report_args = [ + "-f", + "uitests.report:PrettyCucumberJSONFormatter", + "-o", + os.path.join(destination, "reports", "report.json"), + "--junit", + "--junit-directory", + os.path.join(destination, "reports"), + "--define", + f"embed_screenshots={embed_screenshots}", + ] + stdout_args = [ + "--format", + "plain", + "-no-timings", + "--no-capture", + "--define", + f"embed_screenshots=False", + ] + args = report_args if out == "file" else stdout_args + args = ( + args + + [ + "--define", + f"destination={destination}", + "--define", + f"embed_screenshots={embed_screenshots}", + "--define", + f"channel={channel}", + "--define", + f"vsix={vsix}", + "--define", + f"output={out}", + os.path.abspath("uitests/uitests"), + ] + + behave_options + ) + + # Change directory for behave to work correctly. + os.chdir(pathlib.Path(__file__).parent) + return __main__.main(args) + + +def main(): + arguments = docopt(__doc__, version="1.0") + with open(os.path.abspath(arguments.get("--config")), "r") as file: + config_options = json.load(file) + behave_options = arguments.get("") + arguments = dict( + (str(key), arguments.get(key) or config_options.get(key)) + for key in set(config_options) | set(arguments) + ) + options = { + key[2:]: value for (key, value) in arguments.items() if key.startswith("--") + } + log = arguments.get("--log") + log_level = getattr(logging, log.upper()) + if log_level == logging.INFO: + logging.basicConfig(level=log_level, format="%(message)s", stream=sys.stdout) + else: + logging.basicConfig(level=log_level) + options.setdefault("behave_options", behave_options) + handler = lambda **kwargs: 0 # noqa + if arguments.get("download"): + handler = download + if arguments.get("install"): + handler = install + if arguments.get("launch"): + handler = launch + if arguments.get("test"): + handler = test + if arguments.get("report"): + handler = report + return handler(**options) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/uitests/uitests/bootstrap/README.md b/uitests/uitests/bootstrap/README.md new file mode 100644 index 000000000000..49c6cb511e9b --- /dev/null +++ b/uitests/uitests/bootstrap/README.md @@ -0,0 +1,9 @@ +# Purpose of the bootstrap extension +* Haven't found a way to pass command line arguments to VSC when using selenium. +* We need to open a workspace folder when launching VSC. +* As we cannot (don't yet know) do this via CLI, the approach is simple: + * Create a simple extension that will activate when VSC loads + * Look for a file that contains the path to the workspace folder that needs to be opened. + * Next use VSC API to re-load VSC by opening that folder. + +* Hacky, but it works, at least untill we know how to pass CLI args when using `selenium` diff --git a/uitests/uitests/bootstrap/__init__.py b/uitests/uitests/bootstrap/__init__.py new file mode 100644 index 000000000000..378218f602fc --- /dev/null +++ b/uitests/uitests/bootstrap/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from . import main # noqa diff --git a/uitests/uitests/bootstrap/extension/.vscodeignore b/uitests/uitests/bootstrap/extension/.vscodeignore new file mode 100644 index 000000000000..ff73d4744519 --- /dev/null +++ b/uitests/uitests/bootstrap/extension/.vscodeignore @@ -0,0 +1,10 @@ +.vscode/** +.vscode-test/** +out/test/** +out/**/*.map +src/** +.gitignore +tsconfig.json +vsc-extension-quickstart.md +tslint.json +*.vsix diff --git a/uitests/uitests/bootstrap/extension/extension.js b/uitests/uitests/bootstrap/extension/extension.js new file mode 100644 index 000000000000..34afae05512e --- /dev/null +++ b/uitests/uitests/bootstrap/extension/extension.js @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +Object.defineProperty(exports, "__esModule", { value: true }); +const vscode = require("vscode"); +const fs = require('fs'); +const path = require('path'); + +async function sleep(timeout) { + return new Promise(resolve => setTimeout(resolve, timeout)); +} +function activate(context) { + const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10000000); + statusBarItem.command = 'workbench.action.quickOpen'; + statusBarItem.text = 'PySmoke'; + statusBarItem.tooltip = 'PySmoke'; + statusBarItem.show(); + + context.subscriptions.push(statusBarItem); + + const ext = vscode.extensions.getExtension('ms-python.python'); + if (!ext.isActive) { + ext.activate(); + } + + vscode.commands.registerCommand('smoketest.activatePython', async () => { + const ext = vscode.extensions.getExtension('ms-python.python'); + if (!ext.isActive) { + await ext.activate(); + } + vscode.window.showInformationMessage('Python Extension Activated'); + }); + vscode.commands.registerCommand('smoketest.runInTerminal', async () => { + const command = fs.readFileSync(path.join(__dirname, '..', 'commands.txt')).toString().trim(); + for (let counter = 0; counter < 5; counter++) { + if (!vscode.window.activeTerminal) { + await sleep(5000); + } + } + if (!vscode.window.activeTerminal) { + vscode.window.createTerminal('Manual'); + await sleep(5000); + } + if (!vscode.window.activeTerminal) { + vscode.window.showErrorMessage('No Terminal in Bootstrap Extension'); + } + await vscode.window.activeTerminal.sendText(command, true); + }); + vscode.commands.registerCommand('smoketest.openFile', async () => { + const file = fs.readFileSync(path.join(__dirname, '..', 'commands.txt')).toString().trim(); + const doc = await vscode.workspace.openTextDocument(file); + await vscode.window.showTextDocument(doc) + }); +} + +exports.activate = activate; +function deactivate() { + // Do nothing. +} +exports.deactivate = deactivate; diff --git a/uitests/uitests/bootstrap/extension/package.json b/uitests/uitests/bootstrap/extension/package.json new file mode 100644 index 000000000000..0006a17835d4 --- /dev/null +++ b/uitests/uitests/bootstrap/extension/package.json @@ -0,0 +1,47 @@ +{ + "name": "smoketest", + "publisher": "ms-python", + "displayName": "smokeTestPython", + "description": "Bootstrap for Python Smoke Tests", + "version": "0.0.1", + "license": "MIT", + "homepage": "https://github.com/Microsoft/vscode-python", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/vscode-python" + }, + "bugs": { + "url": "https://github.com/Microsoft/vscode-python/issues" + }, + "qna": "https://stackoverflow.com/questions/tagged/visual-studio-code+python", + "engines": { + "vscode": "^1.32.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "*" + ], + "main": "./extension", + "contributes": { + "commands": [ + { + "command": "smoketest.openworkspace", + "title": "Open Smoke Test Workspace" + }, + { + "command": "smoketest.activatePython", + "title": "Activate Python Extension" + }, + { + "command": "smoketest.runInTerminal", + "title": "Smoke: Run Command In Terminal" + }, + { + "command": "smoketest.openFile", + "title": "Smoke: Open File" + } + ] + } +} diff --git a/uitests/uitests/bootstrap/main.py b/uitests/uitests/bootstrap/main.py new file mode 100644 index 000000000000..4f7d33dd232b --- /dev/null +++ b/uitests/uitests/bootstrap/main.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import os.path + +import uitests.tools + +_current_dir = os.path.dirname(os.path.realpath(__file__)) +EXTENSION_DIR = os.path.abspath(os.path.join(_current_dir, "extension")) # noqa +EXTENSION_FILE = os.path.join(EXTENSION_DIR, "smoketest-0.0.1.vsix") + + +def build_extension(): + """Build the bootstrap extension.""" + command = ["vsce", "package"] + uitests.tools.run_command( + command, cwd=EXTENSION_DIR, progress_message="Build Bootstrap Extension" # noqa + ) + + +def get_extension_path(): + """Get the path to the VSIX of the bootstrap extension.""" + if not os.path.isfile(EXTENSION_FILE): + build_extension() + return EXTENSION_FILE diff --git a/uitests/uitests/config.json b/uitests/uitests/config.json new file mode 100644 index 000000000000..7b959282a406 --- /dev/null +++ b/uitests/uitests/config.json @@ -0,0 +1,7 @@ +{ + "--python-path": "/Library/Frameworks/Python.framework/Versions/3.7/bin/python3", + "--python-type": "", + "--python-version": "3.7", + "--python3-path": "/Library/Frameworks/Python.framework/Versions/3.7/bin/python3", + "--pipenv-path": "" +} diff --git a/uitests/uitests/environment.py b/uitests/uitests/environment.py new file mode 100644 index 000000000000..ef64fdcbbf33 --- /dev/null +++ b/uitests/uitests/environment.py @@ -0,0 +1,104 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os.path +import sys + +import behave +import parse + +import uitests.tools +import uitests.vscode +import uitests.vscode.settings +import uitests.vscode.startup + + +@parse.with_pattern(r"\d+") +def parse_number(text): + return int(text) + + +behave.register_type(Number=parse_number) +feature_workspace_folder = None + + +def before_all(context): + options = uitests.vscode.application.get_options(**context.config.userdata) + app_context = uitests.vscode.startup.start(options) + uitests.vscode.startup.clear_everything(app_context) + context.driver = app_context.driver + context.options = app_context.options + context.workspace_repo = None + + +def after_all(context): + context.driver = uitests.vscode.startup.CONTEXT["driver"] + uitests.vscode.application.exit(context) + + +def write_log_header(message, options): + pass + # with open(os.path.join(options.logfiles_dir, "vsc.log"), "a+") as fp: + # fp.write(os.linesep) + # fp.write(message) + # fp.write(os.linesep) + + +@uitests.tools.retry((PermissionError, FileNotFoundError), tries=2) +def before_feature(context, feature): + # Restore `drive`, as behave will overwrite with original value. + # Note, its possible we have a new driver instance due to reloading of VSC. + context.driver = uitests.vscode.startup.CONTEXT["driver"] + uitests.vscode.startup.clear_everything(context) + write_log_header(feature.name, context.options) + + repo = [ + tag for tag in feature.tags if tag.lower().startswith("https://github.com/") + ] + uitests.vscode.startup.setup_workspace(context, repo[0] if repo else None) + global feature_workspace_folder + feature_workspace_folder = context.options.workspace_folder + + # On windows, always reload, as we'll have a new worksapce folder for every feature. + # If we have a repo, then we might have a new workspace folder, so just reload. + # Can optimize later (for non-windows). + if sys.platform.startswith("darwin") or repo: + uitests.vscode.startup.reload(context) + + +@uitests.tools.retry((PermissionError, FileNotFoundError), tries=2) +def before_scenario(context, scenario): + # Restore `drive`, as behave will overwrite with original value. + # Note, its possible we have a new driver instance due to reloading of VSC. + context.driver = uitests.vscode.startup.CONTEXT["driver"] + context.options = uitests.vscode.application.get_options(**context.config.userdata) + context.options.workspace_folder = feature_workspace_folder + write_log_header(scenario.name, context.options) + + # Restore python.pythonPath in user & workspace settings. + settings_json = os.path.join(context.options.user_dir, "User", "settings.json") + + uitests.vscode.settings.update_settings( + settings_json, {"python.pythonPath": context.options.python_path} + ) + + # We want this open so it can get captured in screenshots. + uitests.vscode.quick_open.select_command(context, "View: Show Explorer") + uitests.vscode.startup.clear_everything(context) + if "preserve.workspace" not in scenario.tags: + uitests.vscode.startup.reset_workspace(context) + + # On windows, always reload, as we create a new workspace folder. + if sys.platform.startswith("win"): + uitests.vscode.startup.reload(context) + + +def after_scenario(context, feature): + context.driver = uitests.vscode.startup.CONTEXT["driver"] + uitests.vscode.notifications.clear(context) + + +def after_step(context, step): + context.driver = uitests.vscode.startup.CONTEXT["driver"] + if step.exception is not None: + uitests.vscode.application.capture_screen(context) diff --git a/uitests/uitests/features/README.md b/uitests/uitests/features/README.md new file mode 100644 index 000000000000..7403d13bbb5d --- /dev/null +++ b/uitests/uitests/features/README.md @@ -0,0 +1,34 @@ +# Tags +* @wip + * Used only for debugging purposes. + * When debugging in VSC, only features/scenarios with @wip tag will be executed. +* @skip + * Used to skip a feature/scenario. +* @https://github.com/xxx/yyy.git + * Can only be used at a feature level. + * The conents of the above repo will be used as the contents of the workspace folder. + * Note: assume the tag is `@https://github.com/DonJayamanne/pyvscSmokeTesting.git` + * The above repo is cloned directly into the workspace. + * If however the tag is `@https://github.com/DonJayamanne/pyvscSmokeTesting/tests` + * Now, the contents of the workspace is the `tests` directory in the above repo. + * This allows us to have a single repo with files/tests for more than just one feature/scenario. + * Else we'd need to have multiple repos for each feature/scenario. +* @preserve.workspace + * Ensures the previous workspace state is used for testing. + * I.e. state setup in previous state is not reset (makes it easier to re-use state from preivous scenarios). +* @mac, @win, @linux + * Used to ensure a particular feature/scenario runs only on mac, win or linux respectively. +* @python2, @python3, @python3.5, , @python3.6, , @python3.7 + * Used to ensure a particular feature/scenario runs only on specific version of Python, respectively. +* @smoke + * All smoke test related functionality. +* @test + * All testing related functionality. +* @debug + * All debugger related functionality. +* @terminal + * All terminal related functionality. +* @terminal.venv + * Related to virtual environments (`python -m venv`) +* @terminal.pipenv + * Related to pipenv environments (`pipenv shell`) diff --git a/uitests/uitests/features/interpreter/conda.feature b/uitests/uitests/features/interpreter/conda.feature new file mode 100644 index 000000000000..a9a14ad6663e --- /dev/null +++ b/uitests/uitests/features/interpreter/conda.feature @@ -0,0 +1,88 @@ +@terminal @terminal.conda +@wip +@https://github.com/DonJayamanne/vscode-python-uitests/terminal/execution +Feature: Terminal (conda) + Scenario: Interpreter display name contains the name of the environment and conda + Given the user setting "python.pythonPath" does not exist + And a conda environment is created with the name "helloworld" + Then take a screenshot + When I wait for 20 seconds + Then take a screenshot + # Wait for some time for the new conda environment to get discovered. + When I reload VSC + Then take a screenshot + When I send the command "conda env list" to the terminal + When I wait for 5 seconds + Then take a screenshot + When I wait for 20 seconds + Then take a screenshot + When I select the command "Python: Select Interpreter" + When I wait for 3 seconds + Then take a screenshot + When I reload VSC + And I wait for 30 seconds + And I select the Python Interpreter containing the name "helloworld" + Then take a screenshot + Then the python interpreter displayed in the the status bar contains the value "conda" in the display name + And the python interpreter displayed in the the status bar contains the value "helloworld" in the display name + And the workspace setting "python.pythonPath" exists + +# @preserve.workspace +# Scenario: Pipenv is auto selected +# Given the workspace setting "python.pythonPath" does not exist +# And the user setting "python.pythonPath" does not exist +# When I reload VSC +# Then the python interpreter displayed in the the status bar contains the value "pipenv" in the display name +# And the python interpreter displayed in the the status bar contains the value "workspace folder" in the display name +# And the workspace setting "python.pythonPath" exists + +# @preserve.workspace +# Scenario: Pipenv is not auto selected (if we already have a local interpreter selected) +# Given a generic Python Interpreter is selected +# When I reload VSC +# Then the python interpreter displayed in the the status bar does not contain the value "pipenv" in the display name +# And the python interpreter displayed in the the status bar does not contain the value "workspace folder" in the display name +# And the workspace setting "python.pythonPath" exists + +# @preserve.workspace +# Scenario: Pipenv is not auto selected (if we have a global interpreter selected) +# Given the workspace setting "python.pythonPath" does not exist +# And the user setting "python.pythonPath" exists +# When I reload VSC +# Then open the file "settings.json" +# Then the python interpreter displayed in the the status bar does not contain the value "pipenv" in the display name +# And the python interpreter displayed in the the status bar does not contain the value "workspace folder" in the display name + +# @preserve.workspace +# Scenario: Environment is not activated in the Terminal +# Given the workspace setting "python.pythonPath" does not exist +# And the user setting "python.pythonPath" does not exist +# When I reload VSC +# Then the python interpreter displayed in the the status bar contains the value "pipenv" in the display name +# And the python interpreter displayed in the the status bar contains the value "workspace folder" in the display name +# Given the file "write_pyPath_in_log.py" is open +# And a file named "log.log" does not exist +# And the workspace setting "python.terminal.activateEnvironment" is disabled +# And a terminal is opened +# When I send the command "python run_in_terminal.py" to the terminal +# Then a file named "log.log" is created +# And open the file "log.log" +# And the file "log.log" does not contain the value "workspace_folder" +# And take a screenshot + +# @preserve.workspace +# Scenario: Environment is activated in the Terminal +# Given the workspace setting "python.pythonPath" does not exist +# And the user setting "python.pythonPath" does not exist +# When I reload VSC +# Then the python interpreter displayed in the the status bar contains the value "pipenv" in the display name +# And the python interpreter displayed in the the status bar contains the value "workspace folder" in the display name +# Given the file "run_in_terminal.py" is open +# And a file named "log.log" does not exist +# And the workspace setting "python.terminal.activateEnvironment" is enabled +# And a terminal is opened +# When I send the command "python run_in_terminal.py" to the terminal +# Then a file named "log.log" is created +# And open the file "log.log" +# And the file "log.log" contains the value "workspace_folder" +# And take a screenshot diff --git a/uitests/uitests/features/interpreter/pipenv.feature b/uitests/uitests/features/interpreter/pipenv.feature new file mode 100644 index 000000000000..62187184f073 --- /dev/null +++ b/uitests/uitests/features/interpreter/pipenv.feature @@ -0,0 +1,71 @@ +@terminal @terminal.pipenv +@https://github.com/DonJayamanne/vscode-python-uitests/terminal/execution +Feature: Terminal (pipenv) + Scenario: Interpreter display name contains the name of the current workspace folder and pipenv + Given the user setting "python.pythonPath" does not exist + And a pipenv environment is created + When I reload VSC + And I select the Python Interpreter containing the name "workspace folder pipenv" + Then the python interpreter displayed in the the status bar contains the value "pipenv" in the display name + And the python interpreter displayed in the the status bar contains the value "workspace folder" in the display name + And the workspace setting "python.pythonPath" exists + + @preserve.workspace + Scenario: Pipenv is auto selected + Given the workspace setting "python.pythonPath" does not exist + And the user setting "python.pythonPath" does not exist + When I reload VSC + Then the python interpreter displayed in the the status bar contains the value "pipenv" in the display name + And the python interpreter displayed in the the status bar contains the value "workspace folder" in the display name + And the workspace setting "python.pythonPath" exists + + @preserve.workspace + Scenario: Pipenv is not auto selected (if we already have a local interpreter selected) + Given a generic Python Interpreter is selected + When I reload VSC + Then the python interpreter displayed in the the status bar does not contain the value "pipenv" in the display name + And the python interpreter displayed in the the status bar does not contain the value "workspace folder" in the display name + And the workspace setting "python.pythonPath" exists + + @preserve.workspace + Scenario: Pipenv is not auto selected (if we have a global interpreter selected) + Given the workspace setting "python.pythonPath" does not exist + And the user setting "python.pythonPath" exists + When I reload VSC + Then open the file "settings.json" + Then the python interpreter displayed in the the status bar does not contain the value "pipenv" in the display name + And the python interpreter displayed in the the status bar does not contain the value "workspace folder" in the display name + + @preserve.workspace + Scenario: Environment is not activated in the Terminal + Given the workspace setting "python.pythonPath" does not exist + And the user setting "python.pythonPath" does not exist + When I reload VSC + Then the python interpreter displayed in the the status bar contains the value "pipenv" in the display name + And the python interpreter displayed in the the status bar contains the value "workspace folder" in the display name + Given the file "write_pyPath_in_log.py" is open + And a file named "log.log" does not exist + And the workspace setting "python.terminal.activateEnvironment" is disabled + And a terminal is opened + When I send the command "python run_in_terminal.py" to the terminal + Then a file named "log.log" is created + And open the file "log.log" + And the file "log.log" does not contain the value "workspace_folder" + And take a screenshot + + @preserve.workspace + Scenario: Environment is activated in the Terminal + Given the workspace setting "python.pythonPath" does not exist + And the user setting "python.pythonPath" does not exist + When I reload VSC + Then the python interpreter displayed in the the status bar contains the value "pipenv" in the display name + And the python interpreter displayed in the the status bar contains the value "workspace folder" in the display name + Given the file "run_in_terminal.py" is open + And a file named "log.log" does not exist + And the workspace setting "python.terminal.activateEnvironment" is enabled + And a terminal is opened + When I send the command "python run_in_terminal.py" to the terminal + Then a file named "log.log" is created + And open the file "log.log" + And the file "log.log" contains the value "workspace_folder" + And take a screenshot diff --git a/uitests/uitests/features/interpreter/statusbar.feature b/uitests/uitests/features/interpreter/statusbar.feature new file mode 100644 index 000000000000..4c1a098424d0 --- /dev/null +++ b/uitests/uitests/features/interpreter/statusbar.feature @@ -0,0 +1,18 @@ +@terminal +Feature: Statusbar + @smoke + Scenario: Interpreter is displayed in the statusbar + Then the python interpreter displayed in the the status bar contains the value "Python" in the display name + And take a screenshot + + @python2 + Scenario: Can select a Python 2.7 interpreter + When I select the Python Interpreter containing the name "2.7" + Then the python interpreter displayed in the the status bar contains the value "2.7" in the display name + And take a screenshot + + @python3 + Scenario: Can select a Python 3 interpreter + When I select the Python Interpreter containing the name "3." + Then the python interpreter displayed in the the status bar contains the value "3." in the display name + And take a screenshot diff --git a/uitests/uitests/features/interpreter/terminal.feature b/uitests/uitests/features/interpreter/terminal.feature new file mode 100644 index 000000000000..e6d4baacfeb7 --- /dev/null +++ b/uitests/uitests/features/interpreter/terminal.feature @@ -0,0 +1,31 @@ +@terminal +@https://github.com/DonJayamanne/vscode-python-uitests/terminal/execution +Feature: Terminal + @mac + Scenario: Select default Mac 2.7 Interpreter + When I reload VSC + And I select the default mac Interpreter + Then a message with the text "You have selected the macOS system install of Python, which is not recommended for use with the Python extension. Some functionality will be limited, please select a different interpreter." is displayed + And take a screenshot + + @smoke + Scenario: Execute File in Terminal + Given the file "run_in_terminal.py" is open + And a file named "log.log" does not exist + When I select the command "Python: Run Python File in Terminal" + Then a file named "log.log" is created + And take a screenshot + + Scenario: Execute Selection in Terminal + Given the file "run_in_terminal.py" is open + And a file named "log.log" does not exist + When I go to line 1 + And I select the command "Python: Run Selection/Line in Python Terminal" + Then a file named "log.log" is created + And the file "log.log" contains the value "Hello World" + And take a screenshot + When I go to line 2 + And I select the command "Python: Run Selection/Line in Python Terminal" + Then a file named "log.log" is created + And the file "log.log" contains the value "Bye" + And take a screenshot diff --git a/uitests/uitests/features/interpreter/venv.feature b/uitests/uitests/features/interpreter/venv.feature new file mode 100644 index 000000000000..88a971c998ff --- /dev/null +++ b/uitests/uitests/features/interpreter/venv.feature @@ -0,0 +1,64 @@ +@terminal @terminal.venv @python3 +@https://github.com/DonJayamanne/vscode-python-uitests/terminal/execution +Feature: Terminal (venv) + Scenario: Interpreter display name contains the name of the venv folder + When In Windows, I update the workspace setting "terminal.integrated.shell.windows" with the value "C:\\Windows\\System32\\cmd.exe" + Given a venv with the name "venv 1" is created + When In Mac, I update the workspace setting "python.pythonPath" with the value "venv 1/bin/python" + When In Linux, I update the workspace setting "python.pythonPath" with the value "venv 1/bin/python" + When In Windows, I update the workspace setting "python.pythonPath" with the value "venv 1/Scripts/python.exe" + Then the python interpreter displayed in the the status bar contains the value "venv 1" in the display name + + @preserve.workspace + Scenario: Venv is auto selected + Given the workspace setting "python.pythonPath" does not exist + And the user setting "python.pythonPath" does not exist + Then the python interpreter displayed in the the status bar does not contain the value "venv 1" in the display name + When I reload VSC + Then the python interpreter displayed in the the status bar contains the value "venv 1" in the display name + + @preserve.workspace + Scenario: Venv is not auto selected (if we already have a local interpreter selected) + Given a generic Python Interpreter is selected + And the user setting "python.pythonPath" does not exist + Then the python interpreter displayed in the the status bar does not contain the value "venv 1" in the display name + When I reload VSC + Then the python interpreter displayed in the the status bar does not contain the value "venv 1" in the display name + + @preserve.workspace + Scenario: Venv is not auto selected (if we have a global interpreter selected) + Given the workspace setting "python.pythonPath" does not exist + And the user setting "python.pythonPath" exists + Then the python interpreter displayed in the the status bar does not contain the value "venv 1" in the display name + When I reload VSC + Then the python interpreter displayed in the the status bar does not contain the value "venv 1" in the display name + + @preserve.workspace + Scenario: Environment is not activated in the Terminal + When In Mac, I update the workspace setting "python.pythonPath" with the value "venv 1/bin/python" + When In Linux, I update the workspace setting "python.pythonPath" with the value "venv 1/bin/python" + When In Windows, I update the workspace setting "python.pythonPath" with the value "venv 1/Scripts/python.exe" + Given the file "write_pyPath_in_log.py" is open + And a file named "log.log" does not exist + And the workspace setting "python.terminal.activateEnvironment" is disabled + And a terminal is opened + When I send the command "python write_pyPath_in_log.py" to the terminal + Then a file named "log.log" is created + And open the file "log.log" + And the file "log.log" does not contain the value "env 1" + And take a screenshot + + @preserve.workspace + Scenario: Environment is activated in the Terminal + When In Mac, I update the workspace setting "python.pythonPath" with the value "venv 1/bin/python" + When In Linux, I update the workspace setting "python.pythonPath" with the value "venv 1/bin/python" + When In Windows, I update the workspace setting "python.pythonPath" with the value "venv 1/Scripts/python.exe" + Given the file "write_pyPath_in_log.py" is open + And a file named "log.log" does not exist + And the workspace setting "python.terminal.activateEnvironment" is enabled + And a terminal is opened + When I send the command "python write_pyPath_in_log.py" to the terminal + Then a file named "log.log" is created + And open the file "log.log" + And the file "log.log" contains the value "env 1" + And take a screenshot diff --git a/uitests/uitests/features/language_server/basic.feature b/uitests/uitests/features/language_server/basic.feature new file mode 100644 index 000000000000..03e3b893923c --- /dev/null +++ b/uitests/uitests/features/language_server/basic.feature @@ -0,0 +1,21 @@ +@ls @smoke +@https://github.com/DonJayamanne/pvscSmokeLS.git +Feature: Language Server + Scenario: Language Server loads and starts analyzing files + Given the user setting "python.jediEnabled" is disabled + When I reload VSC + And I select the command "Python: Show Output" + Then the output panel contains the text "Microsoft Python Language Server" + And the output panel contains the text "Initializing for" + + Scenario: Navigate to definition of a variable + Given the user setting "python.jediEnabled" is disabled + When I reload VSC + And I select the command "Python: Show Output" + Then the output panel contains the text "Microsoft Python Language Server" + And the output panel contains the text "Initializing for" + Given the file "my_sample.py" is open + When I go to line 3 + And I select the command "Go to Definition" + Then the cursor is on line 1 + And take a screenshot diff --git a/uitests/uitests/features/testing/discover.feature b/uitests/uitests/features/testing/discover.feature new file mode 100644 index 000000000000..68f009527729 --- /dev/null +++ b/uitests/uitests/features/testing/discover.feature @@ -0,0 +1,69 @@ +@test +Feature: Discovery Prompts + Scenario: Discover will display prompt to configure when not configured + Given the file ".vscode/settings.json" does not exist + When I reload VSC + When I select the command "Python: Discover Tests" + Then a message containing the text "No test framework configured" is displayed + + Scenario: Discover will prompt to install pytest + Given the package "pytest" is not installed + And the workspace setting "python.testing.pyTestEnabled" is enabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + And I select the command "Python: Discover Tests" + Then a message containing the text "pytest is not installed" is displayed + + Scenario: Discover will prompt to install nose + Given the package "nose" is not installed + And the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is enabled + When I reload VSC + And I select the command "Python: Discover Tests" + Then a message containing the text "nosetest is not installed" is displayed + + Scenario: Discover will display prompt indicating there are no tests (unittest) + Given a file named ".vscode/settings.json" is created with the following contents + """ + { + "python.testing.unittestArgs": ["-v","-s",".","-p","*test*.py"], + "python.testing.pyTestEnabled": false, + "python.testing.nosetestsEnabled": false, + "python.testing.unittestEnabled": true + } + """ + When I reload VSC + And I select the command "Python: Discover Tests" + Then a message containing the text "No tests discovered" is displayed + + Scenario: Discover will display prompt indicating there are no tests (pytest) + Given the package "pytest" is installed + And a file named ".vscode/settings.json" is created with the following contents + """ + { + "python.testing.pyTestEnabled": true, + "python.testing.nosetestsEnabled": false, + "python.testing.unittestEnabled": false, + "python.testing.pyTestArgs": ["."], + } + """ + When I reload VSC + And I select the command "Python: Discover Tests" + Then a message containing the text "No tests discovered" is displayed + + Scenario: Discover will display prompt indicating there are no tests (nose) + Given the package "nose" is installed + And a file named ".vscode/settings.json" is created with the following contents + """ + { + "python.testing.pyTestEnabled": false, + "python.testing.nosetestsEnabled": true, + "python.testing.unittestEnabled": false, + "python.testing.nosetestArgs": ["."] + } + """ + When I reload VSC + And I select the command "Python: Discover Tests" + Then a message containing the text "No tests discovered" is displayed diff --git a/uitests/uitests/features/testing/explorer/code.navigation.feature b/uitests/uitests/features/testing/explorer/code.navigation.feature new file mode 100644 index 000000000000..3a792820857a --- /dev/null +++ b/uitests/uitests/features/testing/explorer/code.navigation.feature @@ -0,0 +1,136 @@ +@test +@https://github.com/DonJayamanne/pyvscSmokeTesting.git +Feature: Test Explorer Discovering icons and stop discovery + Scenario: When navigating to a test file, suite & test, then open the file and set the cursor at the right line (unitest) + Given the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is enabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + And the command "View: Close All Editors" is selected + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + When I navigate to the code associated with test node "test_one.py" + Then the file "test_one.py" is opened + When I navigate to the code associated with test node "test_one_first_suite" + Then the file "test_one.py" is opened + And the cursor is on line 20 + When I navigate to the code associated with test node "test_three_first_suite" + Then the file "test_one.py" is opened + And the cursor is on line 30 + When I navigate to the code associated with test node "test_two_first_suite" + Then the file "test_one.py" is opened + And the cursor is on line 25 + + Scenario: When navigating to a test file, suite & test, then open the file and set the cursor at the right line (pytest) + Given the package "pytest" is installed + And the workspace setting "python.testing.pyTestEnabled" is enabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + When I navigate to the code associated with test node "test_one.py" + Then the file "test_one.py" is opened + When I navigate to the code associated with test node "test_one_first_suite" + Then the file "test_one.py" is opened + And the cursor is on line 20 + When I navigate to the code associated with test node "test_three_first_suite" + Then the file "test_one.py" is opened + And the cursor is on line 30 + When I navigate to the code associated with test node "test_two_first_suite" + Then the file "test_one.py" is opened + And the cursor is on line 25 + + Scenario: When navigating to a test file, suite & test, then open the file and set the cursor at the right line (nose) + Given the package "nose" is installed + And the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is enabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + When I navigate to the code associated with test node "tests/test_one.py" + Then the file "test_one.py" is opened + When I navigate to the code associated with test node "test_one_first_suite" + Then the file "test_one.py" is opened + And the cursor is on line 20 + When I navigate to the code associated with test node "test_three_first_suite" + Then the file "test_one.py" is opened + And the cursor is on line 30 + When I navigate to the code associated with test node "test_two_first_suite" + Then the file "test_one.py" is opened + And the cursor is on line 25 + + + Scenario: When selecting a node, then open the file (unitest) + Given the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is enabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + And the command "View: Close All Editors" is selected + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + When I click node "TestFirstSuite" + Then the file "test_one.py" is opened + Given the command "View: Close All Editors" is selected + When I click node "test_one_first_suite" + Then the file "test_one.py" is opened + Given the command "View: Close All Editors" is selected + When I click node "test_three_first_suite" + Then the file "test_one.py" is opened + Given the command "View: Close All Editors" is selected + When I click node "test_two_third_suite" + Then the file "test_two.py" is opened + + + Scenario: When selecting a node, then open the file (pytest) + Given the package "pytest" is installed + And the workspace setting "python.testing.pyTestEnabled" is enabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + When I click node "TestFirstSuite" + Then the file "test_one.py" is opened + Given the command "View: Close All Editors" is selected + When I click node "test_one_first_suite" + Then the file "test_one.py" is opened + Given the command "View: Close All Editors" is selected + When I click node "test_three_first_suite" + Then the file "test_one.py" is opened + Given the command "View: Close All Editors" is selected + When I click node "test_two_third_suite" + Then the file "test_two.py" is opened + + Scenario: When selecting a node, then open the file (nose) + Given the package "nose" is installed + And the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is enabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + When I click node "TestFirstSuite" + Then the file "test_one.py" is opened + Given the command "View: Close All Editors" is selected + When I click node "test_one_first_suite" + Then the file "test_one.py" is opened + Given the command "View: Close All Editors" is selected + When I click node "test_three_first_suite" + Then the file "test_one.py" is opened + Given the command "View: Close All Editors" is selected + When I click node "test_two_third_suite" + Then the file "test_two.py" is opened diff --git a/uitests/uitests/features/testing/explorer/debug.feature b/uitests/uitests/features/testing/explorer/debug.feature new file mode 100644 index 000000000000..ca9e29f1af0d --- /dev/null +++ b/uitests/uitests/features/testing/explorer/debug.feature @@ -0,0 +1,278 @@ +@test +@https://github.com/DonJayamanne/pyvscSmokeTesting.git +Feature: Test Explorer Discovering icons and stop discovery + Scenario: When debugging tests, the nodes will have the progress icon and clicking stop will stop the debugger (unitest) + Given the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is enabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + And a file named "tests/test_running_delay" is created with the following contents + """ + 5 + """ + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 14 nodes in the tree + And 14 nodes have a status of "Unknown" + When I debug the test node "test_three_first_suite" + Then the debugger starts + When I select the command "Debug: Stop" + Then the debugger stops + + Scenario: When debugging tests, the nodes will have the progress icon and clicking stop will stop the debugger (pytest) + Given the package "pytest" is installed + And the workspace setting "python.testing.pyTestEnabled" is enabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + And a file named "tests/test_running_delay" is created with the following contents + """ + 5 + """ + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 15 nodes in the tree + And 15 nodes have a status of "Unknown" + When I debug the test node "test_three_first_suite" + Then the debugger starts + When I select the command "Debug: Stop" + Then the debugger stops + + Scenario: When debugging tests, the nodes will have the progress icon and clicking stop will stop the debugger (nose) + Given the package "nose" is installed + And the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is enabled + And a file named "tests/test_running_delay" is created with the following contents + """ + 5 + """ + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 14 nodes in the tree + And 14 nodes have a status of "Unknown" + When I debug the test node "test_three_first_suite" + Then the debugger starts + When I select the command "Debug: Stop" + Then the debugger stops + + Scenario: When debugging tests, only the specific function will be debugged (unitest) + Given the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is enabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + When I add a breakpoint to line 33 in "test_one.py" + And I add a breakpoint to line 23 in "test_one.py" + And I debug the test node "test_three_first_suite" + Then the debugger starts + And the debugger pauses + And the current stack frame is at line 33 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger stops + + + Scenario: When debugging tests, only the specific function will be debugged (pytest) + Given the package "pytest" is installed + And the workspace setting "python.testing.pyTestEnabled" is enabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + When I add a breakpoint to line 33 in "test_one.py" + And I add a breakpoint to line 23 in "test_one.py" + And I debug the test node "test_three_first_suite" + Then the debugger starts + And the debugger pauses + And the current stack frame is at line 33 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger stops + + Scenario: When debugging tests, only the specific function will be debugged (nose) + Given the package "nose" is installed + And the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is enabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + When I add a breakpoint to line 33 in "test_one.py" + And I add a breakpoint to line 23 in "test_one.py" + And I debug the test node "test_three_first_suite" + Then the debugger starts + And the debugger pauses + And the current stack frame is at line 33 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger stops + + + Scenario: When debugging tests, only the specific suite will be debugged (unitest) + Given the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is enabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + When I add a breakpoint to line 33 in "test_one.py" + And I add a breakpoint to line 28 in "test_one.py" + And I add a breakpoint to line 23 in "test_one.py" + And I debug the test node "TestFirstSuite" + Then the debugger starts + And the debugger pauses + And the current stack frame is at line 23 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger pauses + And the current stack frame is at line 33 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger pauses + And the current stack frame is at line 28 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger stops + + + Scenario: When debugging tests, only the specific suite will be debugged (pytest) + Given the package "pytest" is installed + And the workspace setting "python.testing.pyTestEnabled" is enabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + When I add a breakpoint to line 33 in "test_one.py" + And I add a breakpoint to line 28 in "test_one.py" + And I add a breakpoint to line 23 in "test_one.py" + And I debug the test node "TestFirstSuite" + Then the debugger starts + And the debugger pauses + And the current stack frame is at line 23 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger pauses + And the current stack frame is at line 33 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger pauses + And the current stack frame is at line 28 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger stops + + Scenario: When debugging tests, only the specific suite will be debugged (nose) + Given the package "nose" is installed + And the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is enabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + When I add a breakpoint to line 33 in "test_one.py" + And I add a breakpoint to line 28 in "test_one.py" + And I add a breakpoint to line 23 in "test_one.py" + And I debug the test node "TestFirstSuite" + Then the debugger starts + And the debugger pauses + And the current stack frame is at line 23 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger pauses + And the current stack frame is at line 33 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger pauses + And the current stack frame is at line 28 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger stops + + Scenario: When debugging tests, everything will be debugged (unitest) + Given the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is enabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + When I add a breakpoint to line 23 in "test_one.py" + And I add a breakpoint to line 38 in "test_one.py" + And I add a breakpoint to line 23 in "test_two.py" + And I select the command "Python: Debug All Tests" + Then the debugger starts + And the debugger pauses + And the current stack frame is at line 23 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger pauses + And the current stack frame is at line 38 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger pauses + And the current stack frame is at line 23 in "test_two.py" + When I select the command "Debug: Continue" + Then the debugger stops + + + Scenario: When debugging tests, everything will be debugged (pytest) + Given the package "pytest" is installed + And the workspace setting "python.testing.pyTestEnabled" is enabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + When I add a breakpoint to line 23 in "test_one.py" + And I add a breakpoint to line 38 in "test_one.py" + And I add a breakpoint to line 23 in "test_two.py" + And I select the command "Python: Debug All Tests" + Then the debugger starts + And the debugger pauses + And the current stack frame is at line 23 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger pauses + And the current stack frame is at line 38 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger pauses + And the current stack frame is at line 23 in "test_two.py" + When I select the command "Debug: Continue" + Then the debugger stops + + Scenario: When debugging tests, everything will be debugged (nose) + Given the package "nose" is installed + And the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is enabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + When I add a breakpoint to line 23 in "test_one.py" + And I add a breakpoint to line 38 in "test_one.py" + And I add a breakpoint to line 23 in "test_two.py" + And I select the command "Python: Debug All Tests" + Then the debugger starts + And the debugger pauses + And the current stack frame is at line 23 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger pauses + And the current stack frame is at line 38 in "test_one.py" + When I select the command "Debug: Continue" + Then the debugger pauses + And the current stack frame is at line 23 in "test_two.py" + When I select the command "Debug: Continue" + Then the debugger stops diff --git a/uitests/uitests/features/testing/explorer/discover.count.feature b/uitests/uitests/features/testing/explorer/discover.count.feature new file mode 100644 index 000000000000..23bf3ec470b7 --- /dev/null +++ b/uitests/uitests/features/testing/explorer/discover.count.feature @@ -0,0 +1,38 @@ +@test +@https://github.com/DonJayamanne/pyvscSmokeTesting.git +Feature: Test Explorer + + Scenario: Explorer will be displayed when tests are discovered (unitest) + Given the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is enabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 14 nodes in the tree + + Scenario: Explorer will be displayed when tests are discovered (pytest) + Given the package "pytest" is installed + And the workspace setting "python.testing.pyTestEnabled" is enabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 15 nodes in the tree + + Scenario: Explorer will be displayed when tests are discovered (nose) + Given the package "nose" is installed + And the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is enabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 14 nodes in the tree diff --git a/uitests/uitests/features/testing/explorer/discover.icons.feature b/uitests/uitests/features/testing/explorer/discover.icons.feature new file mode 100644 index 000000000000..2fd976d73bbf --- /dev/null +++ b/uitests/uitests/features/testing/explorer/discover.icons.feature @@ -0,0 +1,71 @@ +@test +@https://github.com/DonJayamanne/pyvscSmokeTesting.git +Feature: Test Explorer Discovering icons and stop discovery + + Scenario: When discovering tests, the nodes will have the progress icon and clicking stop will stop discovery (unitest) + Given the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is enabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 14 nodes in the tree + # Now, add a delay for the discovery of the tests + Given a file named "tests/test_discovery_delay" is created with the following contents + """ + 10 + """ + When I select the command "Python: Discover Tests" + And I wait for 1 second + Then all of the test tree nodes have a progress icon + And the stop icon is visible in the toolbar + When I stop discovering tests + Then the stop icon is not visible in the toolbar + + Scenario: When discovering tests, the nodes will have the progress icon and clicking stop will stop discovery (pytest) + Given the package "pytest" is installed + And the workspace setting "python.testing.pyTestEnabled" is enabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 15 nodes in the tree + # Now, add a delay for the discovery of the tests + Given a file named "tests/test_discovery_delay" is created with the following contents + """ + 10 + """ + When I select the command "Python: Discover Tests" + And I wait for 1 second + Then all of the test tree nodes have a progress icon + And the stop icon is visible in the toolbar + When I stop discovering tests + Then the stop icon is not visible in the toolbar + + Scenario: When discovering tests, the nodes will have the progress icon and clicking stop will stop discovery (nose) + Given the package "nose" is installed + And the workspace setting "python.testing.pyTestEnabled" is enabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 15 nodes in the tree + # Now, add a delay for the discovery of the tests + Given a file named "tests/test_discovery_delay" is created with the following contents + """ + 10 + """ + When I select the command "Python: Discover Tests" + And I wait for 1 second + Then all of the test tree nodes have a progress icon + And the stop icon is visible in the toolbar + When I stop discovering tests + Then the stop icon is not visible in the toolbar diff --git a/uitests/uitests/features/testing/explorer/run.failed.feature b/uitests/uitests/features/testing/explorer/run.failed.feature new file mode 100644 index 000000000000..95547585ba1f --- /dev/null +++ b/uitests/uitests/features/testing/explorer/run.failed.feature @@ -0,0 +1,308 @@ +@test +@https://github.com/DonJayamanne/pyvscSmokeTesting.git +Feature: Test Explorer - Re-run Failed Tests + + Scenario: We are able to re-run a failed tests (unitest) + Given the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is enabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + And a file named "tests/test_running_delay" is created with the following contents + """ + 0 + """ + And a file named "tests/data.json" is created with the following contents + """ + [1,-1,-1,4,5,6] + """ + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 14 nodes in the tree + And 14 nodes have a status of "Unknown" + When I select the command "Python: Run All Tests" + And I wait for tests to complete running + Then the node "test_one.py" has a status of "Fail" + And the node "TestFirstSuite" has a status of "Fail" + And the node "test_three_first_suite" has a status of "Fail" + And the node "test_two_first_suite" has a status of "Fail" + And the node "test_two.py" has a status of "Fail" + And the node "TestThirdSuite" has a status of "Fail" + And the node "test_three_third_suite" has a status of "Fail" + And the node "test_two_third_suite" has a status of "Fail" + And 6 nodes have a status of "Success" + And the run failed tests icon is visible in the toolbar + Given a file named "tests/test_running_delay" is created with the following contents + """ + 1 + """ + And a file named "tests/data.json" is created with the following contents + """ + [1,2,3,4,5,6] + """ + When I run failed tests + And I wait for tests to complete running + Then 14 nodes have a status of "Success" + + Scenario: We are able to re-run a failed tests (pytest) + Given the package "pytest" is installed + And the workspace setting "python.testing.pyTestEnabled" is enabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + And a file named "tests/test_running_delay" is created with the following contents + """ + 0 + """ + And a file named "tests/data.json" is created with the following contents + """ + [1,-1,-1,4,5,6] + """ + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 15 nodes in the tree + And 15 nodes have a status of "Unknown" + When I select the command "Python: Run All Tests" + And I wait for tests to complete running + Then the node "test_one.py" has a status of "Fail" + And the node "TestFirstSuite" has a status of "Fail" + And the node "test_three_first_suite" has a status of "Fail" + And the node "test_two_first_suite" has a status of "Fail" + And the node "test_two.py" has a status of "Fail" + And the node "TestThirdSuite" has a status of "Fail" + And the node "test_three_third_suite" has a status of "Fail" + And the node "test_two_third_suite" has a status of "Fail" + And 6 nodes have a status of "Success" + And the run failed tests icon is visible in the toolbar + Given a file named "tests/test_running_delay" is created with the following contents + """ + 1 + """ + And a file named "tests/data.json" is created with the following contents + """ + [1,2,3,4,5,6] + """ + When I run failed tests + And I wait for tests to complete running + Then 15 nodes have a status of "Success" + + Scenario: We are able to re-run a failed tests (nose) + Given the package "nose" is installed + And the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is enabled + And a file named "tests/test_running_delay" is created with the following contents + """ + 0 + """ + And a file named "tests/data.json" is created with the following contents + """ + [1,-1,-1,4,5,6] + """ + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 14 nodes in the tree + And 14 nodes have a status of "Unknown" + When I select the command "Python: Run All Tests" + And I wait for tests to complete running + Then the node "tests/test_one.py" has a status of "Fail" + And the node "TestFirstSuite" has a status of "Fail" + And the node "test_three_first_suite" has a status of "Fail" + And the node "test_two_first_suite" has a status of "Fail" + And the node "tests/test_two.py" has a status of "Fail" + And the node "TestThirdSuite" has a status of "Fail" + And the node "test_three_third_suite" has a status of "Fail" + And the node "test_two_third_suite" has a status of "Fail" + And 6 nodes have a status of "Success" + And the run failed tests icon is visible in the toolbar + Given a file named "tests/test_running_delay" is created with the following contents + """ + 1 + """ + And a file named "tests/data.json" is created with the following contents + """ + [1,2,3,4,5,6] + """ + When I run failed tests + And I wait for tests to complete running + Then 14 nodes have a status of "Success" + + Scenario: We are able to stop tests after re-running failed tests (unitest) + Given the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is enabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + And a file named "tests/test_running_delay" is created with the following contents + """ + 0 + """ + And a file named "tests/data.json" is created with the following contents + """ + [1,-1,-1,4,5,6] + """ + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 14 nodes in the tree + And 14 nodes have a status of "Unknown" + When I select the command "Python: Run All Tests" + And I wait for tests to complete running + Then the node "test_one.py" has a status of "Fail" + And the node "TestFirstSuite" has a status of "Fail" + And the node "test_three_first_suite" has a status of "Fail" + And the node "test_two_first_suite" has a status of "Fail" + And the node "test_two.py" has a status of "Fail" + And the node "TestThirdSuite" has a status of "Fail" + And the node "test_three_third_suite" has a status of "Fail" + And the node "test_two_third_suite" has a status of "Fail" + And 6 nodes have a status of "Success" + And the run failed tests icon is visible in the toolbar + Given a file named "tests/test_running_delay" is created with the following contents + """ + 100 + """ + And a file named "tests/data.json" is created with the following contents + """ + [1,2,3,4,5,6] + """ + When I run failed tests + And I wait for 1 seconds + Then the stop icon is visible in the toolbar + Then the node "TestFirstSuite" has a status of "Progress" + And the node "test_three_first_suite" has a status of "Progress" + And the node "test_two_first_suite" has a status of "Progress" + And the node "TestThirdSuite" has a status of "Progress" + And the node "test_three_third_suite" has a status of "Progress" + And the node "test_two_third_suite" has a status of "Progress" + And 6 nodes have a status of "Progress" + When I stop running tests + And I wait for tests to complete running + Then the stop icon is not visible in the toolbar + And the node "test_three_first_suite" has a status of "Unknown" + And the node "test_two_first_suite" has a status of "Unknown" + And the node "test_three_third_suite" has a status of "Unknown" + And the node "test_two_third_suite" has a status of "Unknown" + + + Scenario: We are able to stop tests after re-running failed tests (pytest) + Given the package "pytest" is installed + And the workspace setting "python.testing.pyTestEnabled" is enabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + And a file named "tests/test_running_delay" is created with the following contents + """ + 0 + """ + And a file named "tests/data.json" is created with the following contents + """ + [1,-1,-1,4,5,6] + """ + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 15 nodes in the tree + And 15 nodes have a status of "Unknown" + When I select the command "Python: Run All Tests" + And I wait for tests to complete running + Then the node "test_one.py" has a status of "Fail" + And the node "TestFirstSuite" has a status of "Fail" + And the node "test_three_first_suite" has a status of "Fail" + And the node "test_two_first_suite" has a status of "Fail" + And the node "test_two.py" has a status of "Fail" + And the node "TestThirdSuite" has a status of "Fail" + And the node "test_three_third_suite" has a status of "Fail" + And the node "test_two_third_suite" has a status of "Fail" + And 6 nodes have a status of "Success" + And the run failed tests icon is visible in the toolbar + Given a file named "tests/test_running_delay" is created with the following contents + """ + 100 + """ + And a file named "tests/data.json" is created with the following contents + """ + [1,2,3,4,5,6] + """ + When I run failed tests + And I wait for 1 seconds + Then the stop icon is visible in the toolbar + Then the node "TestFirstSuite" has a status of "Progress" + And the node "test_three_first_suite" has a status of "Progress" + And the node "test_two_first_suite" has a status of "Progress" + And the node "TestThirdSuite" has a status of "Progress" + And the node "test_three_third_suite" has a status of "Progress" + And the node "test_two_third_suite" has a status of "Progress" + And 6 nodes have a status of "Progress" + When I stop running tests + And I wait for tests to complete running + Then the stop icon is not visible in the toolbar + And the node "test_three_first_suite" has a status of "Unknown" + And the node "test_two_first_suite" has a status of "Unknown" + And the node "test_three_third_suite" has a status of "Unknown" + And the node "test_two_third_suite" has a status of "Unknown" + + Scenario: We are able to stop tests after re-running failed tests (nose) + Given the package "nose" is installed + And the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is enabled + And a file named "tests/test_running_delay" is created with the following contents + """ + 0 + """ + And a file named "tests/data.json" is created with the following contents + """ + [1,-1,-1,4,5,6] + """ + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 14 nodes in the tree + And 14 nodes have a status of "Unknown" + When I select the command "Python: Run All Tests" + And I wait for tests to complete running + Then the node "tests/test_one.py" has a status of "Fail" + And the node "TestFirstSuite" has a status of "Fail" + And the node "test_three_first_suite" has a status of "Fail" + And the node "test_two_first_suite" has a status of "Fail" + And the node "tests/test_two.py" has a status of "Fail" + And the node "TestThirdSuite" has a status of "Fail" + And the node "test_three_third_suite" has a status of "Fail" + And the node "test_two_third_suite" has a status of "Fail" + And 6 nodes have a status of "Success" + And the run failed tests icon is visible in the toolbar + Given a file named "tests/test_running_delay" is created with the following contents + """ + 100 + """ + And a file named "tests/data.json" is created with the following contents + """ + [1,2,3,4,5,6] + """ + When I run failed tests + And I wait for 1 seconds + Then the stop icon is visible in the toolbar + Then the node "TestFirstSuite" has a status of "Progress" + And the node "test_three_first_suite" has a status of "Progress" + And the node "test_two_first_suite" has a status of "Progress" + And the node "TestThirdSuite" has a status of "Progress" + And the node "test_three_third_suite" has a status of "Progress" + And the node "test_two_third_suite" has a status of "Progress" + And 6 nodes have a status of "Progress" + When I stop running tests + And I wait for tests to complete running + Then the stop icon is not visible in the toolbar + And the node "test_three_first_suite" has a status of "Unknown" + And the node "test_two_first_suite" has a status of "Unknown" + And the node "test_three_third_suite" has a status of "Unknown" + And the node "test_two_third_suite" has a status of "Unknown" diff --git a/uitests/uitests/features/testing/explorer/run.progress.feature b/uitests/uitests/features/testing/explorer/run.progress.feature new file mode 100644 index 000000000000..45a492358dcd --- /dev/null +++ b/uitests/uitests/features/testing/explorer/run.progress.feature @@ -0,0 +1,64 @@ +@test +@https://github.com/DonJayamanne/pyvscSmokeTesting.git +Feature: Test Explorer Running icons and stop running + Scenario: When running tests, the nodes will have the progress icon and clicking stop will stop running (unitest) + Given the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is enabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + And the file "tests/test_running_delay" has the following content + """ + 10 + """ + When I select the command "Python: Run All Tests" + And I wait for 1 second + Then all of the test tree nodes have a progress icon + And the stop icon is visible in the toolbar + When I stop running tests + Then the stop icon is not visible in the toolbar + + Scenario: When running tests, the nodes will have the progress icon and clicking stop will stop running (pytest) + Given the package "pytest" is installed + And the workspace setting "python.testing.pyTestEnabled" is enabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + And the file "tests/test_running_delay" has the following content + """ + 10 + """ + When I select the command "Python: Run All Tests" + And I wait for 1 second + Then all of the test tree nodes have a progress icon + And the stop icon is visible in the toolbar + When I stop running tests + Then the stop icon is not visible in the toolbar + + Scenario: When running tests, the nodes will have the progress icon and clicking stop will stop running (nose) + Given the package "nose" is installed + And the workspace setting "python.testing.pyTestEnabled" is enabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + And the file "tests/test_running_delay" has the following content + """ + 10 + """ + When I select the command "Python: Run All Tests" + And I wait for 1 second + Then all of the test tree nodes have a progress icon + And the stop icon is visible in the toolbar + When I stop running tests + Then the stop icon is not visible in the toolbar diff --git a/uitests/uitests/features/testing/explorer/run.success.feature b/uitests/uitests/features/testing/explorer/run.success.feature new file mode 100644 index 000000000000..7894dffd4e95 --- /dev/null +++ b/uitests/uitests/features/testing/explorer/run.success.feature @@ -0,0 +1,176 @@ +@test +@https://github.com/DonJayamanne/pyvscSmokeTesting.git +Feature: Test Explorer Discovering icons and stop discovery + Scenario: When running tests, the nodes will have the progress icon and when completed will have a success status (unitest) + Given the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is enabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + And a file named "tests/test_running_delay" is created with the following contents + """ + 5 + """ + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 14 nodes in the tree + And 14 nodes have a status of "Unknown" + When I run the test node "test_two_first_suite" + And I wait for 1 seconds + Then the stop icon is visible in the toolbar + And 1 node has a status of "Progress" + And the node "test_two_first_suite" has a status of "Progress" + When I wait for tests to complete running + Then the node "test_one.py" has a status of "Success" + And the node "TestFirstSuite" has a status of "Success" + And the node "test_two_first_suite" has a status of "Success" + And 11 nodes have a status of "Unknown" + + + Scenario: When running tests, the nodes will have the progress icon and when completed will have a success status (pytest) + Given the package "pytest" is installed + And the workspace setting "python.testing.pyTestEnabled" is enabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + And a file named "tests/test_running_delay" is created with the following contents + """ + 5 + """ + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 15 nodes in the tree + And 15 nodes have a status of "Unknown" + When I run the test node "test_two_first_suite" + And I wait for 1 seconds + Then the stop icon is visible in the toolbar + And 1 node has a status of "Progress" + And the node "test_two_first_suite" has a status of "Progress" + When I wait for tests to complete running + Then the node "test_one.py" has a status of "Success" + And the node "TestFirstSuite" has a status of "Success" + And the node "test_two_first_suite" has a status of "Success" + And 11 nodes have a status of "Unknown" + + Scenario: When running tests, the nodes will have the progress icon and when completed will have a success status (nose) + Given the package "nose" is installed + And the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is enabled + And a file named "tests/test_running_delay" is created with the following contents + """ + 5 + """ + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 14 nodes in the tree + And 14 nodes have a status of "Unknown" + When I run the test node "test_two_first_suite" + And I wait for 1 seconds + Then the stop icon is visible in the toolbar + And 1 node has a status of "Progress" + And the node "test_two_first_suite" has a status of "Progress" + When I wait for tests to complete running + Then the node "tests/test_one.py" has a status of "Success" + And the node "TestFirstSuite" has a status of "Success" + And the node "test_two_first_suite" has a status of "Success" + And 11 nodes have a status of "Unknown" + + + Scenario: When running tests, the nodes will have the progress icon and when completed will have a error status (unitest) + Given the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is enabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + And a file named "tests/test_running_delay" is created with the following contents + """ + 5 + """ + And a file named "tests/data.json" is created with the following contents + """ + [1,2,-1,4,5,6] + """ + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 14 nodes in the tree + And 14 nodes have a status of "Unknown" + When I run the test node "test_three_first_suite" + And I wait for 1 seconds + Then the stop icon is visible in the toolbar + And 1 node has a status of "Progress" + And the node "test_three_first_suite" has a status of "Progress" + When I wait for tests to complete running + Then the node "test_one.py" has a status of "Fail" + And the node "TestFirstSuite" has a status of "Fail" + And the node "test_three_first_suite" has a status of "Fail" + And 11 nodes have a status of "Unknown" + + Scenario: When running tests, the nodes will have the progress icon and when completed will have a error status (pytest) + Given the package "pytest" is installed + And the workspace setting "python.testing.pyTestEnabled" is enabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + And a file named "tests/test_running_delay" is created with the following contents + """ + 5 + """ + And a file named "tests/data.json" is created with the following contents + """ + [1,2,-1,4,5,6] + """ + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 15 nodes in the tree + And 15 nodes have a status of "Unknown" + When I run the test node "test_three_first_suite" + And I wait for 1 seconds + Then the stop icon is visible in the toolbar + And 1 node has a status of "Progress" + And the node "test_three_first_suite" has a status of "Progress" + When I wait for tests to complete running + Then the node "test_one.py" has a status of "Fail" + And the node "TestFirstSuite" has a status of "Fail" + And the node "test_three_first_suite" has a status of "Fail" + And 11 nodes have a status of "Unknown" + + Scenario: When running tests, the nodes will have the progress icon and when completed will have a error status (nose) + Given the package "nose" is installed + And the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is enabled + And a file named "tests/test_running_delay" is created with the following contents + """ + 5 + """ + And a file named "tests/data.json" is created with the following contents + """ + [1,2,-1,4,5,6] + """ + When I reload VSC + When I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + When I select the command "View: Show Test" + And I expand all of the test tree nodes + Then there are 14 nodes in the tree + And 14 nodes have a status of "Unknown" + When I run the test node "test_three_first_suite" + And I wait for 1 seconds + Then the stop icon is visible in the toolbar + And 1 node has a status of "Progress" + And the node "test_three_first_suite" has a status of "Progress" + When I wait for tests to complete running + Then the node "tests/test_one.py" has a status of "Fail" + And the node "TestFirstSuite" has a status of "Fail" + And the node "test_three_first_suite" has a status of "Fail" + And 11 nodes have a status of "Unknown" diff --git a/uitests/uitests/features/testing/explorer/visible.feature b/uitests/uitests/features/testing/explorer/visible.feature new file mode 100644 index 000000000000..a5b56f3ca88f --- /dev/null +++ b/uitests/uitests/features/testing/explorer/visible.feature @@ -0,0 +1,29 @@ +@test +@https://github.com/DonJayamanne/pyvscSmokeTesting.git +Feature: Test Explorer + + Scenario: Explorer will be displayed when tests are discovered (unitest) + Given the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is enabled + And the workspace setting "python.testing.nosetestsEnabled" is disabled + When I reload VSC + And I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + + Scenario: Explorer will be displayed when tests are discovered (pytest) + Given the package "pytest" is installed + And the workspace setting "python.testing.pyTestEnabled" is enabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is enabled + When I reload VSC + And I select the command "Python: Discover Tests" + Then the test explorer icon will be visible + + Scenario: Explorer will be displayed when tests are discovered (nose) + Given the package "nose" is installed + And the workspace setting "python.testing.pyTestEnabled" is disabled + And the workspace setting "python.testing.unittestEnabled" is disabled + And the workspace setting "python.testing.nosetestsEnabled" is enabled + When I reload VSC + And I select the command "Python: Discover Tests" + Then the test explorer icon will be visible diff --git a/uitests/uitests/js/chromeDownloader.js b/uitests/uitests/js/chromeDownloader.js new file mode 100644 index 000000000000..4d9acad1ba78 --- /dev/null +++ b/uitests/uitests/js/chromeDownloader.js @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +const fs = require('fs') +const path = require('path') +const electronDownload = require('electron-download') +const extractZip = require('extract-zip') +const versionToDownload = process.argv.length > 2 ? process.argv[2] : '3.1.3'; +const downloadDir = process.argv.length > 3 ? process.argv[3] : path.join(__dirname, 'bin'); + +function download(version, callback) { + electronDownload({ + version, + chromedriver: true, + platform: process.env.npm_config_platform, + arch: process.env.npm_config_arch, + strictSSL: process.env.npm_config_strict_ssl === 'true', + quiet: ['info', 'verbose', 'silly', 'http'].indexOf(process.env.npm_config_loglevel) === -1 + }, callback) +} + +function processDownload(err, zipPath) { + if (err != null) throw err + extractZip(zipPath, { dir: downloadDir }, error => { + if (error != null) throw error + if (process.platform !== 'win32') { + fs.chmod(path.join(downloadDir, 'chromedriver'), '755', error => { + if (error != null) throw error + }) + } + }) +} + +download(versionToDownload, (err, zipPath) => { + if (err) { + const parts = versionToDownload.split('.') + const baseVersion = `${parts[0]}.${parts[1]}.0` + download(baseVersion, processDownload) + } else { + processDownload(err, zipPath) + } +}) diff --git a/uitests/uitests/js/report.js b/uitests/uitests/js/report.js new file mode 100644 index 000000000000..bc0824b3de83 --- /dev/null +++ b/uitests/uitests/js/report.js @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +const reporter = require('cucumber-html-reporter'); +const path = require('path'); +const os = require('os'); +const reportsDir = process.argv[2]; +const launchReport = process.argv[3].toUpperCase() === 'TRUE'; + +const options = { + theme: 'bootstrap', + jsonFile: path.join(reportsDir, 'report.json'), + output: path.join(reportsDir, 'report.html'), + reportSuiteAsScenarios: true, + launchReport, + metadata: { + "Platform": os.platform() + } +}; + +reporter.generate(options, () => process.exit(0)); diff --git a/uitests/uitests/js/unzip.js b/uitests/uitests/js/unzip.js new file mode 100644 index 000000000000..5b25fe3ba023 --- /dev/null +++ b/uitests/uitests/js/unzip.js @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +const gulp = require('gulp'); +const fs = require("fs-extra"); +const vzip = require('gulp-vinyl-zip'); +const vfs = require('vinyl-fs'); +const untar = require('gulp-untar'); +const gunzip = require('gulp-gunzip'); +const chmod = require('gulp-chmod'); +const filter = require('gulp-filter'); + +const zipFile = process.argv[2]; +const targetDir = process.argv[3]; + +const unzipFn = (zipFile.indexOf('.gz') > 0 || zipFile.indexOf('.tag') > 0) ? unzipTarGz : unzip; + +unzipFn(zipFile, targetDir).catch(ex => { + console.error(ex); + return Promise.reject(ex); +}); + +async function unzip(zipFile, targetFolder) { + await fs.ensureDir(targetFolder); + return new Promise((resolve, reject) => { + gulp.src(zipFile) + .pipe(vzip.src()) + .pipe(vfs.dest(targetFolder)) + .on('end', resolve) + .on('error', reject); + }); +} + +async function unzipTarGz(zipFile, targetFolder) { + await fs.ensureDir(targetFolder); + return new Promise((resolve, reject) => { + var gulpFilter = filter(['VSCode-linux-x64/code', 'VSCode-linux-x64/code-insiders', 'VSCode-linux-x64/resources/app/node_modules*/vscode-ripgrep/**/rg'], { restore: true }); + gulp.src(zipFile) + .pipe(gunzip()) + .pipe(untar()) + .pipe(gulpFilter) + .pipe(chmod(493)) // 0o755 + .pipe(gulpFilter.restore) + .pipe(vfs.dest(targetFolder)) + .on('end', resolve) + .on('error', reject); + }); +} diff --git a/uitests/uitests/misc.py b/uitests/uitests/misc.py new file mode 100644 index 000000000000..0adeadd496a1 --- /dev/null +++ b/uitests/uitests/misc.py @@ -0,0 +1,70 @@ +from azure.cognitiveservices.vision.computervision import ComputerVisionClient +from azure.cognitiveservices.vision.computervision.models import VisualFeatureTypes +from msrest.authentication import CognitiveServicesCredentials +from azure.cognitiveservices.vision.computervision.models import TextRecognitionMode +from azure.cognitiveservices.vision.computervision.models import TextOperationStatusCodes +import time + +# Get endpoint and key from environment variables +import os +endpoint = "xx" +key = "xx" + +# Set credentials +credentials = CognitiveServicesCredentials(key) + +# Create client +client = ComputerVisionClient(endpoint, credentials) + +#%% +# domain = "landmarks" +# url = "http://www.public-domain-photos.com/free-stock-photos-4/travel/san-francisco/golden-gate-bridge-in-san-francisco.jpg" +# language = "en" +# max_descriptions = 3 + +# analysis = client.describe_image(url, max_descriptions, language) + +# for caption in analysis.captions: +# print(caption.text) +# print(caption.confidence) + +#%% +# url = "https://upload.wikimedia.org/wikipedia/commons/thumb/1/12/Broadway_and_Times_Square_by_night.jpg/450px-Broadway_and_Times_Square_by_night.jpg" + +# image_analysis = client.analyze_image(url,visual_features=[VisualFeatureTypes.tags]) + +# for tag in image_analysis.tags: +# print(tag) + +#%% +url = "https://user-images.githubusercontent.com/1948812/57894667-877b9d00-77fc-11e9-97e9-405edee1e2d0.png" +# mode = TextRecognitionMode.handwritten +mode = TextRecognitionMode.printed +raw = True +custom_headers = None +numberOfCharsInOperationId = 36 + +# Async SDK call +# rawHttpResponse = client.batch_read_file(url, mode, custom_headers, raw) + +with open("/Users/donjayamanne/Desktop/Screen Shot 2019-05-16 at 7.40.41 PM.png", "rb") as fp: + rawHttpResponse = client.batch_read_file_in_stream(fp, mode, custom_headers, raw) + +# Get ID from returned headers +operationLocation = rawHttpResponse.headers["Operation-Location"] +idLocation = len(operationLocation) - numberOfCharsInOperationId +operationId = operationLocation[idLocation:] + +# SDK call +while True: + result = client.get_read_operation_result(operationId) + if result.status not in ['NotStarted', 'Running']: + break + time.sleep(1) + +# Get data +if result.status == TextOperationStatusCodes.succeeded: + for textResult in result.recognition_results: + for line in textResult.lines: + print(line.text) + # print(line.bounding_box) diff --git a/uitests/uitests/report.py b/uitests/uitests/report.py new file mode 100644 index 000000000000..c7008239c779 --- /dev/null +++ b/uitests/uitests/report.py @@ -0,0 +1,255 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# Original source can be found here: +# https://gist.github.com/fredizzimo/b92adf1d4596c0c1da1b05cc9899574b +# Code has been adopted to allow for adding attachments to cucumber reports. + +import base64 +import copy +import json + +import behave.formatter.base +import behave.model_core + + +class CucumberJSONFormatter(behave.formatter.base.Formatter): + instance = None + name = "json" + description = "JSON dump of test run" + dumps_kwargs = {} + + json_number_types = (int, float) + json_scalar_types = (str, bool, type(None)) + + def __new__(cls, stream_opener, config): + if cls.instance is None: + cls.instance = object.__new__(cls) + return cls.instance + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.stream = self.open() + self.feature_count = 0 + self.attachments = [] + self.reset() + + @property + def current_feature_element(self): + assert self.current_feature_data is not None + return self.current_feature_data["elements"][-1] + + @property + def current_step(self): + step_index = self._step_index + if self.current_feature.background is not None: + element = self.current_feature_data["elements"][-2] + if step_index >= len(self.current_feature.background.steps): + step_index -= len(self.current_feature.background.steps) + element = self.current_feature_element + else: + element = self.current_feature_element + + return element["steps"][step_index] + + def reset(self): + self.current_feature = None + self.current_feature_data = None + self._step_index = 0 + self.current_background = None + + def uri(self, uri): + pass + + def status(self, status_obj): + if status_obj == behave.model_core.Status.passed: + return "passed" + elif status_obj == behave.model_core.Status.failed: + return "failed" + else: + return "skipped" + + def feature(self, feature): + self.reset() + self.current_feature = feature + self.current_feature_data = { + "id": self.generate_id(feature), + "uri": feature.location.filename, + "line": feature.location.line, + "description": "", + "keyword": feature.keyword, + "name": feature.name, + "tags": self.write_tags(feature.tags), + "status": self.status(feature.status), + } + element = self.current_feature_data + if feature.description: + element["description"] = self.format_description(feature.description) + + def background(self, background): + element = { + "type": "background", + "keyword": background.keyword, + "name": background.name, + "location": str(background.location), + "steps": [], + } + self._step_index = 0 + self.current_background = element + + def scenario(self, scenario): + if self.current_background is not None: + self.add_feature_element(copy.deepcopy(self.current_background)) + element = self.add_feature_element( + { + "type": "scenario", + "id": self.generate_id(self.current_feature, scenario), + "line": scenario.location.line, + "description": "", + "keyword": scenario.keyword, + "name": scenario.name, + "tags": self.write_tags(scenario.tags), + "location": str(scenario.location), + "steps": [], + } + ) + if scenario.description: + element["description"] = self.format_description(scenario.description) + self._step_index = 0 + + def step(self, step): + self.attachments.clear() + step_info = { + "keyword": step.keyword, + "step_type": step.step_type, + "name": step.name, + "line": step.location.line, + "result": {"status": "skipped", "duration": 0}, + "embeddings": [], + } + + if step.text: + step_info["doc_string"] = {"value": step.text, "line": step.text.line} + if step.table: + step_info["rows"] = [ + {"cells": [heading for heading in step.table.headings]} + ] + step_info["rows"] += [ + {"cells": [cell for cell in row.cells]} for row in step.table + ] + + if self.current_feature.background is not None: + element = self.current_feature_data["elements"][-2] + if len(element["steps"]) >= len(self.current_feature.background.steps): + element = self.current_feature_element + else: + element = self.current_feature_element + element["steps"].append(step_info) + + def match(self, match): + if match.location: + match_data = {"location": str(match.location) or ""} + self.current_step["match"] = match_data + + def attach_image(self, base64): + self.attachments.append({"mime_type": "image/png", "data": base64}) + + def attach_html(self, html): + self.attachments.append({"mime_type": "text/html", "data": html}) + + def result(self, result): + self.current_step["embeddings"] = self.attachments.copy() + self.attachments.clear() + self.current_step["result"] = { + "status": self.status(result.status), + "duration": int(round(result.duration * 1000.0 * 1000.0 * 1000.0)), + } + if result.error_message and result.status == "failed": + error_message = result.error_message + result_element = self.current_step["result"] + result_element["error_message"] = error_message + self._step_index += 1 + + def embedding(self, mime_type, data): + step = self.current_feature_element["steps"][-1] + step["embeddings"].append( + {"mime_type": mime_type, "data": base64.b64encode(data).replace("\n", "")} + ) + + def eof(self): + """ + End of feature + """ + if not self.current_feature_data: + return + + self.update_status_data() + + if self.feature_count == 0: + self.write_json_header() + else: + self.write_json_feature_separator() + + self.write_json_feature(self.current_feature_data) + self.current_feature_data = None + self.feature_count += 1 + + def close(self): + self.write_json_footer() + self.close_stream() + + def add_feature_element(self, element): + assert self.current_feature_data is not None + if "elements" not in self.current_feature_data: + self.current_feature_data["elements"] = [] + self.current_feature_data["elements"].append(element) + return element + + def update_status_data(self): + assert self.current_feature + assert self.current_feature_data + self.current_feature_data["status"] = self.status(self.current_feature.status) + + def write_tags(self, tags): + return [ + {"name": tag, "line": tag.line if hasattr(tag, "line") else 1} + for tag in tags + ] + + def generate_id(self, feature, scenario=None): + def convert(name): + return name.lower().replace(" ", "-") + + id = convert(feature.name) + if scenario is not None: + id += ";" + id += convert(scenario.name) + return id + + def format_description(self, lines): + description = "\n".join(lines) + description = "
%s
" % description + return description + + def write_json_header(self): + self.stream.write("[\n") + + def write_json_footer(self): + self.stream.write("\n]\n") + + def write_json_feature(self, feature_data): + self.stream.write(json.dumps(feature_data, **self.dumps_kwargs)) + self.stream.flush() + + def write_json_feature_separator(self): + self.stream.write(",\n\n") + + +class PrettyCucumberJSONFormatter(CucumberJSONFormatter): + """ + Provides readable/comparable textual JSON output. + """ + + name = "json.pretty" + description = "JSON dump of test run (human readable)" + dumps_kwargs = {"indent": 4, "sort_keys": True} diff --git a/uitests/uitests/steps/commands.py b/uitests/uitests/steps/commands.py new file mode 100644 index 000000000000..4795d0b038f2 --- /dev/null +++ b/uitests/uitests/steps/commands.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import behave + +import uitests.vscode.quick_open + + +@behave.given('the command "{command}" is selected') +def given_command_selected(context, command): + uitests.vscode.quick_open.select_command(context, command) + + +@behave.when('I select the command "{command}"') +def when_select_command(context, command): + uitests.vscode.quick_open.select_command(context, command) + + +@behave.then('select the command "{command}"') +def then_select_command(context, command): + uitests.vscode.quick_open.select_command(context, command) diff --git a/uitests/uitests/steps/core.py b/uitests/uitests/steps/core.py new file mode 100644 index 000000000000..e822459e11e1 --- /dev/null +++ b/uitests/uitests/steps/core.py @@ -0,0 +1,115 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging +import sys +import time + +import behave + +import uitests.vscode.application +import uitests.vscode.quick_open +import uitests.vscode.startup + + +@behave.given("In Windows,{command}") +def given_on_windows(context, command): + if not sys.platform.startswith("win"): + return + context.execute_steps(f"Given {command.strip()}") + + +@behave.given("In Mac,{command}") +def given_on_mac(context, command): + if not sys.platform.startswith("darwin"): + return + context.execute_steps(f"Given {command.strip()}") + + +@behave.given("In Linux,{command}") +def given_on_linux(context, command): + if not sys.platform.startswith("linux"): + return + context.execute_steps(f"When {command.strip()}") + + +@behave.when("In Windows,{command}") +def when_on_widows(context, command): + if not sys.platform.startswith("win"): + return + context.execute_steps(f"When {command.strip()}") + + +@behave.when("In Mac,{command}") +def when_on_mac(context, command): + if not sys.platform.startswith("darwin"): + return + context.execute_steps(f"When {command.strip()}") + + +@behave.when("In Linux,{command}") +def when_on_linux(context, command): + if not sys.platform.startswith("linux"): + return + context.execute_steps(f"When {command.strip()}") + + +@behave.then("In Windows,{command}") +def then_on_windows(context, command): + if not sys.platform.startswith("win"): + return + context.execute_steps(f"Then {command.strip()}") + + +@behave.then("In Mac,{command}") +def then_on_mac(context, command): + if not sys.platform.startswith("darwin"): + return + context.execute_steps(f"Then {command.strip()}") + + +@behave.then("In Linux,{command}") +def then_on_linux(context, command): + if not sys.platform.startswith("linux"): + return + context.execute_steps(f"Then {command.strip()}") + + +@behave.when("I wait for {seconds:n} seconds") +def when_sleep(context, seconds): + time.sleep(seconds) + + +@behave.when("I wait for 1 second") +def when_sleep1(context): + time.sleep(1) + + +@behave.when("I reload VSC") +def when_reload(context): + uitests.vscode.startup.reload(context) + + +@behave.then("reload VSC") +def then_reload(context): + uitests.vscode.startup.reload(context) + + +@behave.then("wait for {seconds:n} seconds") +def then_sleep(context, seconds): + time.sleep(seconds) + + +@behave.then("wait for 1 second") +def then_sleep1(context, seconds): + time.sleep(seconds) + + +@behave.then('log the message "{message}"') +def log_message(context, message): + logging.info(message) + + +@behave.then("take a screenshot") +def capture_screen(context): + uitests.vscode.application.capture_screen(context) diff --git a/uitests/uitests/steps/debugger.py b/uitests/uitests/steps/debugger.py new file mode 100644 index 000000000000..7c23ae40c9c9 --- /dev/null +++ b/uitests/uitests/steps/debugger.py @@ -0,0 +1,50 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os.path +import time + +import behave + +import uitests.vscode.debugger +import uitests.tools + + +@behave.then("the debugger starts") +def then_starts(context): + uitests.vscode.debugger.wait_for_debugger_to_start(context) + + +@behave.then("the debugger stops") +def then_stops(context): + uitests.vscode.debugger.wait_for_debugger_to_stop(context) + + +@behave.then("the debugger pauses") +def then_stops(context): + uitests.vscode.debugger.wait_for_debugger_to_pause(context) + + +@behave.when('I add a breakpoint to line {line:Number} in "{file}"') +def add_breakpoint(context, line, file): + uitests.vscode.debugger.add_breakpoint(context, file, line) + + +@behave.then('the current stack frame is at line {line_number:Number} in "{file_name}"') +@uitests.tools.retry(AssertionError) +def current_stack_is(context, line_number, file_name): + uitests.vscode.documents.is_file_open(context, file_name) + current_position = uitests.vscode.documents.get_current_position(context) + assert current_position[0] == line_number + + +# @behave.then( +# 'the current stack frame is not at line {line_number:Number} in "{file_name}"' +# ) +# def current_stack_is_not(context, line_number, file_name): +# try: +# current_frame = uitests.vscode.debugger.get_current_frame_position(context) +# assert current_frame[0] != file_name +# assert current_frame[1] != line_number +# except Exception: +# pass diff --git a/uitests/uitests/steps/documents.py b/uitests/uitests/steps/documents.py new file mode 100644 index 000000000000..0a51a7d457ac --- /dev/null +++ b/uitests/uitests/steps/documents.py @@ -0,0 +1,101 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os.path + +import behave + +import uitests.vscode.documents +import uitests.tools + + +@behave.given('a file named "{name}" is created with the following contents') +def given_file_create(context, name): + with open(os.path.join(context.options.workspace_folder, name), "w") as file: + file.write(context.text) + + +@behave.when('the file "{name}" has the following content') +def when_file_with_content(context, name): + with open(os.path.join(context.options.workspace_folder, name), "w") as file: + file.write(context.text) + + +@behave.given('a file named "{name}" does not exist') +def given_file_no_exist(context, name): + try: + os.unlink(os.path.join(context.options.workspace_folder, name)) + except Exception: + pass + + +@behave.given('the file "{name}" does not exist') +def given_the_file_no_exist(context, name): + try: + os.unlink(os.path.join(context.options.workspace_folder, name)) + except Exception: + pass + + +@behave.then('a file named "{name}" is created') +@uitests.tools.retry(AssertionError) +def then_file_exists(context, name): + assert os.path.exists(os.path.join(context.options.workspace_folder, name)) + + +@behave.given('the file "{name}" is open') +def given_file_opened(context, name): + uitests.vscode.documents.open_file(context, name) + + +@behave.then('the file "{name}" is opened') +def then_file_opened(context, name): + uitests.vscode.documents.is_file_open(context, name) + + +@behave.when("I go to line {line_number:Number}") +def when_go_to_line(context, line_number): + uitests.vscode.documents.go_to_line(context, line_number) + + +@behave.then("the cursor is on line {line_number:Number}") +@uitests.tools.retry(AssertionError) +def then_line(context, line_number): + value = uitests.vscode.documents.get_current_position(context) + assert line_number == value[0] + + +@behave.then("the cursor is on line {line_number:Number} and column {column:Number}") +@uitests.tools.retry(AssertionError) +def then_line_and_column(context, line_number, column): + value = uitests.vscode.documents.get_current_position(context) + assert line_number == value[0] + assert column == value[0] + + +@behave.then('the file "{name}" contains the value "{value}"') +@uitests.tools.retry(AssertionError) +def file_contains(context, name, value): + file_name = os.path.join(context.options.workspace_folder, name) + with open(file_name, "r") as file: + contents = file.read() + assert value in contents + + +@behave.then('the file "{name}" does not contain the value "{value}"') +@uitests.tools.retry(AssertionError) +def file_not_contains(context, name, value): + file_name = os.path.join(context.options.workspace_folder, name) + with open(file_name, "r") as file: + contents = file.read() + assert value not in contents + + +@behave.when('I open the file "{name}"') +def when_file_opened(context, name): + uitests.vscode.documents.open_file(context, name) + + +@behave.then('open the file "{name}"') +def then_open_file(context, name): + uitests.vscode.documents.open_file(context, name) diff --git a/uitests/uitests/steps/interpreter.py b/uitests/uitests/steps/interpreter.py new file mode 100644 index 000000000000..b0d6243ab209 --- /dev/null +++ b/uitests/uitests/steps/interpreter.py @@ -0,0 +1,83 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import os.path +import sys + +import behave + +import uitests.tools +import uitests.vscode.quick_input +import uitests.vscode.quick_open +import uitests.vscode.settings + + +@behave.given('a Python Interpreter containing the name "{name}" is selected') +def given_select_interpreter_with_name(context, name): + uitests.vscode.quick_open.select_command(context, "Python: Select Interpreter") + uitests.vscode.quick_input.select_value(context, name) + + +@behave.given('a venv with the name "{name}" is created') +def given_venv_created(context, name): + context.execute_steps("Given a terminal is opened") + context.execute_steps( + f'When I send the command ""{context.options.python3_path}" -m venv "{name}"" to the terminal' + ) + uitests.tools.wait_for_python_env(context.options.workspace_folder, name) + + +@behave.given("a pipenv environment is created") +def given_pipenv_created(context): + context.execute_steps("Given a terminal is opened") + context.execute_steps( + f'When I send the command "pipenv shell --anyway" to the terminal' + ) + uitests.tools.wait_for_pipenv(context.options.workspace_folder) + + +@behave.given('a conda environment is created with the name "{name}"') +def given_conda_env_created(context, name): + context.execute_steps("Given a terminal is opened") + context.execute_steps( + f'When I send the command ""{context.options.conda_path}" create --yes --name "{name}"" to the terminal' + ) + uitests.tools.wait_for_conda_env(context.options.conda_path, name) + + +@behave.given("a generic Python Interpreter is selected") +def given_select_generic_interpreter(context): + uitests.vscode.settings.update_workspace_settings( + context, {"python.pythonPath": sys.executable} + ) + + +@behave.when('I select the Python Interpreter containing the name "{name}"') +def when_select_interpreter_with_name(context, name): + uitests.vscode.quick_open.select_command(context, "Python: Select Interpreter") + uitests.vscode.quick_input.select_value(context, name) + + +@behave.when("I select the default mac Interpreter") +def select_interpreter(context): + uitests.vscode.quick_open.select_command(context, "Python: Select Interpreter") + uitests.vscode.quick_input.select_value(context, "/usr/bin/python") + + +@behave.then( + 'the contents of the file "{name}" does not contain the current python interpreter' +) +def file_not_contains_interpreter(context, name): + with open(os.path.join(context.options.workspace_folder, name), "r") as file: + contents = file.read() + assert context.options.python_path not in contents + + +@behave.then( + 'the contents of the file "{name}" contains the current python interpreter' +) +def file_contains_interpreter(context, name): + with open(os.path.join(context.options.workspace_folder, name), "r") as file: + contents = file.read() + assert context.options.python_path in contents diff --git a/uitests/uitests/steps/notifications.py b/uitests/uitests/steps/notifications.py new file mode 100644 index 000000000000..d903fa5e5adc --- /dev/null +++ b/uitests/uitests/steps/notifications.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import behave +import uitests.vscode.notifications + + +@behave.then('a message with the text "{message}" is displayed') +def show_message(context, message): + uitests.vscode.notifications.wait_for_message(context, message) + + +@behave.then('a message containing the text "{message}" is displayed') +def show_message_containing(context, message): + uitests.vscode.notifications.wait_for_message_containing(context, message) diff --git a/uitests/uitests/steps/output_panel.py b/uitests/uitests/steps/output_panel.py new file mode 100644 index 000000000000..8dbb0efa0893 --- /dev/null +++ b/uitests/uitests/steps/output_panel.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import behave + +import uitests.tools +import uitests.vscode.output_panel + + +@behave.then('the output panel contains the text "{text}"') +@uitests.tools.retry(AssertionError) +def then_output_contains(context, text): + lines = uitests.vscode.output_panel.get_output_panel_lines(context) + assert text.lower() in "".join(lines).lower() diff --git a/uitests/uitests/steps/packages.py b/uitests/uitests/steps/packages.py new file mode 100644 index 000000000000..eeb6af057216 --- /dev/null +++ b/uitests/uitests/steps/packages.py @@ -0,0 +1,56 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import behave +import uitests.tools + + +@behave.given('the package "{name}" is not installed') +def given_no_package(context, name): + _uninstall_module(context, name) + + +@behave.when('I uninstall the package "{name}"') +def when_no_package(context, name): + _uninstall_module(context, name) + + +@behave.then('uninstall the package "{name}"') +def then_no_package(context, name): + _uninstall_module(context, name) + + +@behave.given('the package "{name}" is installed') +def given_package_installed(context, name): + _install_module(context, name) + + +@behave.when('I install the package "{name}"') +def when_package_installed(context, name): + _install_module(context, name) + + +@behave.then('install the package "{name}"') +def then_package_installed(context, name): + _install_module(context, name) + + +def _uninstall_module(context, name): + python_path = context.options.python_path + try: + uitests.tools.run_command( + [python_path, "-m", "pip", "uninstall", name, "-y", "-q"], silent=True + ) + except Exception: + pass + + +def _install_module(context, name): + python_path = context.options.python_path + try: + uitests.tools.run_command( + [python_path, "-m", "pip", "install", name, "-q"], silent=True + ) + except Exception: + pass diff --git a/uitests/uitests/steps/problems.py b/uitests/uitests/steps/problems.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/uitests/uitests/steps/sample.py b/uitests/uitests/steps/sample.py new file mode 100644 index 000000000000..bd5cacead899 --- /dev/null +++ b/uitests/uitests/steps/sample.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import behave +import uitests.vscode.application + + +@behave.given("we have behave installed") +def step_impl(context): + uitests.vscode.application.capture_screen(context) + pass + + +@behave.when("we implement a test") +def implement_test(context): + assert True is not False + + +@behave.then("behave will test it for us!") +def test_it(context): + uitests.vscode.application.capture_screen(context) + assert True + + +@behave.then("Another one!") +def another(context): + assert True diff --git a/uitests/uitests/steps/settings.py b/uitests/uitests/steps/settings.py new file mode 100644 index 000000000000..09341e421fcf --- /dev/null +++ b/uitests/uitests/steps/settings.py @@ -0,0 +1,106 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os.path + +import behave + +import uitests.vscode.settings + + +@behave.given('the workspace setting "{name}" has the value "{value}"') +def given_workspace_setting(context, name, value): + uitests.vscode.settings.update_workspace_settings(context, {name: value}) + + +@behave.given('the workspace setting "{name}" is enabled') +def given_workspace_setting_enabled(context, name): + uitests.vscode.settings.update_workspace_settings(context, {name: True}) + + +@behave.given('the workspace setting "{name}" is disabled') +def given_workspace_setting_disabled(context, name): + uitests.vscode.settings.update_workspace_settings(context, {name: False}) + + +@behave.given('the user setting "{name}" is disabled') +def given_user_setting_disabled(context, name): + settings_json = os.path.join(context.options.user_dir, "User", "settings.json") + uitests.vscode.settings.update_settings(settings_json, {name: False}) + + +@behave.when('I update the workspace setting "{name}" with the value "{value}"') +def given_workspace_setting_value(context, name, value): + uitests.vscode.settings.update_workspace_settings(context, {name: value}) + + +@behave.when('I enable the workspace setting "{name}"') +def when_workspace_setting_enable(context, name): + uitests.vscode.settings.update_workspace_settings(context, {name: True}) + + +@behave.when('I disable the workspace setting "{name}"') +def when_workspace_setting_disable(context, name): + uitests.vscode.settings.update_workspace_settings(context, {name: False}) + + +@behave.given('the workspace setting "{name}" does not exist') +def given_workspace_setting_is_removed(context, name): + uitests.vscode.settings.remove_workspace_setting(context, name) + + +@behave.given('the user setting "{name}" does not exist') +def given_user_setting_is_removed(context, name): + uitests.vscode.settings.remove_user_setting(context, name) + + +@behave.given('the user setting "{name}" exists') +def given_user_setting_is_not_empty(context, name): + settings_json = os.path.join(context.options.user_dir, "User", "settings.json") + current_value = uitests.vscode.settings.get_setting(settings_json, name) + assert current_value is not None + + +@behave.when('I remove the workspace setting "{name}"') +def when_workspace_setting_is_removed(context, name): + uitests.vscode.settings.remove_workspace_setting(context, name) + + +@behave.then('the workspace setting "{name}" is enabled') +def then_workspace_setting_enabled(context, name): + settings_json = os.path.join( + context.options.workspace_folder, ".vscode", "settings.json" + ) + assert uitests.vscode.settings.get_setting(settings_json, name) is True + + +@behave.then('the workspace setting "{name}" is disabled') +def then_workspace_setting_disabled(context, name): + settings_json = os.path.join( + context.options.workspace_folder, ".vscode", "settings.json" + ) + assert uitests.vscode.settings.get_setting(settings_json, name) is False + + +@behave.then('the workspace setting "{name}" is "{value}"') +def then_workspace_setting_value(context, name, value): + settings_json = os.path.join( + context.options.workspace_folder, ".vscode", "settings.json" + ) + assert uitests.vscode.settings.get_setting(settings_json, name) == value + + +@behave.then('the workspace setting "{name}" contains the value "{value}"') +def then_workspace_setting_contains_value(context, name, value): + settings_json = os.path.join( + context.options.workspace_folder, ".vscode", "settings.json" + ) + assert value in uitests.vscode.settings.get_setting(settings_json, name) + + +@behave.then('the workspace setting "{name}" exists') +def then_workspace_setting_is_defined(context, name): + settings_json = os.path.join( + context.options.workspace_folder, ".vscode", "settings.json" + ) + assert uitests.vscode.settings.get_setting(settings_json, name) is not None diff --git a/uitests/uitests/steps/status_bar.py b/uitests/uitests/steps/status_bar.py new file mode 100644 index 000000000000..20b770c3166d --- /dev/null +++ b/uitests/uitests/steps/status_bar.py @@ -0,0 +1,65 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import time + +import behave +from selenium.common.exceptions import StaleElementReferenceException + +import uitests.vscode.status_bar +import uitests.tools + + +@behave.then( + 'the python interpreter displayed in the the status bar contains the value "{name}" in the tooltip' +) +@uitests.tools.retry((AssertionError, StaleElementReferenceException)) +def then_selected_interpreter_has_tooltip(context, name): + element = uitests.vscode.status_bar.wait_for_python_statusbar(context) + assert name in element.get_attribute("title") + # start_time = time.time() + # while time.time() - start_time < 5: + # element = uitests.vscode.status_bar.wait_for_python_statusbar(context) + # try: + # assert name in element.get_attribute("title") + # return + # except (AssertionError, StaleElementReferenceException): + # time.sleep(0.5) + # assert name in element.get_attribute("title") + + +@behave.then( + 'the python interpreter displayed in the the status bar contains the value "{name}" in the display name' +) +@uitests.tools.retry((AssertionError, StaleElementReferenceException)) +def then_selected_interpreter_has_text(context, name): + element = uitests.vscode.status_bar.wait_for_python_statusbar(context) + assert name in element.text + # start_time = time.time() + # while time.time() - start_time < 10: + # element = uitests.vscode.status_bar.wait_for_python_statusbar(context) + # try: + # assert name in element.text + # return + # except (AssertionError, StaleElementReferenceException): + # time.sleep(0.5) + # assert name in element.text + + +@behave.then( + 'the python interpreter displayed in the the status bar does not contain the value "{name}" in the display name' +) +@uitests.tools.retry((AssertionError, StaleElementReferenceException)) +def then_selected_interpreter_does_not_have_text(context, name): + element = uitests.vscode.status_bar.wait_for_python_statusbar(context) + assert name not in element.text + # start_time = time.time() + # while time.time() - start_time < 10: + # element = uitests.vscode.status_bar.wait_for_python_statusbar(context) + # try: + # assert name not in element.text + # return + # except (AssertionError, StaleElementReferenceException): + # time.sleep(0.5) + # assert name not in element.text diff --git a/uitests/uitests/steps/terminal.py b/uitests/uitests/steps/terminal.py new file mode 100644 index 000000000000..c143130ea6cc --- /dev/null +++ b/uitests/uitests/steps/terminal.py @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import os.path +import time + +import behave + +import uitests.vscode.quick_open + + +@behave.given("a terminal is opened") +def terminal_opened(context): + uitests.vscode.quick_open.select_command( + context, "Terminal: Create New Integrated Terminal" + ) + time.sleep(5) # wait for terminal to open and wait for activation. + + +@behave.when('I send the command "{command}" to the terminal') +def send_command_to_terminal(context, command): + with open( + os.path.join(context.options.extensions_dir, "commands.txt"), "w" + ) as file: + file.write(command) + uitests.vscode.quick_open.select_command(context, "Smoke: Run Command In Terminal") diff --git a/uitests/uitests/steps/testing.py b/uitests/uitests/steps/testing.py new file mode 100644 index 000000000000..edf4bbdd5448 --- /dev/null +++ b/uitests/uitests/steps/testing.py @@ -0,0 +1,198 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import time + +import behave + +import uitests.vscode.testing +import uitests.tools + +node_status_icon_mapping = { + "UNKNOWN": "status-unknown.svg", + "SKIP": "status-unknown.svg", + "PROGRESS": "discovering-tests.svg", + "OK": "status-ok.svg", + "PASS": "status-ok.svg", + "SUCCESS": "status-ok.svg", + "FAIL": "status-error.svg", + "ERROR": "status-error.svg", +} + + +@behave.then("the test explorer icon will be visible") +@uitests.tools.retry(AssertionError) +def icon_visible(context): + uitests.vscode.testing.wait_for_explorer_icon(context) + + +@behave.when("I run the test node number {number:Number}") +def run_node(context, number): + uitests.vscode.testing.click_node_action_item(context, number, "Run") + + +@behave.when('I run the test node "{label}"') +def run_node_by_name(context, label): + number = uitests.vscode.testing.get_node_number(context, label) + run_node(context, number) + + +@behave.when("I debug the test node number {number:Number}") +def debug_node(context, number): + uitests.vscode.testing.click_node_action_item(context, number, "Debug") + + +@behave.when('I debug the test node "{label}"') +def debug_node_by_name(context, label): + number = uitests.vscode.testing.get_node_number(context, label) + debug_node(context, number) + + +@behave.when("I navigate to the code associated with test node number {number:Number}") +def navigate_node(context, number): + uitests.vscode.testing.click_node_action_item(context, number, "Open") + + +@behave.when('I navigate to the code associated with test node "{label}"') +def navigate_node_by_name(context, label): + number = uitests.vscode.testing.get_node_number(context, label) + navigate_node(context, number) + + +@behave.then("there are {count:Number} nodes in the tree") +def explorer_node_count(context, count): + total_count = uitests.vscode.testing.get_node_count(context) + assert total_count == count + + +@behave.when("I expand all of the test tree nodes") +def explorer_expand_nodes(context): + try: + uitests.vscode.testing.expand_nodes(context) + return + except TimeoutError: + # Rediscover tests. + uitests.vscode.quick_open.select_command(context, "Python: Discover Tests") + # As this is a flaky scenario, lets wait for 5s. + # Enough time for tests to start & perhaps complete. + time.sleep(5) + # If tests discovery has not completed, then lets wait. + wait_for_discovery_to_complete(context) + # try again. + uitests.vscode.testing.expand_nodes(context) + + +@behave.when("I click node number {number:Number}") +def click_node(context, number): + uitests.vscode.testing.click_node(context, number) + + +@behave.when('I click node "{label}"') +def click_node_by_name(context, label): + number = uitests.vscode.testing.get_node_number(context, label) + click_node(context, number) + + +@behave.then("all of the test tree nodes have an unknown icon") +def all_unknown(context): + icons = uitests.vscode.testing.get_node_icons(context) + assert all("status-unknown.svg" in icon.get_attribute("style") for icon in icons) + + +@behave.then('the node number {number:Number} has a status of "{status}"') +@uitests.tools.retry(AssertionError) +def node_status(context, number, status): + icon = uitests.vscode.testing.get_node_icon(context, number) + assert node_status_icon_mapping.get(status.upper(), "") in icon.get_attribute( + "style" + ) + + +@behave.then('the node "{label}" has a status of "{status}"') +@uitests.tools.retry(AssertionError) +def node_status_by_name(context, label, status): + number = uitests.vscode.testing.get_node_number(context, label) + node_status(context, number, status) + + +@behave.then('{number:Number} nodes have a status of "{status}"') +@uitests.tools.retry(AssertionError) +def node_count_status(context, number, status): + check_node_count_status(context, number, status) + + +@behave.then('1 node has a status of "{status}"') +@uitests.tools.retry(AssertionError) +def node_one_status(context, status): + check_node_count_status(context, 1, status) + + +@behave.then("all of the test tree nodes have a progress icon") +def all_progress(context): + icons = uitests.vscode.testing.get_node_icons(context) + assert all("discovering-tests.svg" in icon.get_attribute("style") for icon in icons) + + +@behave.then("the stop icon is visible in the toolbar") +@uitests.tools.retry(AssertionError) +def stop_icon_visible(context): + uitests.vscode.testing.wait_for_stop_icon(context) + + +@behave.then("the stop icon is not visible in the toolbar") +@uitests.tools.retry(AssertionError) +def stop_icon_not_visible(context): + uitests.vscode.testing.wait_for_stop_hidden(context) + + +@behave.then("the run failed tests icon is visible in the toolbar") +@uitests.tools.retry(AssertionError) +def fun_failed_icon_visible(context): + uitests.vscode.testing.wait_for_run_failed_icon(context) + + +@behave.then("the run failed tests icon is not visible in the toolbar") +@uitests.tools.retry(AssertionError) +def fun_failed_icon_not_visible(context): + uitests.vscode.testing.wait_for_run_failed_hidden(context) + + +@behave.when("I wait for tests to complete running") +@uitests.tools.retry(AssertionError) +def wait_for_run_to_complete(context): + uitests.vscode.testing.wait_for_stop_hidden(context) + + +@behave.when("I wait for tests discovery to complete") +@uitests.tools.retry(AssertionError) +def wait_for_discovery_to_complete(context): + uitests.vscode.testing.wait_for_stop_hidden(context) + + +@behave.when("I stop discovering tests") +def when_stop_discovering(context): + uitests.vscode.testing.stop(context) + + +@behave.when("I run failed tests") +def when_run_failed_tests(context): + uitests.vscode.testing.run_failed_tests(context) + + +@behave.when("I stop running tests") +def when_stop_running(context): + uitests.vscode.testing.stop(context) + + +@behave.then("stop discovering tests") +def then_stop_discovering(context): + uitests.vscode.testing.stop(context) + + +def check_node_count_status(context, number, status): + icon_name = node_status_icon_mapping.get(status.upper(), "") + icons = uitests.vscode.testing.get_node_icons(context) + assert ( + len(list(icon for icon in icons if icon_name in icon.get_attribute("style"))) + == number + ) diff --git a/uitests/uitests/tools.py b/uitests/uitests/tools.py new file mode 100644 index 000000000000..54fee4bcb44c --- /dev/null +++ b/uitests/uitests/tools.py @@ -0,0 +1,235 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import logging +import os +import os.path +import shutil +import subprocess +import sys +import time +from functools import wraps + +import progress.bar +import requests + + +def retry(exceptions, tries=100, delay=0.1, backoff=1): + """ + Retry calling the decorated function using an exponential backoff. + Original source from https://www.calazan.com/retry-decorator-for-python-3/ + Args: + exceptions: The exception to check. may be a tuple of + exceptions to check. + tries: Number of times to try (not retry) before giving up. + delay: Initial delay between retries in seconds. + backoff: Backoff multiplier (e.g. value of 2 will double the delay + each retry). + """ + + def deco_retry(f): + @wraps(f) + def f_retry(*args, **kwargs): + mtries, mdelay = tries, delay + while mtries > 1: + try: + return f(*args, **kwargs) + except exceptions: + # except exceptions as e: + # msg = "{}, Retrying in {} seconds...".format(e, mdelay) + # logging.info(msg) + time.sleep(mdelay) + mtries -= 1 + mdelay *= backoff + return f(*args, **kwargs) + + return f_retry # true decorator + + return deco_retry + + +def run_command(command, *, cwd=None, silent=False, progress_message=None, env=None): + """Run the specified command in a subprocess shell with the following options: + - Pipe output from subprocess into current console. + - Display a progress message.""" + + if progress_message is not None: + logging.info(progress_message) + is_git = command[0] == "git" + shell = is_git + command[0] = shutil.which(command[0]) + out = subprocess.PIPE if silent else None + # Else Python throws crazy errors (socket (10106) The requested service provider could not be loaded or initialized.) + # The solution is to pass the complete list of variables. + if sys.platform.startswith("win"): + new_env = {} if env is None else env + env = os.environ.copy() + env.update(new_env) + if not is_git: + proc = subprocess.run( + command, cwd=cwd, shell=shell, env=env, stdout=out, stderr=out + ) + proc.check_returncode() + return + p = subprocess.Popen(command, cwd=cwd, stdout=out, stderr=out, shell=False) + _, err = p.communicate() + + if p.returncode != 0: + raise SystemError( + f"Exit code is not 0, {p.returncode} for command {command}, with an error: {err}" + ) + # return (p.returncode, out, err) + + # Note, we'll need some output to tell CI servers that process is still active. + # if progress_message: + # progress = Spinner(progress_message) + # while True: + # try: + # exit_code = proc.wait(1) + # except Exception: + # if progress: + # progress.next() + # continue + + # print(exit_code) + # if exit_code == 0: + # return + # if exit_code is not None: + # raise SystemError( + # "Command exited with a non-zero exit code," + command + # ) # noqa + + +def unzip_file(zip_file, destination, progress_message="Unzip"): + """Unzip a file.""" + + # For now now using zipfile module, + # as the unzippig didn't work for executables. + os.makedirs(destination, exist_ok=True) + dir = os.path.dirname(os.path.realpath(__file__)) + js_file = os.path.join(dir, "js", "unzip.js") + run_command( + ["node", js_file, zip_file, destination], progress_message=progress_message + ) + + +def download_file(url, download_file, progress_message="Downloading"): # noqa + """Download a file and optionally displays a progress indicator.""" + + try: + os.remove(download_file) + except FileNotFoundError: + pass + progress_bar = progress.bar.Bar(progress_message, max=100) + response = requests.get(url, stream=True) + total = response.headers.get("content-length") + + try: + with open(download_file, "wb") as fs: + if total is None: + fs.write(response.content) + else: + downloaded = 0 + total = int(total) + chunk_size = 1024 * 1024 + percent = 0 + for data in response.iter_content(chunk_size=chunk_size): + downloaded += len(data) + fs.write(data) + change_in_percent = (downloaded * 100 // total) - percent + percent = downloaded * 100 // total + for i in range(change_in_percent): + progress_bar.next() + except Exception: + os.remove(download_file) + raise + finally: + progress_bar.finish() + + +def empty_directory(dir): + # Ignore errors on windows. + for root, dirs, files in os.walk(dir): + for f in files: + try: + os.unlink(os.path.join(root, f)) + except Exception: + pass + for d in dirs: + try: + shutil.rmtree(os.path.join(root, d)) + except Exception: + pass + + +# def wait_for_python_env(cwd, path, timeout=30): +# start_time = time.time() +# python_exec = _get_python_executable(path) +# while time.time() - start_time < timeout: +# try: +# subprocess.run( +# [python_exec, "--version"], check=True, stdout=subprocess.PIPE, cwd=cwd +# ).stdout +# return +# except Exception as ex: +# print(ex) +# pass +# raise SystemError(f"Virtual Env not detected after {timeout}s") + + +@retry(Exception, tries=30) +def wait_for_python_env(cwd, path): + python_exec = _get_python_executable(path) + subprocess.run( + [python_exec, "--version"], check=True, stdout=subprocess.PIPE, cwd=cwd + ).stdout + + +# def wait_for_pipenv(cwd, timeout=30): +# start_time = time.time() +# while time.time() - start_time < timeout: +# try: +# subprocess.run( +# ["pipenv", "--py"], +# check=True, +# stdout=subprocess.DEVNULL, +# stderr=subprocess.DEVNULL, +# cwd=cwd, +# ) +# return +# except Exception as ex: +# print(ex) +# pass +# raise SystemError(f"PipEnv not detected after {timeout}s") + + +@retry(Exception, tries=30) +def wait_for_pipenv(cwd): + subprocess.run( + ["pipenv", "--py"], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + cwd=cwd, + ) + + +@retry(Exception, tries=30, delay=5) +def wait_for_conda_env(conda_path, env_name): + proc = subprocess.run( + [conda_path, "env", "list"], + check=False, + env=os.environ.copy(), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + output = proc.stdout.decode("utf-8") + proc.stderr.decode("utf-8") + assert env_name in output + + +def _get_python_executable(path): + if sys.platform.startswith("win"): + return os.path.join(path, "Scripts", "python.exe") + else: + return os.path.join(path, "bin", "python") diff --git a/uitests/uitests/vscode/__init__.py b/uitests/uitests/vscode/__init__.py new file mode 100644 index 000000000000..bed0001adc81 --- /dev/null +++ b/uitests/uitests/vscode/__init__.py @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from . import ( # noqa + application, + constants, + core, + documents, + download, + extension, + notifications, + output_panel, + quick_input, + quick_open, + startup, + settings, + status_bar, + testing, + debugger, +) diff --git a/uitests/uitests/vscode/application.py b/uitests/uitests/vscode/application.py new file mode 100644 index 000000000000..2f96902d434c --- /dev/null +++ b/uitests/uitests/vscode/application.py @@ -0,0 +1,260 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import base64 +import contextlib +import io +import os +import os.path +import shutil +import stat +import sys +import tempfile +import traceback +from dataclasses import dataclass + +from selenium import webdriver + +import uitests.bootstrap +import uitests.report +import uitests.tools + +# from . import quick_open + + +@dataclass +class Options: + executable_dir: str + user_dir: str + extensions_dir: str + extension_path: str + workspace_folder: str + temp_folder: str + screenshots_dir: str + embed_screenshots: bool + output: str + python_path: str + python_type: str + python_version: str + python3_path: str + pipenv_path: str + logfiles_dir: str + conda_path: str + + +def get_options( + destination=".vscode-test", + vsix="ms-python-insiders.vsix", + embed_screenshots=True, + output="file", + channel="stable", + python_path=sys.executable, + python_type=None, + python_version=f"{sys.version_info[0]}.{sys.version_info[1]}", + python3_path=sys.executable, + pipenv_path=None, + conda_path="conda", +): + """Gets the options used for smoke tests.""" + embed_screenshots = ( + embed_screenshots == "True" + if type(embed_screenshots) is str + else embed_screenshots + ) + + destination = os.path.abspath(destination) + options = Options( + os.path.join(destination, channel), + os.path.join(destination, "user"), + os.path.join(destination, "extensions"), + vsix, + os.path.join(destination, "workspace folder"), + os.path.join(destination, "temp"), + os.path.join(destination, "screenshots"), + embed_screenshots, + output, + python_path, + python_type, + python_version, + python3_path, + pipenv_path, + os.path.join(destination, "logs"), + conda_path, + ) + os.makedirs(options.extensions_dir, exist_ok=True) + os.makedirs(options.user_dir, exist_ok=True) + os.makedirs(options.workspace_folder, exist_ok=True) + os.makedirs(options.temp_folder, exist_ok=True) + os.makedirs(options.screenshots_dir, exist_ok=True) + os.makedirs(options.logfiles_dir, exist_ok=True) + return options + + +def setup_environment(options): + """Setup environment for smoke tests.""" + # Required for chrome driver. + os.environ["PATH"] += os.pathsep + options.executable_dir + # Ensure PTVSD logs are in the reports directory, + # This way they are available for analyzing. + os.environ["PTVSD_LOG_DIR"] = options.logfiles_dir + # Log extension stuff into vsc.log file. + os.environ["VSC_PYTHON_LOG_FILE"] = os.path.join(options.logfiles_dir, "vsc.log") + + +def uninstall_extension(options): + """Uninstalls extensions from smoke tests copy of VSC.""" + shutil.rmtree(options.extensions_dir, ignore_errors=True) + + +def install_extension(options): + """Installs extensions into smoke tests copy of VSC.""" + _set_permissions(options) + uninstall_extension(options) + bootstrap_extension = uitests.bootstrap.main.get_extension_path() + _install_extension(options.extensions_dir, "bootstrap", bootstrap_extension) + _install_extension( + options.extensions_dir, "pythonExtension", options.extension_path + ) + + +def _set_permissions(options): + # Set necessary permissions on Linux to be able to start. + # Else selenium throws errors. + # & so does VSC, when accessing vscode-ripgrep/bin/rg. + if sys.platform.startswith("linux"): + binary_location = _get_binary_location(options.executable_dir) + file_stat = os.stat(binary_location) + os.chmod(binary_location, file_stat.st_mode | stat.S_IEXEC) + + rg_path = os.path.join( + os.path.dirname(binary_location), + "resources", + "app", + "node_modules.asar.unpacked", + "vscode-ripgrep", + "bin", + "rg", + ) + file_stat = os.stat(rg_path) + os.chmod(rg_path, file_stat.st_mode | stat.S_IEXEC) + + +def _install_extension(extensions_dir, extension_name, vsix): + """Installs an extensions into smoke tests copy of VSC.""" + temp_dir = os.path.join(tempfile.gettempdir(), extension_name) + uitests.tools.unzip_file(vsix, temp_dir) + shutil.copytree( + os.path.join(temp_dir, "extension"), + os.path.join(extensions_dir, extension_name), + ) + shutil.rmtree(temp_dir, ignore_errors=True) + + +def launch_extension(options): + """Launches the smoke tests copy of VSC.""" + chrome_options = webdriver.ChromeOptions() + # Remember to remove the leading `--`. + # Chromedriver will add `--` for ALL arguments. + # I.e. arguments without a leading `--` are not supported. + for arg in [ + f"user-data-dir={options.user_dir}", + f"extensions-dir={options.extensions_dir}", + f"folder-uri=file:{options.workspace_folder}", + "skip-getting-started", + "skip-release-notes", + "sticky-quickopen", + "disable-telemetry", + "disable-updates", + "disable-crash-reporter", + ]: + chrome_options.add_argument(arg) + + chrome_options.binary_location = _get_binary_location(options.executable_dir) + driver = webdriver.Chrome(options=chrome_options) + return driver + + +def exit(context): + # Ignore all messages written to console. + with contextlib.redirect_stdout(io.StringIO()): + with contextlib.redirect_stderr(io.StringIO()): + # try: + # quick_open.select_command(context, "Close Window") + # except Exception: + # pass + try: + context.driver.close() + except Exception: + pass + try: + context.driver.quit() + except Exception: + pass + + +def capture_screen(context): + if context.options.output != "file": + return + + if context.options.embed_screenshots: + screenshot = context.driver.get_screenshot_as_base64() + uitests.report.PrettyCucumberJSONFormatter.instance.attach_image(screenshot) + else: + filename = tempfile.NamedTemporaryFile(prefix="screen_capture_") + filename = f"{os.path.basename(filename.name)}.png" + filename = os.path.join(context.options.screenshots_dir, filename) + context.driver.save_screenshot(filename) + html = f'Screen Shot' + html = base64.b64encode(html.encode("utf-8")).decode("utf-8") + + uitests.report.PrettyCucumberJSONFormatter.instance.attach_html(html) + + +def capture_exception(context, info=None): + if info is None or info.exc_traceback is None or info.exception is None: + return + + formatted_ex = traceback.format_exception( + type(info.exception), info.exception, info.exc_traceback + ) + html = f"Error

{formatted_ex}

" + html = base64.b64encode(html.encode("utf-8")).decode("utf-8") + + uitests.report.PrettyCucumberJSONFormatter.instance.attach_html(html) + + +def _get_binary_location(executable_directory): + if sys.platform.startswith("darwin"): + return os.path.join( + executable_directory, + "Visual Studio Code.app", + "Contents", + "MacOS", + "Electron", + ) + + if sys.platform.startswith("win"): + return os.path.join(executable_directory, "Code.exe") + + return os.path.join(executable_directory, "VSCode-linux-x64", "code") + + +def _get_cli_location(executable_directory): + if sys.platform.startswith("darwin"): + return os.path.join( + executable_directory, + "Visual Studio Code.app", + "Contents", + "Resources", + "app", + "out", + "cli.js", + ) + + if sys.platform.startswith("win"): + return os.path.join(executable_directory, "resources", "app", "out", "cli.js") + + return os.path.join( + executable_directory, "VSCode-linux-x64", "resources", "app", "out", "cli.js" + ) diff --git a/uitests/uitests/vscode/code_lenses.py b/uitests/uitests/vscode/code_lenses.py new file mode 100644 index 000000000000..51e4956d4425 --- /dev/null +++ b/uitests/uitests/vscode/code_lenses.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +from . import core + + +def get_code_lenses(context, **kwargs): + selector = ".editor-container .monaco-editor .lines-content .codelens-decoration a" + + return core.wait_for_elements(context.driver, selector, **kwargs) diff --git a/uitests/uitests/vscode/constants.py b/uitests/uitests/vscode/constants.py new file mode 100644 index 000000000000..dbcadcf0eee8 --- /dev/null +++ b/uitests/uitests/vscode/constants.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +PYTHON_STATUS_BAR_PRIORITY = 100 +PYTHON_STATUS_BAR_PREFIX = "Python" +MAC_INTERPRETER_SELECTED_AND_HAVE_OTEHR_INTERPRETER_MESSAGE = "You have selected the macOS system install of Python, which is not recommended for use with the Python extension. Some functionality will be limited, please select a different interpreter." diff --git a/uitests/uitests/vscode/core.py b/uitests/uitests/vscode/core.py new file mode 100644 index 000000000000..3f2d7d934a3d --- /dev/null +++ b/uitests/uitests/vscode/core.py @@ -0,0 +1,124 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import time + +from selenium.common import exceptions + + +class ElementVisibleException(exceptions.InvalidElementStateException): + """ + Thrown when an element is present on the DOM, + it is not visible when it should not be. + """ + + pass + + +def _try_and_find( + fn, + timeout_messge="Timeout", + retry_count=100, + retry_interval=100, + timeout=None, + **kwargs, +): + """Try and find a DOM element in VSC based on a predicate within a given time period.""" + if timeout is not None: + retry_count = (timeout * 1000) / retry_interval + else: + timeout = retry_count * retry_interval / 1000 + + trial_counter = 1 + start = time.time() + while trial_counter < retry_count: + if time.time() - start > timeout: + trial_counter = retry_count + 1 + try: + return fn.__call__() + except ( + exceptions.NoSuchElementException, + exceptions.StaleElementReferenceException, + ElementVisibleException, + ): + trial_counter += 1 + time.sleep(retry_interval / 1000) + else: + msg = f"Timeout: {timeout_messge} after {(retry_count * retry_interval) / 1000} seconds." + raise TimeoutError(msg) + + +def dispatch_keys(driver, keys, element=None): + """Sends key stokes to a DOM element.""" + element = driver.switch_to.active_element if element is None else element + element.send_keys(keys) + + +def wait_for_element(driver, css_selector, predicate=lambda ele: True, **kwargs): + """Wait till a DOM element in VSC is found.""" + + def find(): + element = driver.find_element_by_css_selector(css_selector) + if not element.is_displayed(): + raise exceptions.NoSuchElementException( + "Element not yet visible, so lets wait again" + ) + if element is None: + raise exceptions.NoSuchElementException( + "Predicate returned False in wait_for_element" + ) + return element + + return _try_and_find(find, **kwargs) + + +def wait_for_element_to_be_hidden(driver, css_selector, **kwargs): + """Wait till a DOM element in VSC is found.""" + + def find(): + try: + element = driver.find_element_by_css_selector(css_selector) + except exceptions.NoSuchElementException: + return + if not element.is_displayed(): + return + raise ElementVisibleException("Element is visible when it should not be") + + return _try_and_find(find, **kwargs) + + +def wait_for_elements( + driver, css_selector, predicate=lambda elements: elements, **kwargs +): + """Wait till DOM elements in VSC is found.""" + + def find(): + elements = driver.find_elements_by_css_selector(css_selector) + filtered = predicate(elements) + if filtered: + # Ensure all items returned are visible. + for element in filtered: + if not element.is_displayed(): + raise exceptions.NoSuchElementException( + "Element not yet visible, so lets wait again" + ) + + return filtered + raise exceptions.NoSuchElementException( + "Predicate returned False in wait_for_elements" + ) + + return _try_and_find(find, **kwargs) + + +def wait_for_active_element(driver, css_selector, **kwargs): + def is_active(): + element = driver.find_element_by_css_selector(css_selector) + assert element == driver.switch_to.active_element + if not element.is_displayed(): + raise exceptions.NoSuchElementException( + "Element not yet visible, so lets wait again" + ) + + return _try_and_find(is_active, **kwargs) diff --git a/uitests/uitests/vscode/debugger.py b/uitests/uitests/vscode/debugger.py new file mode 100644 index 000000000000..8e42e2704367 --- /dev/null +++ b/uitests/uitests/vscode/debugger.py @@ -0,0 +1,37 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os.path + +import uitests.vscode.core + + +def wait_for_debugger_to_start(context): + uitests.vscode.core.wait_for_element(context.driver, "div.debug-toolbar") + + +def wait_for_debugger_to_pause(context): + find = lambda ele: "Continue" in ele.get_attribute("title") + uitests.vscode.core.wait_for_element( + context.driver, "div.debug-toolbar .action-item .action-label.icon", find + ) + + +def wait_for_debugger_to_stop(context): + uitests.vscode.core.wait_for_element_to_be_hidden( + context.driver, "div.debug-toolbar" + ) + + +def add_breakpoint(context, file_name, line): + uitests.vscode.documents.open_file(context, file_name) + uitests.vscode.documents.go_to_line(context, line) + uitests.vscode.quick_open.select_command(context, "Debug: Toggle Breakpoint") + + +# def get_current_frame_position(context): +# selector = ".panel-body.debug-call-stack .monaco-list-row.selected" +# stack_trace = uitests.vscode.core.wait_for_element(context.driver, selector) +# file_name = stack_trace.find_element_by_css_selector(".file-name").text +# position = stack_trace.find_element_by_css_selector(".line-number").text.split(":") +# return file_name, int(position[0]), int(position[1]) diff --git a/uitests/uitests/vscode/documents.py b/uitests/uitests/vscode/documents.py new file mode 100644 index 000000000000..4b68d7769a4f --- /dev/null +++ b/uitests/uitests/vscode/documents.py @@ -0,0 +1,76 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import re +from urllib.parse import quote + +import uitests.tools +import uitests.vscode.application +import uitests.vscode.core +import uitests.vscode.quick_open +import uitests.vscode.startup + +from . import core, quick_input, quick_open + +LINE_COLUMN_REGEX = re.compile("Ln (?P\d+), Col (?P\d+)") + + +@uitests.tools.retry(TimeoutError, tries=5) +def open_file(context, filename): + uitests.vscode.application.capture_screen(context) + quick_open.select_command(context, "Go to File...") + uitests.vscode.application.capture_screen(context) + quick_open.select_value(context, filename) + uitests.vscode.application.capture_screen(context) + _wait_for_editor_focus(context, filename) + + +def is_file_open(context, filename, **kwargs): + _wait_for_active_tab(context, filename, **kwargs) + _wait_for_editor_focus(context, filename) + + +def create_new_untitled_file(context, language="Python"): + quick_open.select_command(context, "File: New Untitled File") + _wait_for_editor_focus(context, "Untitled-1") + quick_open.select_command(context, "Change Language Mode") + quick_input.select_value(context, language) + + +def scroll_to_top(context): + go_to_line(context, 1) + + +def go_to_line(context, line_number): + quick_open.select_command(context, "Go to Line...") + quick_open.select_value(context, str(line_number)) + + +def get_current_position(context): + selector = "a.editor-status-selection" + element = core.wait_for_element(context.driver, selector) + match = LINE_COLUMN_REGEX.match(element.text) + if match is None: + raise ValueError(f"Unable to detemrine line & column") + return int(match.group("line")), int(match.group("col")) + + +def _wait_for_active_tab(context, filename, is_dirty=False): + """Wait till a tab is active with the given file name.""" + dirty_class = ".dirty" if is_dirty else "" + selector = f'.tabs-container div.tab.active{dirty_class}[aria-selected="true"][aria-label="{filename}, tab"]' + core.wait_for_element(context.driver, selector) + + +def _wait_for_active_editor(context, filename, is_dirty=False): + """Wait till an editor with the given file name is active.""" + selector = ( + f'.editor-instance .monaco-editor[data-uri$="{quote(filename)}"] textarea' + ) + core.wait_for_element(context.driver, selector) + + +def _wait_for_editor_focus(context, filename, is_dirty=False, **kwargs): + """Wait till an editor with the given file name receives focus.""" + _wait_for_active_tab(context, filename, is_dirty, **kwargs) + _wait_for_active_editor(context, filename, is_dirty, **kwargs) diff --git a/uitests/uitests/vscode/download.py b/uitests/uitests/vscode/download.py new file mode 100644 index 000000000000..7067a1522b82 --- /dev/null +++ b/uitests/uitests/vscode/download.py @@ -0,0 +1,91 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import os +import os.path +import re +import shutil +import sys +import tempfile + +import requests + +import uitests.tools + + +def _get_download_platform(): + platform = sys.platform + if platform.startswith('linux'): + return "linux-x64" + if platform.startswith('darwin'): + return "darwin" + if platform.startswith('win'): + return "win32-archive" + + +def _get_latest_version(channel="stable"): + """Get the latest version of VS Code + The channel defines the channel for VSC (stable or insiders).""" + + download_platform = _get_download_platform() + url = f"https://update.code.visualstudio.com/api/releases/{channel}/{download_platform}" # noqa + versions = requests.get(url) + return versions.json()[0] + + +def _get_download_url( + version, download_platform, channel="stable" +) -> str: + """Get the download url for vs code.""" + return f"https://vscode-update.azurewebsites.net/{version}/{download_platform}/{channel}" # noqa + + +def _get_electron_version(channel="stable"): + if channel == "stable": + version = _get_latest_version() + # Assume that VSC tags based on major and minor numbers. + # E.g. 1.32 and not 1.32.1 + version_parts = version.split(".") + tag = f"{version_parts[0]}.{version_parts[1]}" + url = ( + f"https://raw.githubusercontent.com/Microsoft/vscode/release/{tag}/.yarnrc" # noqa + ) + else: + url = "https://raw.githubusercontent.com/Microsoft/vscode/master/.yarnrc" # noqa + + response = requests.get(url) + matches = re.finditer(r'target\s"(\d+.\d+.\d+)"', response.text, re.MULTILINE) + for _, match in enumerate(matches, start=1): + return match.groups()[0] + + +def download_chrome_driver(download_path, channel="stable"): + """Download chrome driver corresponding to the version of electron. + Basically check version of chrome released with the version of Electron.""" + + os.makedirs(download_path, exist_ok=True) + electron_version = _get_electron_version(channel) + dir = os.path.dirname(os.path.realpath(__file__)) + js_file = os.path.join(dir, "..", "js", "chromeDownloader.js") + # Use an exising npm package. + uitests.tools.run_command( + ["node", js_file, electron_version, download_path], + progress_message="Downloading chrome driver", + ) + + +def download_vscode(download_path, channel="stable"): + """Download VS Code.""" + + shutil.rmtree(download_path, ignore_errors=True) + os.makedirs(download_path, exist_ok=True) + + download_platform = _get_download_platform() + version = _get_latest_version(channel) + url = _get_download_url(version, download_platform, channel) + + file_name = "vscode.tar.gz" if sys.platform.startswith("linux") else "vscode.zip" + zip_file = os.path.join(tempfile.mkdtemp(), file_name) + uitests.tools.download_file(url, zip_file, f"Downloading VS Code {channel}") + uitests.tools.unzip_file(zip_file, download_path) diff --git a/uitests/uitests/vscode/extension.py b/uitests/uitests/vscode/extension.py new file mode 100644 index 000000000000..b5c735037809 --- /dev/null +++ b/uitests/uitests/vscode/extension.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +from . import notifications, quick_open, status_bar + + +def activate_python_extension(context): + last_error = None + for _ in range(5): + quick_open.select_command(context, "Activate Python Extension") + try: + # Sometimes it takes a while, specially on Windows. + notifications.wait_for_message(context, "Python Extension Activated", timeout=30) + break + except Exception as ex: + last_error = ex + continue + else: + raise SystemError("Failed to activate extension") from last_error + status_bar.wait_for_python_statusbar(context) + notifications.clear(context) diff --git a/uitests/uitests/vscode/notifications.py b/uitests/uitests/vscode/notifications.py new file mode 100644 index 000000000000..b04efe8ef8e4 --- /dev/null +++ b/uitests/uitests/vscode/notifications.py @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +from . import core, quick_open + + +def clear(context, **kwargs): + quick_open.select_command(context, "Notifications: Clear All Notifications") + + +def wait_for_message(context, value, **kwargs): + selector = ".notifications-toasts.visible .notifications-list-container .notification-list-item-message" + + def find(elements): + return [element for element in elements if element.text == value] + + return core.wait_for_elements(context.driver, selector, find, **kwargs) + + +def wait_for_message_containing(context, value, **kwargs): + selector = ".notifications-toasts.visible .notifications-list-container .notification-list-item-message" + + def find(elements): + return [element for element in elements if value in element.text] + + return core.wait_for_elements(context.driver, selector, find, **kwargs) diff --git a/uitests/uitests/vscode/output_panel.py b/uitests/uitests/vscode/output_panel.py new file mode 100644 index 000000000000..ae9086031097 --- /dev/null +++ b/uitests/uitests/vscode/output_panel.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +from selenium.common.exceptions import StaleElementReferenceException + +import uitests.tools + +from . import core + + +# The ui can get updated, hence retry at least 10 times. +@uitests.tools.retry(StaleElementReferenceException) +def get_output_panel_lines(context, **kwargs): + selector = ".part.panel.bottom .view-lines .view-line span span" + elements = core.wait_for_elements(context.driver, selector, **kwargs) + return [element.text for element in elements] diff --git a/uitests/uitests/vscode/problems.py b/uitests/uitests/vscode/problems.py new file mode 100644 index 000000000000..030df594b858 --- /dev/null +++ b/uitests/uitests/vscode/problems.py @@ -0,0 +1,40 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +from enum import Enum + +from . import core, quick_open + + +class ProblemType(Enum): + All = 0 + Error = 1 + Warning = 2 + Info = 3 + + +def get_problem_count(context, problem_type=ProblemType.All, **kwargs): + if problem_type == ProblemType.All: + selector = ".part.panel.bottom .action-item.checked .badge-content" + element = core.wait_for_element(context.driver, selector, **kwargs) + if element.text == "": + return 0 + else: + return int(element.text) + + if problem_type == ProblemType.Errors: + selector = ".part.panel.bottom .content .tree-container .monaco-tl-row .marker-icon.error" + elif problem_type == ProblemType.Warning: + selector = ".part.panel.bottom .content .tree-container .monaco-tl-row .marker-icon.warning" + elif problem_type == ProblemType.Info: + selector = ".part.panel.bottom .content .tree-container .monaco-tl-row .marker-icon.info" + + return len(core.wait_for_elements(context.driver, selector, **kwargs)) + + +def get_problem_files(context, **kwargs): + selector = ".part.panel.bottom .content .tree-container .monaco-tl-row .file-icon .label-name span span" + + elements = core.wait_for_elements(context.driver, selector, **kwargs) + return [element.text for element in elements] diff --git a/uitests/uitests/vscode/quick_input.py b/uitests/uitests/vscode/quick_input.py new file mode 100644 index 000000000000..e18bb44aaa8c --- /dev/null +++ b/uitests/uitests/vscode/quick_input.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +from selenium.webdriver.common.keys import Keys + +from . import core + +QUICK_OPEN_GENERIC = f".quick-input-widget" +QUICK_OPEN_GENERIC_INPUT = f"{QUICK_OPEN_GENERIC} .quick-input-box input" + + +def select_value(context, value, **kwargs): + element = _wait_until_opened(context, 5, is_command=False) + core.dispatch_keys(context.driver, value, element=element) + core.dispatch_keys(context.driver, Keys.ENTER, element=element) + + +def _wait_until_opened(context, retry_count=10, is_command=True): + return core.wait_for_element( + context.driver, QUICK_OPEN_GENERIC_INPUT, retry_count=retry_count + ) diff --git a/uitests/uitests/vscode/quick_open.py b/uitests/uitests/vscode/quick_open.py new file mode 100644 index 000000000000..d6a3fb53f0b5 --- /dev/null +++ b/uitests/uitests/vscode/quick_open.py @@ -0,0 +1,122 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import time + +import uitests.vscode.application + +from selenium.webdriver.common.keys import Keys +from selenium.common import exceptions + +from . import core + +QUICK_OPEN = "div.monaco-quick-open-widget" +QUICK_OPEN_HIDDEN = 'div.monaco-quick-open-widget[aria-hidden="true"]' +QUICK_OPEN_INPUT = f"{QUICK_OPEN} .quick-open-input input" +QUICK_OPEN_FOCUSED_ELEMENT = ( + f"{QUICK_OPEN} .quick-open-tree .monaco-tree-row.focused .monaco-highlighted-label" +) +QUICK_OPEN_ENTRY_SELECTOR = 'div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties .monaco-tree-row .quick-open-entry' # noqa +QUICK_OPEN_ENTRY_LABEL_SELECTOR = 'div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties .monaco-tree-row .quick-open-entry .label-name' # noqa +QUICK_OPEN_ENTRY_LABEL_SELECTOR_FOCUSED = 'div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties .monaco-tree-row.focused .quick-open-entry .label-name' # noqa + + +def select_command(context, command, **kwargs): + if command == "Debug: Continue": + # When debugging, add a delay of 0.5s before continuing. + time.sleep(0.5) + element = _open(context, command, **kwargs) + core.dispatch_keys(context.driver, Keys.ENTER, element=element) + if command == "View: Close All Editors": + # When closing, wait for all editors to close. + are_files_closed(context) + + +def are_files_closed(context): + start_time = time.time() + while time.time() - start_time < 5: + try: + context.driver.find_element_by_css_selector( + "div[id='workbench.parts.editor'] .title.tabs .tab" + ) + time.sleep(0.1) + except exceptions.NoSuchElementException: + return + else: + raise TimeoutError("Timeout waiting for documents to close") + + +def select_value(context, value, retry_count=5, **kwargs): + element = _wait_until_opened(context, retry_count, **kwargs) + # uitests.vscode.application.capture_screen(context) + # if value.startswith("run_in"): + # import time + + # time.sleep(1) + # core.dispatch_keys(context.driver, "run", element=element) + # time.sleep(1) + # uitests.vscode.application.capture_screen(context) + # core.dispatch_keys(context.driver, "_in_terminal", element=element) + # uitests.vscode.application.capture_screen(context) + # else: + # core.dispatch_keys(context.driver, value, element=element) + core.dispatch_keys(context.driver, value, element=element) + uitests.vscode.application.capture_screen(context) + core.dispatch_keys(context.driver, Keys.ENTER, element=element) + + +def wait_until_selected(context, value, **kwargs): + def find(eles): + try: + if eles[0].text == value: + return [eles[0]] + if any([ele for ele in eles if ele.text == value]): + # Check if the item that matches exactly is highlighted, + # If it is, then select that and return it + highlighted_element = core.wait_for_element( + context.driver, QUICK_OPEN_ENTRY_LABEL_SELECTOR_FOCUSED + ) + if highlighted_element.text == value: + return [highlighted_element] + return [] + + return [eles[0]] if eles[0].text == value else [] + except Exception: + return [] + + return core.wait_for_elements( + context.driver, QUICK_OPEN_ENTRY_LABEL_SELECTOR, find, **kwargs + ) + + +def _open(context, value, **kwargs): + retry = kwargs.get("retry", 5) + timeout = kwargs.get("timeout", 5) + # This is a hack, we cannot send key strokes to the electron app using selenium. + # So, lets bring up the `Go to line` input window + # then type in the character '>' to turn it into a quick input window 😊 + last_ex = None + for _ in range(retry, -1, -1): + element = core.wait_for_element( + context.driver, + ".part.statusbar .statusbar-item.left.statusbar-entry a[title='PySmoke']", + timeout=timeout, + ) + element.click() + try: + element = _wait_until_opened(context, 10) + core.dispatch_keys(context.driver, f"> {value}", element=element) + wait_until_selected(context, value, timeout=timeout) + return element + except Exception as ex: + last_ex = ex + continue + else: + raise SystemError("Failed to open quick open") from last_ex + + +def _wait_until_opened(context, retry_count=10, is_command=True, **kwargs): + return core.wait_for_element( + context.driver, QUICK_OPEN_INPUT, retry_count=retry_count + ) diff --git a/uitests/uitests/vscode/settings.py b/uitests/uitests/vscode/settings.py new file mode 100644 index 000000000000..018a638c0c08 --- /dev/null +++ b/uitests/uitests/vscode/settings.py @@ -0,0 +1,71 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import json +import os +import pathlib + +import uitests.tools + + +def update_workspace_settings(context, settings={}): + workspace_folder = context.options.workspace_folder + settings_json = os.path.join(workspace_folder, ".vscode", "settings.json") + update_settings(settings_json, settings) + + +def remove_workspace_setting(context, setting): + settings_json = os.path.join( + context.options.workspace_folder, ".vscode", "settings.json" + ) + remove_setting(settings_json, setting) + + +def remove_user_setting(context, setting): + settings_json = os.path.join(context.options.user_dir, "User", "settings.json") + remove_setting(settings_json, setting) + + +# For some reason this throws an error on Widows. +@uitests.tools.retry(AssertionError) +def _ensure_setttings_json(settings_json): + os.makedirs(pathlib.Path(settings_json).parent, exist_ok=True) + if os.path.exists(settings_json): + return + with open(settings_json, "w") as file: + file.write("{}") + + +def update_settings(settings_json, settings={}): + _ensure_setttings_json(settings_json) + existing_settings = {} + with open(settings_json, "r") as file: + existing_settings = json.loads(file.read()) + + with open(settings_json, "w") as file: + existing_settings.update(settings) + json.dump(existing_settings, file, indent=4) + + +def remove_setting(settings_json, setting): + _ensure_setttings_json(settings_json) + existing_settings = {} + with open(settings_json, "r") as file: + existing_settings = json.loads(file.read()) + + if setting not in existing_settings: + return + del existing_settings[setting] + + with open(settings_json, "w") as file: + json.dump(existing_settings, file, indent=4) + + +def get_setting(settings_json, setting): + _ensure_setttings_json(settings_json) + existing_settings = {} + with open(settings_json, "r") as file: + existing_settings = json.loads(file.read()) + + return existing_settings.get(setting) diff --git a/uitests/uitests/vscode/startup.py b/uitests/uitests/vscode/startup.py new file mode 100644 index 000000000000..15deef952b13 --- /dev/null +++ b/uitests/uitests/vscode/startup.py @@ -0,0 +1,189 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import contextlib +import io +import logging +import os +import os.path +import tempfile +import time +import sys +from dataclasses import dataclass + +from selenium import webdriver + +import uitests.tools + +from . import application, extension, quick_open, settings + +CONTEXT = {"driver": None} + + +@dataclass +class Context: + options: application.Options + driver: webdriver.Chrome + + +def start(options): + logging.debug("Starting VS Code") + uitests.tools.empty_directory(options.workspace_folder) + user_settings = { + "python.pythonPath": options.python_path, + "python.venvFolders": ["envs", ".pyenv", ".direnv", ".local/share/virtualenvs"], + } + setup_user_settings(options.user_dir, user_settings=user_settings) + return launch(options) + + +def launch(options): + app_context = _start_vscode(options) + CONTEXT["driver"] = app_context.driver + extension.activate_python_extension(app_context) + return app_context + + +def _start_vscode(options): + application.setup_environment(options) + driver = application.launch_extension(options) + context = Context(options, driver) + # Wait for VSC to startup. + time.sleep(2) + return context + + +def reload(context): + logging.debug("Reloading VS Code") + # Ignore all messages written to console. + with contextlib.redirect_stdout(io.StringIO()): + with contextlib.redirect_stderr(io.StringIO()): + application.exit(context) + app_context = launch(context.options) + context.driver = app_context.driver + CONTEXT["driver"] = context.driver + # clear_everything(app_context) + return app_context + + +def clear_everything(context): + quick_open.select_command(context, "View: Revert and Close Editor") + quick_open.select_command(context, "Terminal: Kill the Active Terminal Instance") + quick_open.select_command(context, "Debug: Remove All Breakpoints") + quick_open.select_command(context, "View: Close All Editors") + quick_open.select_command(context, "View: Close Panel") + quick_open.select_command(context, "Notifications: Clear All Notifications") + + +def reset_workspace(context): + if sys.platform.startswith("win"): + # On windows, create a new folder everytime. + # Deleting/reverting changes doesn't work too well. + # We get a number of access denied errors (files are in use). + setup_workspace(context, getattr(context, "workspace_repo", None)) + workspace_folder = context.options.workspace_folder + else: + # On non-widows, just revert the changes made using git (easy). + workspace_folder = context.options.workspace_folder + if getattr(context, "workspace_repo", None) is None: + uitests.tools.empty_directory(workspace_folder) + else: + logging.debug(f"Resetting workspace folder") + uitests.tools.run_command( + ["git", "reset", "--hard"], cwd=workspace_folder, silent=True + ) + uitests.tools.run_command( + ["git", "clean", "-fd"], cwd=workspace_folder, silent=True + ) + + settings_json = os.path.join(workspace_folder, ".vscode", "settings.json") + settings.update_settings(settings_json) + + +def setup_workspace(context, source_repo=None): + """ + Set the workspace for a feature/scenario. + source_repo is either the github url of the repo to be used as the workspace folder. + Or it is None. + """ + context.workspace_repo = source_repo + if source_repo is None: + if sys.platform.startswith("win"): + try: + uitests.tools.empty_directory(context.options.temp_folder) + except (PermissionError, FileNotFoundError, OSError): + pass + # On windows, create a new folder everytime. + # Deleting/reverting changes doesn't work too well. + # We get a number of access denied errors (files are in use). + workspace_folder_name = os.path.basename( + tempfile.NamedTemporaryFile(prefix="workspace folder ").name + ) + context.options.workspace_folder = os.path.join( + context.options.temp_folder, workspace_folder_name + ) + os.makedirs(context.options.workspace_folder, exist_ok=True) + settings_json = os.path.join( + context.options.workspace_folder, ".vscode", "settings.json" + ) + settings.update_settings(settings_json) + else: + uitests.tools.empty_directory(context.options.workspace_folder) + return + + logging.debug(f"Setting up workspace folder from {source_repo}") + + if sys.platform.startswith("win"): + # On windows, create a new folder everytime. + # Deleting/reverting changes doesn't work too well. + # We get a number of access denied errors (files are in use). + try: + uitests.tools.empty_directory(context.options.temp_folder) + except (PermissionError, FileNotFoundError, OSError): + pass + workspace_folder_name = os.path.basename( + tempfile.NamedTemporaryFile(prefix="workspace folder ").name + ) + context.options.workspace_folder = os.path.join( + context.options.temp_folder, workspace_folder_name + ) + os.makedirs(context.options.workspace_folder, exist_ok=True) + + # If on non-windows, just delete the files in current workspace. + uitests.tools.empty_directory(context.options.workspace_folder) + target = context.options.workspace_folder + repo_url = _get_repo_url(source_repo) + uitests.tools.run_command(["git", "clone", repo_url, "."], cwd=target, silent=True) + + # Its possible source_repo is https://github.com/Microsoft/vscode-python/tree/master/build + # Meaning, we want to glon https://github.com/Microsoft/vscode-python + # and want the workspace folder to be tree/master/build when cloned. + if len(source_repo) > len(repo_url): + # Exclude trailing `.git` and take everthying after. + sub_directory = source_repo[len(repo_url[:-4]) + 1 :] + context.options.workspace_folder = os.path.join( + context.options.workspace_folder, os.path.sep.join(sub_directory.split("/")) + ) + + settings_json = os.path.join(target, ".vscode", "settings.json") + settings.update_settings(settings_json) + + +def _get_repo_url(source_repo): + """Will return the repo url ignoring any sub directories.""" + + repo_parts = source_repo[len("https://github.com/") :].split("/") + repo_name = ( + repo_parts[1] if repo_parts[1].endswith(".git") else f"{repo_parts[1]}.git" + ) + return f"https://github.com/{repo_parts[0]}/{repo_name}" + + +def setup_user_settings(user_folder, **kwargs): + folder = os.path.join(user_folder, "User") + os.makedirs(folder, exist_ok=True) + settings_json = os.path.join(folder, "settings.json") + user_settings = kwargs.get("user_settings", None) + if user_settings is not None: + settings.update_settings(settings_json, user_settings) diff --git a/uitests/uitests/vscode/status_bar.py b/uitests/uitests/vscode/status_bar.py new file mode 100644 index 000000000000..9adaf7f0f52e --- /dev/null +++ b/uitests/uitests/vscode/status_bar.py @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +from typing import List + +from . import constants, core + +STATUS_BAR_SELECTOR = 'div[id="workbench.parts.statusbar"]' + + +def wait_for_item_with_tooltip(context, value): + selector = f'{STATUS_BAR_SELECTOR} span[title="${value}"]' + core.wait_for_element(context.driver, selector) + + +def wait_for_python_statusbar(context, parts: List[str] = []): + selector = f"div.statusbar-item.left.statusbar-entry[statusbar-entry-priority='{constants.PYTHON_STATUS_BAR_PRIORITY}'] a" # noqa + + def find(elements): + for element in elements: + if element.text.index(constants.PYTHON_STATUS_BAR_PREFIX) != 0: + continue + if not parts: + return [element] + text_parts = element.text.split(" ") + if all(map(text_parts.index, parts)): + return [element] + return [] + + return core.wait_for_elements(context.driver, selector, find)[0] diff --git a/uitests/uitests/vscode/testing.py b/uitests/uitests/vscode/testing.py new file mode 100644 index 000000000000..c39807a89d33 --- /dev/null +++ b/uitests/uitests/vscode/testing.py @@ -0,0 +1,172 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os.path +import time + +from selenium.common.exceptions import StaleElementReferenceException +from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.common.keys import Keys + +import uitests.tools +import uitests.vscode.core + + +def wait_for_explorer_icon(context): + selector = ".activitybar.left .actions-container a[title='Test']" + uitests.vscode.core.wait_for_element(context.driver, selector) + + +def wait_for_stop_icon(context): + selector = "div[id='workbench.parts.sidebar'] .action-item a[title='Stop']" + uitests.vscode.core.wait_for_element(context.driver, selector) + + +def wait_for_stop_hidden(context): + selector = "div[id='workbench.parts.sidebar'] .action-item a[title='Stop']" + uitests.vscode.core.wait_for_element_to_be_hidden(context.driver, selector) + + +def wait_for_run_failed_icon(context): + selector = ( + "div[id='workbench.parts.sidebar'] .action-item a[title='Run Failed Tests']" + ) + uitests.vscode.core.wait_for_element(context.driver, selector) + + +def wait_for_run_failed_hidden(context): + selector = ( + "div[id='workbench.parts.sidebar'] .action-item a[title='Run Failed Tests']" + ) + uitests.vscode.core.wait_for_element_to_be_hidden(context.driver, selector) + + +def stop(context): + selector = "div[id='workbench.parts.sidebar'] .action-item a[title='Stop']" + element = uitests.vscode.core.wait_for_element(context.driver, selector) + element.click() + + +def run_failed_tests(context): + selector = ( + "div[id='workbench.parts.sidebar'] .action-item a[title='Run Failed Tests']" + ) + element = uitests.vscode.core.wait_for_element(context.driver, selector) + element.click() + + +def get_node_count(context): + selector = "div[id='workbench.view.extension.test'] .monaco-tree-row" + return len(list(uitests.vscode.core.wait_for_elements(context.driver, selector))) + + +def get_node_icons(context): + selector = "div[id='workbench.view.extension.test'] .monaco-tree-row .custom-view-tree-node-item-icon" + return uitests.vscode.core.wait_for_elements(context.driver, selector) + + +def get_node_icon(context, number): + selector = f"div[id='workbench.view.extension.test'] .monaco-tree-row:nth-child({number}) .custom-view-tree-node-item-icon" + return uitests.vscode.core.wait_for_element(context.driver, selector) + + +def get_node(context, number): + selector = ( + f"div[id='workbench.view.extension.test'] .monaco-tree-row:nth-child({number})" + ) + return uitests.vscode.core.wait_for_elements(context.driver, selector) + + +def get_node_number(context, text): + # Node names can contain folder & file names delimited by path separator. + # Ensure we use the OS specific path separator. + text = text.replace("/", os.path.sep) + nodes = _get_node_labels(context) + return nodes.index(text) + 1 + + +@uitests.tools.retry(StaleElementReferenceException) +def _get_node_labels(context): + selector = "div[id='workbench.view.extension.test'] .monaco-tree-row .monaco-icon-label .label-name span span" + elements = context.driver.find_elements_by_css_selector(selector) + return [element.text for element in elements] + + +def _select_node(context, number): + tree = uitests.vscode.core.wait_for_element( + context.driver, ".monaco-tree.monaco-tree-instance-2" + ) + tree.click() + selector = ( + f"div[id='workbench.view.extension.test'] .monaco-tree-row:nth-child({number})" + ) + element = context.driver.find_element_by_css_selector(selector) + action = ActionChains(context.driver) + action.context_click(element) + action.perform() + find = lambda ele: "focused" in ele.get_attribute("class") + uitests.vscode.core.wait_for_element(context.driver, selector, find) + return element + + +def click_node(context, number): + element = _select_node(context, number) + element.click() + + +def click_node_action_item(context, number, tooltip): + expand_nodes(context) + _select_node(context, number) + action = _get_action_item(context, number, tooltip) + action.click() + + +def _get_action_item(context, number, tooltip): + selector = f"div[id='workbench.view.extension.test'] .monaco-tree-row:nth-child({number}) .actions .action-item a.action-label.icon[title='{tooltip}']" + return context.driver.find_element_by_css_selector(selector) + + +def expand_nodes(context): + time.sleep(0.1) + start_time = time.time() + while time.time() - start_time < 5: + _expand_nodes(context) + if get_node_count(context) > 1: + return + time.sleep(0.1) + else: + raise TimeoutError("Timeout waiting to expand all nodes") + + +def _expand_nodes(context): + tree = uitests.vscode.core.wait_for_element( + context.driver, ".monaco-tree.monaco-tree-instance-2" + ) + tree.click() + for i in range(1, 5000): + selector = ( + f"div[id='workbench.view.extension.test'] .monaco-tree-row:nth-child({i})" + ) + element = context.driver.find_element_by_css_selector(selector) + action = ActionChains(context.driver) + action.context_click(element) + action.perform() + find = lambda ele: "focused" in ele.get_attribute("class") + uitests.vscode.core.wait_for_element(context.driver, selector, find) + css_class = element.get_attribute("class") + + if "has-children" in css_class and "expanded" not in css_class: + tree.send_keys(Keys.RIGHT) + find = lambda ele: "expanded" in ele.get_attribute("class") + uitests.vscode.core.wait_for_element(context.driver, selector, find) + + try: + selector = f"div[id='workbench.view.extension.test'] .monaco-tree-row:nth-child({i+1})" + element = context.driver.find_element_by_css_selector(selector) + except Exception: + return + + +def get_root_node(context): + selector = "div[id='workbench.view.extension.test'] .monaco-tree-row:nth-child(1)" + return uitests.vscode.core.wait_for_element(context.driver, selector)