diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 15b32856030e..9d814df8c882 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -21,7 +21,7 @@ env: COVERAGE_REPORTS: tests-coverage-reports CI_PYTHON_PATH: python TEST_RESULTS_DIRECTORY: . - TEST_RESULTS_GLOB: '**/test-results.xml' + TEST_RESULTS_GLOB: '**/test-results*.xml' jobs: build-vsix: @@ -201,7 +201,7 @@ jobs: check_name: Ts-Unit Test Report tests: - name: Tests (with Python) + name: Functional Jupyter Tests runs-on: ${{ matrix.os }} if: github.repository == 'microsoft/vscode-jupyter' strategy: @@ -210,13 +210,8 @@ jobs: # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, # macOS runners are expensive, and we assume that Ubuntu is enough to cover the UNIX case. os: [ubuntu-latest] - python: [3.8] # Use flaky tests to run against more versions of Python. - # python-unit: Python tests - # functional: Tests with mocked VS Code, & mocked Python) - # functional-with-jupyter: Tests with mocked VS Code & real jupyter - # single-workspace: Tests with VS Code - # vscode: Tests with VS Code, Python extension & real Jupyter - test-suite: [python-unit, functional, test-based-on-pr-body] + python: [3.8] + test-suite: [group1, group2, group3, group4] steps: - name: Checkout uses: actions/checkout@v2 @@ -240,7 +235,7 @@ jobs: # Caching (https://github.com/actions/cache/blob/main/examples.md#python---pip - name: Cache pip on linux uses: actions/cache@v2 - if: startsWith(matrix.test-suite, 'functional') && matrix.os == 'ubuntu-latest' + if: matrix.os == 'ubuntu-latest' with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{env.PYTHON_VERSION}}-${{ hashFiles('requirements.txt') }}-${{hashFiles('build/debugger-install-requirements.txt')}}-${{hashFiles('test-requirements.txt')}}-${{hashFiles('ipython-test-requirements.txt')}}-${{hashFiles('functional-test-requirements.txt')}}-${{hashFiles('conda-functional-requirements.txt')}} @@ -249,7 +244,7 @@ jobs: - name: Cache pip on mac uses: actions/cache@v2 - if: startsWith(matrix.test-suite, 'functional') && matrix.os == 'macos-latest' + if: matrix.os == 'macos-latest' with: path: ~/Library/Caches/pip key: ${{ runner.os }}-pip-${{env.PYTHON_VERSION}}-${{ hashFiles('requirements.txt') }}-${{hashFiles('build/debugger-install-requirements.txt')}}-${{hashFiles('test-requirements.txt')}}-${{hashFiles('ipython-test-requirements.txt')}}-${{hashFiles('functional-test-requirements.txt')}}-${{hashFiles('conda-functional-requirements.txt')}} @@ -258,7 +253,7 @@ jobs: - name: Cache pip on windows uses: actions/cache@v2 - if: startsWith(matrix.test-suite, 'functional') && matrix.os == 'windows-latest' + if: matrix.os == 'windows-latest' with: path: ~\AppData\Local\pip\Cache key: ${{ runner.os }}-pip-${{env.PYTHON_VERSION}}-${{ hashFiles('requirements.txt') }}-${{hashFiles('build/debugger-install-requirements.txt')}}-${{hashFiles('test-requirements.txt')}}-${{hashFiles('ipython-test-requirements.txt')}}-${{hashFiles('functional-test-requirements.txt')}}-${{hashFiles('conda-functional-requirements.txt')}} @@ -299,12 +294,12 @@ jobs: # For faster/better builds of sdists. - run: python -m pip install wheel shell: bash - if: matrix.test-suite != 'test-based-on-pr-body' || contains(github.event.pull_request.body, '[x] Run ') # debugpy is not shipped, only installed for local tests. # In production, we get debugpy from python extension. - name: Install functional test requirements run: | + python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r ./requirements.txt python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt python ./pythonFiles/install_debugpy.py python -m pip install numpy @@ -314,56 +309,28 @@ jobs: python -m pip install --upgrade -r ./build/conda-functional-requirements.txt python -m ipykernel install --user # This step is slow. - # If running the placeholder suite & not required to run any tests, then don't install python dependencies. - if: matrix.test-suite != 'test-based-on-pr-body' || contains(github.event.pull_request.body, '[x] Run ') - name: Install dependencies (npm ci) run: npm ci --prefer-offline # This step is slow. - # If running the placeholder suite & not required to run any tests, then don't install python dependencies. - if: matrix.test-suite != 'test-based-on-pr-body' || contains(github.event.pull_request.body, '[x] Run ') - - # Run the Python and IPython tests in our codebase. - - name: Run Python and IPython unit tests - run: | - python pythonFiles/tests/run_all.py - python -m IPython pythonFiles/tests/run_all.py - if: matrix.test-suite == 'python-unit' - name: Compile if not cached run: npx gulp prePublishNonBundle - # If running the placeholder suite & not required to run any tests, then don't compile. - # if: steps.out-cache.outputs.cache-hit == false && (matrix.test-suite != 'test-based-on-pr-body' || contains(github.event.pull_request.body, '[x] Run ')) - if: matrix.test-suite != 'test-based-on-pr-body' || contains(github.event.pull_request.body, '[x] Run ') - name: Run functional tests - run: npm run test:functional - id: test_functional - if: matrix.test-suite == 'functional' - - - name: Run single-workspace tests - env: - CI_PYTHON_VERSION: ${{matrix.python}} - uses: GabrielBB/xvfb-action@v1.4 - with: - run: npm run testSingleWorkspace - if: matrix.test-suite == 'single-workspace' || (matrix.test-suite == 'test-based-on-pr-body' && contains(github.event.pull_request.body, '[x] Run single-workspace test')) - - - name: Run functional tests with Jupyter - run: npm run test:functional -- --grep="MimeTypes" - id: test_functional_jupyter + run: npm run test:functional:parallel -- --${{matrix.test-suite}} env: VSCODE_PYTHON_ROLLING: 1 VSC_PYTHON_FORCE_LOGGING: 1 - if: matrix.test-suite == 'functional-with-jupyter' || (matrix.test-suite == 'test-based-on-pr-body' && contains(github.event.pull_request.body, '[x] Run functional-with-jupyter test')) + id: test_functional_group - name: Publish Test Report uses: scacap/action-surefire-report@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} report_paths: ${{ env.TEST_RESULTS_GLOB }} - check_name: Functional Test Report - if: (steps.test_functional.outcome == 'failure' || steps.test_functional_jupyter.outcome == 'failure') && failure() + check_name: Functional Test Report ${{matrix.test-suite}} + if: steps.test_functional_group.outcome == 'failure' && failure() - name: Run DataScience tests with VSCode & Jupyter uses: GabrielBB/xvfb-action@v1.4 diff --git a/.vscode/launch.json b/.vscode/launch.json index 8c89946e9e47..5735deae446e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -296,7 +296,9 @@ // Some tests require multiple python interpreters (do not rely on discovery for functional tests, be explicit). "XCI_PYTHON_PATH2": "", // Remove 'X' prefix to dump output for debugger. Directory has to exist prior to launch - "XDEBUGPY_LOG_DIR": "${workspaceRoot}/tmp/Debug_Output" + "XDEBUGPY_LOG_DIR": "${workspaceRoot}/tmp/Debug_Output", + // Remove 'X' prefix to dump webview redux action log + "XVSC_PYTHON_WEBVIEW_LOG_FILE": "${workspaceRoot}/test-webview.log" }, "outFiles": [ "${workspaceFolder}/out/**/*.js", diff --git a/ThirdPartyNotices-Repository.txt b/ThirdPartyNotices-Repository.txt index 1162db48bc9c..13cfc9e6b7ae 100644 --- a/ThirdPartyNotices-Repository.txt +++ b/ThirdPartyNotices-Repository.txt @@ -20,6 +20,7 @@ Microsoft Python extension for Visual Studio Code incorporates third party mater 16. ipywidgets (https://github.com/jupyter-widgets) 17. vscode-cpptools (https://github.com/microsoft/vscode-cpptools) 18. font-awesome (https://github.com/FortAwesome/Font-Awesome) +19. mocha (https://github.com/mochajs/mocha) %% Go for Visual Studio Code NOTICES, INFORMATION, AND LICENSE BEGIN HERE @@ -1167,4 +1168,33 @@ trademarks does not indicate endorsement of the trademark holder by Font Awesome, nor vice versa. **Please do not use brand logos for any purpose except to represent the company, product, or service to which they refer.** ========================================= -END OF font-awesome NOTICES, INFORMATION, AND LICENSE \ No newline at end of file +END OF font-awesome NOTICES, INFORMATION, AND LICENSE + +%% mocha NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= + +(The MIT License) + +Copyright (c) 2011-2020 OpenJS Foundation and contributors, https://openjsf.org + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +========================================= +END OF mocha NOTICES, INFORMATION, AND LICENSE diff --git a/build/.mocha-multi-reporters.config b/build/.mocha-multi-reporters.config index 3afa21784dbf..33dd391270fc 100644 --- a/build/.mocha-multi-reporters.config +++ b/build/.mocha-multi-reporters.config @@ -1,5 +1,5 @@ { - "reporterEnabled": "spec,mocha-junit-reporter", + "reporterEnabled": "./build/ci/scripts/spec_with_pid,mocha-junit-reporter", "mochaJunitReporterReporterOptions": { "includePending": true } diff --git a/build/ci/scripts/runFunctionalTests.js b/build/ci/scripts/runFunctionalTests.js index 4171900586da..1f78007440f8 100644 --- a/build/ci/scripts/runFunctionalTests.js +++ b/build/ci/scripts/runFunctionalTests.js @@ -8,12 +8,85 @@ var path = require('path'); var glob = require('glob'); var child_process = require('child_process'); +var fs = require('fs-extra'); // Create a base for the output file var originalMochaFile = process.env['MOCHA_FILE']; var mochaFile = originalMochaFile || './test-results.xml'; var mochaBaseFile = path.join(path.dirname(mochaFile), path.basename(mochaFile, '.xml')); var mochaFileExt = '.xml'; +var groupCount = 4; + +function gatherArgs(extraArgs, file) { + return [ + file, + '--require=out/test/unittests.js', + '--exclude=out/**/*.jsx', + '--reporter=mocha-multi-reporters', + '--reporter-option=configFile=build/.mocha-multi-reporters.config', + '--ui=tdd', + '--recursive', + '--colors', + '--exit', + '--timeout=180000', + ...extraArgs + ]; +} + +async function generateGroups(files) { + // Go through each file putting it into a bucket. Each bucket will attempt to + // have equal size + + // Start with largest files first (sort by size) + var stats = await Promise.all(files.map((f) => fs.stat(f))); + var filesWithSize = files.map((f, i) => { + return { + file: f, + size: stats[i].size + }; + }); + var sorted = filesWithSize.sort((a, b) => b.size - a.size); + + // Generate buckets that try to hold the largest file first + var buckets = new Array(groupCount).fill().map((_, i) => { + return { + index: i, + totalSize: 0, + files: [] + }; + }); + var lowestBucket = buckets[0]; + sorted.forEach((fs) => { + buckets[lowestBucket.index].totalSize += fs.size; + buckets[lowestBucket.index].files.push(fs.file); + lowestBucket = buckets.find((b) => b.totalSize < lowestBucket.totalSize) || lowestBucket; + }); + + // Return these groups of files + return buckets.map((b) => b.files); +} + +async function runIndividualTest(extraArgs, file, index) { + var subMochaFile = `${mochaBaseFile}_${index}_${path.basename(file)}${mochaFileExt}`; + var args = gatherArgs(extraArgs, file); + console.log(`Running functional test for file ${file} ...`); + var exitCode = await new Promise((resolve) => { + // Spawn the sub node process + var proc = child_process.fork('./node_modules/mocha/bin/_mocha', args, { + env: { ...process.env, MOCHA_FILE: subMochaFile } + }); + proc.on('exit', resolve); + }); + + // If failed keep track + if (exitCode !== 0) { + console.log(`Functional tests for ${file} failed.`); + } else { + console.log(`Functional test for ${file} succeeded`); + } + + return exitCode; +} // Wrap async code in a function so can wait till done async function main() { @@ -21,7 +94,7 @@ async function main() { // Glob all of the files that we usually send to mocha as a group (see mocha.functional.opts.xml) var files = await new Promise((resolve, reject) => { - glob('./out/test/**/*.functional.test.js', (ex, res) => { + glob('./out/test/datascience/**/*.functional.test.js', (ex, res) => { if (ex) { reject(ex); } else { @@ -30,38 +103,42 @@ async function main() { }); }); + // Figure out what group is running (should be something like --group1, --group2 etc.) + var groupArgIndex = process.argv.findIndex((a) => a.includes('--group')); + var groupIndex = groupArgIndex >= 0 ? parseInt(process.argv[groupArgIndex].slice(7), 10) - 1 : -1; + + // Generate 4 groups based on sorting by size + var groups = await generateGroups(files); + files = groupIndex >= 0 ? groups[groupIndex] : files; + console.log(`Running for group ${groupIndex}`); + + // Extract any extra args for the individual mocha processes + var extraArgs = + groupIndex >= 0 && process.argv.length > 3 + ? process.argv.slice(3) + : process.argv.length > 2 + ? process.argv.slice(2) + : []; + // Iterate over them, running mocha on each var returnCode = 0; - // Go through each one at a time + // Start timing now (don't care about glob time) + var startTime = Date.now(); + + // Run all of the tests (in parallel or sync based on env) try { - for (var index = 0; index < files.length; index += 1) { - // Each run with a file will expect a $MOCHA_FILE$ variable. Generate one for each - // Note: this index is used as a pattern when setting mocha file in the test_phases.yml - var subMochaFile = `${mochaBaseFile}_${index}_${path.basename(files[index])}${mochaFileExt}`; - process.env['MOCHA_FILE'] = subMochaFile; - var exitCode = await new Promise((resolve) => { - // Spawn the sub node process - var proc = child_process.fork('./node_modules/mocha/bin/_mocha', [ - files[index], - '--require=out/test/unittests.js', - '--exclude=out/**/*.jsx', - '--reporter=mocha-multi-reporters', - '--reporter-option=configFile=build/.mocha-multi-reporters.config', - '--ui=tdd', - '--recursive', - '--colors', - '--exit', - '--timeout=180000' - ]); - proc.on('exit', resolve); - }); - - // If failed keep track - if (exitCode !== 0) { - console.log(`Functional tests for ${files[index]} failed.`); - returnCode = exitCode; + if (process.env.VSCODE_PYTHON_FORCE_TEST_SYNC) { + for (var i = 0; i < files.length; i += 1) { + // Synchronous, one at a time + returnCode = returnCode | (await runIndividualTest(extraArgs, files[i], i)); } + } else { + // Parallel, all at once + const returnCodes = await Promise.all(files.map(runIndividualTest.bind(undefined, extraArgs))); + + // Or all of the codes together + returnCode = returnCodes.reduce((p, c) => p | c); } } catch (ex) { console.log(`Functional tests run failure: ${ex}.`); @@ -73,8 +150,10 @@ async function main() { process.env['MOCHA_FILE'] = originalMochaFile; } - // Indicate error code - console.log(`Functional test run result: ${returnCode}`); + var endTime = Date.now(); + + // Indicate error code and total time of the run + console.log(`Functional test run result: ${returnCode} after ${(endTime - startTime) / 1_000} seconds`); process.exit(returnCode); } diff --git a/build/ci/scripts/spec_with_pid.js b/build/ci/scripts/spec_with_pid.js new file mode 100644 index 000000000000..52cb064446cc --- /dev/null +++ b/build/ci/scripts/spec_with_pid.js @@ -0,0 +1,96 @@ +'use strict'; +/** + * @module Spec + */ +/** + * Module dependencies. + */ + +var Base = require('mocha/lib/reporters/base'); +var constants = require('mocha/lib/runner').constants; +var EVENT_RUN_BEGIN = constants.EVENT_RUN_BEGIN; +var EVENT_RUN_END = constants.EVENT_RUN_END; +var EVENT_SUITE_BEGIN = constants.EVENT_SUITE_BEGIN; +var EVENT_SUITE_END = constants.EVENT_SUITE_END; +var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; +var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; +var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING; +var inherits = require('mocha/lib/utils').inherits; +var color = Base.color; + +/** + * Expose `Spec`. + */ + +exports = module.exports = Spec; + +/** + * Constructs a new `Spec` reporter instance. + * + * @public + * @class + * @memberof Mocha.reporters + * @extends Mocha.reporters.Base + * @param {Runner} runner - Instance triggers reporter actions. + * @param {Object} [options] - runner options + */ +function Spec(runner, options) { + Base.call(this, runner, options); + + var self = this; + var indents = 0; + var n = 0; + + function indent() { + return Array(indents).join(' '); + } + + runner.on(EVENT_RUN_BEGIN, function () { + Base.consoleLog(); + }); + + runner.on(EVENT_SUITE_BEGIN, function (suite) { + ++indents; + Base.consoleLog(color('suite', `${process.pid} %s%s`), indent(), suite.title); + }); + + runner.on(EVENT_SUITE_END, function () { + --indents; + if (indents === 1) { + Base.consoleLog(); + } + }); + + runner.on(EVENT_TEST_PENDING, function (test) { + var fmt = indent() + color('pending', `${process.pid} - %s`); + Base.consoleLog(fmt, test.title); + }); + + runner.on(EVENT_TEST_PASS, function (test) { + var fmt; + if (test.speed === 'fast') { + fmt = indent() + color('checkmark', `${process.pid} ` + Base.symbols.ok) + color('pass', ' %s'); + Base.consoleLog(fmt, test.title); + } else { + fmt = + indent() + + color('checkmark', `${process.pid} ` + Base.symbols.ok) + + color('pass', ' %s') + + color(test.speed, ' (%dms)'); + Base.consoleLog(fmt, test.title, test.duration); + } + }); + + runner.on(EVENT_TEST_FAIL, function (test) { + Base.consoleLog(indent() + color('fail', `${process.pid} %d) %s`), ++n, test.title); + }); + + runner.once(EVENT_RUN_END, self.epilogue.bind(self)); +} + +/** + * Inherit from `Base.prototype`. + */ +inherits(Spec, Base); + +Spec.description = 'hierarchical & verbose [default]'; diff --git a/build/ci/templates/test_phases.yml b/build/ci/templates/test_phases.yml index 09d55bb967bb..d4e9a30ac28e 100644 --- a/build/ci/templates/test_phases.yml +++ b/build/ci/templates/test_phases.yml @@ -117,7 +117,7 @@ steps: python -c "import sys;print(sys.executable)" displayName: 'pip install functional requirements' condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true')) - + # Add CONDA to the path so anaconda works # # This task will only run if variable `NeedsPythonFunctionalReqs` is true. @@ -264,39 +264,26 @@ steps: python -c "from __future__ import print_function;import sys;print('##vso[task.setvariable variable=CI_PYTHON_PATH;]{}'.format(sys.executable))" displayName: 'Set CI_PYTHON_PATH' - # Run the functional tests with each file split. + # Run the non DS functional tests # # This task only runs if the string 'testFunctional' exists in variable `TestsToRun`. # - # Note it is crucial this uses npm to start the runFunctionalTests.js. Otherwise the - # environment will be messed up. - # - # Example command line (windows pwsh): - # > node build/ci/scripts/runFunctionalTests.js - - script: | - npm run test:functional:split - displayName: 'Run functional split' - condition: and(succeeded(), contains(variables['TestsToRun'], 'testFunctional'), eq(variables['SplitFunctionalTests'], 'true')) - env: - DISPLAY: :10 - - # Run the functional tests when not splitting - # - # This task only runs if the string 'testFunctional' exists in variable `TestsToRun`. + # It runs the functional tests that don't start with 'DataScience'. DataScience functional tests + # will be handled in a separate yml. # # Example command line (windows pwsh): - # > node build/ci/scripts/runFunctionalTests.js + # > npm run test:functional - script: | - npm run test:functional + npm run test:functional -- --grep="^(?!DataScience).*$" displayName: 'Run functional tests' - condition: and(succeeded(), contains(variables['TestsToRun'], 'testFunctional'), not(eq(variables['SplitFunctionalTests'], 'true'))) + condition: and(succeeded(), contains(variables['TestsToRun'], 'testFunctional')) env: DISPLAY: :10 # Upload the test results to Azure DevOps to facilitate test reporting in their UX. - task: PublishTestResults@2 displayName: 'Publish functional tests results' - condition: contains(variables['TestsToRun'], 'testFunctional') + condition: or(contains(variables['TestsToRun'], 'testFunctional'), contains(variables['TestsToRun'], 'testParallelFunctional')) inputs: testResultsFiles: '$(Build.ArtifactStagingDirectory)/test-junit*.xml' testRunTitle: 'functional-$(Agent.Os)-Py$(pythonVersion)' diff --git a/build/ci/vscode-python-ci-manual.yaml b/build/ci/vscode-python-ci-manual.yaml deleted file mode 100644 index 5b157b52dbd8..000000000000 --- a/build/ci/vscode-python-ci-manual.yaml +++ /dev/null @@ -1,244 +0,0 @@ -# manual CI build - -name: '$(Year:yyyy).$(Month).0.$(BuildID)-manual' - -trigger: none -pr: none - -# Variables that are available for the entire pipeline. -variables: - - template: templates/globals.yml - -stages: - - stage: Build - jobs: - - template: templates/jobs/build_compile.yml - - # Each item in each matrix has a number of possible values it may - # define. They are detailed in templates/test_phases.yml. The only - # required value is "TestsToRun". - - - stage: Linux - dependsOn: [] - jobs: - - job: 'Py3x' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - # with mocks - # focused on small units (i.e. functions) - # and tightly controlled dependencies - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - # no mocks, no vscode - # focused on integration - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - # no mocks, with vscode - # focused on integration - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Smoke': - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - #maxParallel: 3 - pool: - vmImage: 'ubuntu-16.04' - steps: - - template: templates/test_phases.yml - - # This is the oldest Python 3 version we support. - - job: 'Py36' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - PythonVersion: '3.6' - # Note: "pythonInternalTools" tests are 3.7+. - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - PythonVersion: '3.6' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - PythonVersion: '3.6' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - #maxParallel: 3 - pool: - vmImage: 'ubuntu-16.04' - steps: - - template: templates/test_phases.yml - - # This is the oldest Python 3 version we support. - - job: 'Py35' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - PythonVersion: '3.5' - # Note: "pythonInternalTools" tests are 3.7+. - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - PythonVersion: '3.5' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - PythonVersion: '3.5' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - #maxParallel: 3 - pool: - vmImage: 'ubuntu-16.04' - steps: - - template: templates/test_phases.yml - - - stage: Mac - dependsOn: [] - jobs: - - job: 'Py3x' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Debugger': - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Smoke': - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - #maxParallel: 3 - pool: - vmImage: '$(vmImageMacOS)' - steps: - - template: templates/test_phases.yml - - # This is the oldest Python 3 version we support. - - job: 'Py35' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - PythonVersion: '3.5' - # Note: "pythonInternalTools" tests are 3.7+. - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - PythonVersion: '3.5' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - PythonVersion: '3.5' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Debugger': - PythonVersion: '3.5' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - #maxParallel: 3 - pool: - vmImage: '$(vmImageMacOS)' - steps: - - template: templates/test_phases.yml - - - stage: Windows - dependsOn: [] - jobs: - - job: 'Py3x' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Debugger': - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Smoke': - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - #maxParallel: 3 - pool: - vmImage: 'vs2017-win2016' - steps: - - template: templates/test_phases.yml - - # This is the oldest Python 3 version we support. - - job: 'Py35' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - PythonVersion: '3.5' - # Note: "pythonInternalTools" tests are 3.7+. - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - PythonVersion: '3.5' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - PythonVersion: '3.5' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Debugger': - PythonVersion: '3.5' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - #maxParallel: 3 - pool: - vmImage: 'vs2017-win2016' - steps: - - template: templates/test_phases.yml - - - stage: Reports - dependsOn: - - Linux - - Mac - - Windows - condition: always() - jobs: - - template: templates/jobs/coverage.yml diff --git a/build/ci/vscode-python-ci.yaml b/build/ci/vscode-python-ci.yaml deleted file mode 100644 index 0e718f8c6c03..000000000000 --- a/build/ci/vscode-python-ci.yaml +++ /dev/null @@ -1,158 +0,0 @@ -# CI build (PR merge) - -name: '$(Year:yyyy).$(Month).0.$(BuildID)-ci' - -# Notes: Only trigger a commit for main and release, and skip build/rebuild -# on changes in the news and .vscode folders. -trigger: - branches: - include: ['main', 'release*'] - paths: - exclude: ['/news/1 Enhancements', '/news/2 Fixes', '/news/3 Code Health', '/.vscode'] - -# Not the PR build for merges to main and release. -pr: none - -# Variables that are available for the entire pipeline. -variables: - - template: templates/globals.yml - -stages: - - stage: Build - jobs: - - template: templates/jobs/build_compile.yml - - # Each item in each matrix has a number of possible values it may - # define. They are detailed in templates/test_phases.yml. The only - # required value is "TestsToRun". - - - stage: Linux - dependsOn: [] - jobs: - - job: 'Py3x' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - # with mocks - # focused on small units (i.e. functions) - # and tightly controlled dependencies - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - # no mocks, no vscode - # focused on integration - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - # no mocks, with vscode - # focused on integration - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - # 'Smoke': - # TestsToRun: 'testSmoke' - # NeedsPythonTestReqs: true - # NeedsIPythonReqs: true - maxParallel: 2 - pool: - vmImage: 'ubuntu-16.04' - steps: - - template: templates/test_phases.yml - - - # # This is the oldest Python 3 version we support. - # - job: 'Py35' - # dependsOn: [] - # timeoutInMinutes: 120 - # strategy: - # matrix: - # 'Unit': - # PythonVersion: '3.5' - # # Note: "pythonInternalTools" tests are 3.7+. - # TestsToRun: 'testUnitTests, pythonIPythonTests' - # NeedsPythonTestReqs: true - # NeedsIPythonReqs: true - # 'Debugger': - # PythonVersion: '3.5' - # TestsToRun: 'testDebugger' - # NeedsPythonTestReqs: true - # maxParallel: 2 - # pool: - # vmImage: 'ubuntu-16.04' - # steps: - # - template: templates/test_phases.yml - # - stage: Mac - # dependsOn: [] - # jobs: - # - job: 'Py3x' - # dependsOn: [] - # timeoutInMinutes: 120 - # strategy: - # matrix: - # 'Unit': - # TestsToRun: 'testUnitTests, pythonIPythonTests' - # NeedsPythonTestReqs: true - # NeedsIPythonReqs: true - # 'Functional': - # TestsToRun: 'testfunctional' - # NeedsPythonTestReqs: true - # NeedsPythonFunctionalReqs: true - # 'Single Workspace': - # TestsToRun: 'testSingleWorkspace' - # NeedsPythonTestReqs: true - # 'Debugger': - # TestsToRun: 'testDebugger' - # NeedsPythonTestReqs: true - # 'Smoke': - # TestsToRun: 'testSmoke' - # NeedsPythonTestReqs: true - # NeedsIPythonReqs: true - # maxParallel: 2 - # pool: - # vmImage: '$(vmImageMacOS)' - # steps: - # - template: templates/test_phases.yml - - # - stage: Windows - # dependsOn: [] - # jobs: - # - job: 'Py3x' - # dependsOn: [] - # timeoutInMinutes: 120 - # strategy: - # matrix: - # 'Unit': - # TestsToRun: 'testUnitTests, pythonIPythonTests' - # NeedsPythonTestReqs: true - # NeedsIPythonReqs: true - # 'Functional': - # TestsToRun: 'testfunctional' - # NeedsPythonTestReqs: true - # NeedsPythonFunctionalReqs: true - # 'Single Workspace': - # TestsToRun: 'testSingleWorkspace' - # NeedsPythonTestReqs: true - # 'Debugger': - # TestsToRun: 'testDebugger' - # NeedsPythonTestReqs: true - # 'Smoke': - # TestsToRun: 'testSmoke' - # NeedsPythonTestReqs: true - # NeedsIPythonReqs: true - # maxParallel: 2 - # pool: - # vmImage: 'vs2017-win2016' - # steps: - # - template: templates/test_phases.yml - - - stage: Reports - dependsOn: - - Linux - # - Mac - # - Windows - condition: always() - jobs: - - template: templates/jobs/coverage.yml diff --git a/build/ci/vscode-python-nightly-ci.yaml b/build/ci/vscode-python-nightly-ci.yaml deleted file mode 100644 index 50723d4e8759..000000000000 --- a/build/ci/vscode-python-nightly-ci.yaml +++ /dev/null @@ -1,422 +0,0 @@ -# Nightly build - -name: '$(Year:yyyy).$(Month).0.$(BuildID)-nightly' - -# Not the CI build, see `vscode-python-ci.yaml`. -trigger: none - -# Not the PR build for merges to main and release. -pr: none - -schedules: - - cron: '0 8 * * 1-5' - # Daily midnight PST build, runs Monday - Friday always - displayName: Nightly build - branches: - include: - - main - - release* - always: true - -# Variables that are available for the entire pipeline. -variables: - - template: templates/globals.yml - -stages: - - stage: Build - jobs: - - template: templates/jobs/build_compile.yml - - # Each item in each matrix has a number of possible values it may - # define. They are detailed in templates/test_phases.yml. The only - # required value is "TestsToRun". - - - stage: Linux - dependsOn: - - Build - jobs: - - job: 'Py3x' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Debugger': - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Smoke': - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - maxParallel: 1 - pool: - vmImage: 'ubuntu-16.04' - steps: - - template: templates/test_phases.yml - - - job: 'Py36' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - PythonVersion: '3.6' - # Note: "pythonInternalTools" tests are 3.7+. - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - PythonVersion: '3.6' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - PythonVersion: '3.6' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Debugger': - PythonVersion: '3.6' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - # Note: We only run the smoke tests with the latest Python release. - maxParallel: 1 - pool: - vmImage: 'ubuntu-16.04' - steps: - - template: templates/test_phases.yml - - - job: 'Py35' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - PythonVersion: '3.5' - # Note: "pythonInternalTools" tests are 3.7+. - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - PythonVersion: '3.5' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - PythonVersion: '3.5' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Debugger': - PythonVersion: '3.5' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - # Note: We only run the smoke tests with the latest Python release. - maxParallel: 1 - pool: - vmImage: 'ubuntu-16.04' - steps: - - template: templates/test_phases.yml - - - job: 'Py27' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - PythonVersion: '2.7' - # Note: "pythonInternalTools" tests are 3.7+. - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - PythonVersion: '2.7' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - PythonVersion: '2.7' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Debugger': - PythonVersion: '2.7' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - # Note: Virtual env tests use `venv` and won't currently work with Python 2.7 - # Note: We only run the smoke tests with the latest Python release. - maxParallel: 1 - pool: - vmImage: 'ubuntu-16.04' - steps: - - template: templates/test_phases.yml - - - stage: Mac - dependsOn: - - Build - jobs: - - job: 'Py3x' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Debugger': - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Smoke': - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - maxParallel: 1 - pool: - vmImage: '$(vmImageMacOS)' - steps: - - template: templates/test_phases.yml - - - job: 'Py36' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - PythonVersion: '3.6' - # Note: "pythonInternalTools" tests are 3.7+. - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - PythonVersion: '3.6' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - PythonVersion: '3.6' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Debugger': - PythonVersion: '3.6' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - # Note: We only run the smoke tests with the latest Python release. - maxParallel: 1 - pool: - vmImage: '$(vmImageMacOS)' - steps: - - template: templates/test_phases.yml - - - job: 'Py35' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - PythonVersion: '3.5' - # Note: "pythonInternalTools" tests are 3.7+. - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - PythonVersion: '3.5' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - PythonVersion: '3.5' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Debugger': - PythonVersion: '3.5' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - # Note: We only run the smoke tests with the latest Python release. - maxParallel: 1 - pool: - vmImage: '$(vmImageMacOS)' - steps: - - template: templates/test_phases.yml - - - job: 'Py27' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - PythonVersion: '2.7' - # Note: "pythonInternalTools" tests are 3.7+. - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - PythonVersion: '2.7' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - PythonVersion: '2.7' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Debugger': - PythonVersion: '2.7' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - # Note: Virtual env tests use `venv` and won't currently work with Python 2.7 - # Note: We only run the smoke tests with the latest Python release. - maxParallel: 1 - pool: - vmImage: '$(vmImageMacOS)' - steps: - - template: templates/test_phases.yml - - - stage: Windows - dependsOn: - - Build - jobs: - - job: 'Py3x' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Debugger': - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Smoke': - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - maxParallel: 1 - pool: - vmImage: 'vs2017-win2016' - steps: - - template: templates/test_phases.yml - - - job: 'Py36' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - PythonVersion: '3.6' - # Note: "pythonInternalTools" tests are 3.7+. - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - PythonVersion: '3.6' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - PythonVersion: '3.6' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Debugger': - PythonVersion: '3.6' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - # Note: We only run the smoke tests with the latest Python release. - maxParallel: 1 - pool: - vmImage: 'vs2017-win2016' - steps: - - template: templates/test_phases.yml - - - job: 'Py35' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - PythonVersion: '3.5' - # Note: "pythonInternalTools" tests are 3.7+. - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - PythonVersion: '3.5' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - PythonVersion: '3.5' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Debugger': - PythonVersion: '3.5' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - # Note: We only run the smoke tests with the latest Python release. - maxParallel: 1 - pool: - vmImage: 'vs2017-win2016' - steps: - - template: templates/test_phases.yml - - - job: 'Py27' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Unit': - PythonVersion: '2.7' - # Note: "pythonInternalTools" tests are 3.7+. - TestsToRun: 'testUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - PythonVersion: '2.7' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Single Workspace': - PythonVersion: '2.7' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Debugger': - PythonVersion: '2.7' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - # Note: Virtual env tests use `venv` and won't currently work with Python 2.7 - # Note: We only run the smoke tests with the latest Python release. - maxParallel: 1 - pool: - vmImage: 'vs2017-win2016' - steps: - - template: templates/test_phases.yml - - - stage: Reports - dependsOn: - - Linux - - Mac - - Windows - condition: always() - jobs: - - template: templates/jobs/coverage.yml diff --git a/build/ci/vscode-python-nightly-flake-ci.yaml b/build/ci/vscode-python-nightly-flake-ci.yaml deleted file mode 100644 index db168f479927..000000000000 --- a/build/ci/vscode-python-nightly-flake-ci.yaml +++ /dev/null @@ -1,90 +0,0 @@ -# Nightly build - -name: '$(Year:yyyy).$(Month).0.$(BuildID)-nightly-flake' - -# Not the CI build, see `vscode-python-nightly-flake-ci.yaml`. -trigger: none - -# Not the PR build for merges to main and release. -pr: none - -schedules: - - cron: '0 8 * * 1-5' - # Daily midnight PST build, runs Monday - Friday always - displayName: Nightly Flake build - branches: - include: - - main - - release* - always: true - -# Variables that are available for the entire pipeline. -variables: - - template: templates/globals.yml - -stages: - - stage: Build - jobs: - - template: templates/jobs/build_compile.yml - - # Each item in each matrix has a number of possible values it may - # define. They are detailed in templates/test_phases.yml. The only - # required value is "TestsToRun". - - - stage: Linux - dependsOn: - - Build - jobs: - - job: 'Py3x_Linux' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Functional': - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - VSCODE_PYTHON_ROLLING: true - pool: - vmImage: 'ubuntu-16.04' - steps: - - template: templates/test_phases.yml - - - stage: Mac - dependsOn: - - Build - jobs: - - job: 'Py3x_Mac' - dependsOn: [] - timeoutInMinutes: 120 - strategy: - matrix: - 'Functional': - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - VSCODE_PYTHON_ROLLING: true - pool: - vmImage: '$(vmImageMacOS)' - steps: - - template: templates/test_phases.yml - - - stage: Windows - dependsOn: - - Build - jobs: - - job: 'Py3x_Windows' - dependsOn: [] - timeoutInMinutes: 180 - strategy: - matrix: - 'Functional': - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - VSCODE_PYTHON_ROLLING: true - SplitFunctionalTests: true - pool: - vmImage: 'vs2017-win2016' - steps: - - template: templates/test_phases.yml diff --git a/build/ci/vscode-python-pr-validation.yaml b/build/ci/vscode-python-pr-validation.yaml deleted file mode 100644 index 04024ad74b25..000000000000 --- a/build/ci/vscode-python-pr-validation.yaml +++ /dev/null @@ -1,126 +0,0 @@ -# PR Validation build. - -name: '$(Year:yyyy).$(Month).0.$(BuildID)-pr' - -# Notes: Only trigger a PR build for main and release, and skip build/rebuild -# on changes in the news and .vscode folders. -pr: - autoCancel: true - branches: - include: - - 'main' - - 'release*' - - 'ds*' - - 'logging-changes-and-drop-old-debugger' - paths: - exclude: ['/news/1 Enhancements', '/news/2 Fixes', '/news/3 Code Health', '/.vscode'] - -# Not the CI build for merges to main and release. -trigger: none - -# Variables that are available for the entire pipeline. -variables: - - template: templates/globals.yml - -stages: - - stage: Build - jobs: - - template: templates/jobs/build_compile.yml - - # Each item in each matrix has a number of possible values it may - # define. They are detailed in templates/test_phases.yml. The only - # required value is "TestsToRun". - - - stage: Linux - dependsOn: [] - jobs: - - job: 'Py3x' - dependsOn: [] - strategy: - matrix: - 'NodeUnit': - TestsToRun: 'testUnitTests' - NeedsPythonTestReqs: false - NeedsIPythonReqs: false - 'Unit': - TestsToRun: 'pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Functional': - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - SplitFunctionalTests: false - # 'DataScience': - # TestsToRun: 'testVSCode' - # NeedsPythonTestReqs: true - # NeedsPythonFunctionalReqs: true - # 'Smoke': - # TestsToRun: 'testSmoke' - # NeedsPythonTestReqs: true - # NeedsIPythonReqs: true - pool: - vmImage: 'ubuntu-16.04' - steps: - - template: templates/test_phases.yml - - # - job: 'Py27' - # dependsOn: [] - # strategy: - # matrix: - # 'Functional': - # PythonVersion: '2.7' - # TestsToRun: 'testfunctional' - # NeedsPythonTestReqs: true - # NeedsPythonFunctionalReqs: true - # pool: - # vmImage: 'ubuntu-16.04' - # steps: - # - template: templates/test_phases.yml - -# - stage: Mac -# dependsOn: [] -# jobs: -# - job: 'Py3x' -# dependsOn: [] -# strategy: -# matrix: -# # This gives us our best functional coverage for the OS. -# 'Functional+Single': -# TestsToRun: 'testfunctional, testSingleWorkspace' -# NeedsPythonTestReqs: true -# NeedsPythonFunctionalReqs: true -# pool: -# vmImage: '$(vmImageMacOS)' -# steps: -# - template: templates/test_phases.yml - -# - stage: Windows -# dependsOn: [] -# jobs: -# - job: 'Py3x' -# dependsOn: [] -# timeoutInMinutes: 90 -# strategy: -# matrix: -# # This gives us our best functional coverage for the OS. -# 'Functional': -# TestsToRun: 'testfunctional' -# NeedsPythonTestReqs: true -# NeedsPythonFunctionalReqs: true -# 'Single Workspace': -# TestsToRun: 'testSingleWorkspace' -# NeedsPythonTestReqs: true -# pool: -# vmImage: 'vs2017-win2016' -# steps: -# - template: templates/test_phases.yml - -# - stage: Reports -# dependsOn: -# - Linux -# # - Mac -# # - Windows -# condition: always() -# jobs: -# - template: templates/jobs/coverage.yml diff --git a/news/2 Fixes/11151.md b/news/2 Fixes/11151.md new file mode 100644 index 000000000000..1b2eb171a996 --- /dev/null +++ b/news/2 Fixes/11151.md @@ -0,0 +1 @@ +Remove transient data when saving a notebook from the interactive window. \ No newline at end of file diff --git a/news/2 Fixes/12530.md b/news/2 Fixes/12530.md new file mode 100644 index 000000000000..1dae3afc16cc --- /dev/null +++ b/news/2 Fixes/12530.md @@ -0,0 +1 @@ +Make sure not to set ```__file__``` unless necessary as this can mess up some modules (like multiprocessing) \ No newline at end of file diff --git a/news/2 Fixes/14212.md b/news/2 Fixes/14212.md new file mode 100644 index 000000000000..97b17eefe462 --- /dev/null +++ b/news/2 Fixes/14212.md @@ -0,0 +1 @@ +Fix interactive debugging starting (trimQuotes error). \ No newline at end of file diff --git a/news/2 Fixes/14216.md b/news/2 Fixes/14216.md new file mode 100644 index 000000000000..8dfd8ad1ba21 --- /dev/null +++ b/news/2 Fixes/14216.md @@ -0,0 +1 @@ +Fix latex output not showing up without a 'display' call. \ No newline at end of file diff --git a/news/3 Code Health/14290.md b/news/3 Code Health/14290.md new file mode 100644 index 000000000000..8e7fac32f023 --- /dev/null +++ b/news/3 Code Health/14290.md @@ -0,0 +1 @@ +Functional test failures related to kernel ports overlapping. \ No newline at end of file diff --git a/package.json b/package.json index 68092788cb2c..58cde7c7aa57 100644 --- a/package.json +++ b/package.json @@ -1695,7 +1695,7 @@ "test:functional:perf": "node --inspect-brk ./node_modules/mocha/bin/_mocha --require source-map-support/register --config ./build/.mocha.functional.perf.json", "test:functional:memleak": "node --inspect-brk ./node_modules/mocha/bin/_mocha --require source-map-support/register --config ./build/.mocha.functional.json", "test:functional:cover": "npm run test:functional", - "test:functional:split": "node ./build/ci/scripts/runFunctionalTests.js", + "test:functional:parallel": "node ./build/ci/scripts/runFunctionalTests.js", "test:cover:report": "nyc --nycrc-path build/.nycrc report --reporter=text --reporter=html --reporter=text-summary --reporter=cobertura", "testSingleWorkspace": "node ./out/test/testBootstrap.js ./out/test/standardTest.js", "preTestJediLSP": "node ./out/test/languageServers/jedi/lspSetup.js", diff --git a/pythonFiles/vscode_datascience_helpers/jupyter_daemon.py b/pythonFiles/vscode_datascience_helpers/jupyter_daemon.py index a8cef85271c1..a72bb7079d7d 100644 --- a/pythonFiles/vscode_datascience_helpers/jupyter_daemon.py +++ b/pythonFiles/vscode_datascience_helpers/jupyter_daemon.py @@ -140,11 +140,15 @@ def _print_kernel_list_json(self): sys.stdout.flush() def _convert(self, args): - self.log.info("nbconvert") + self.log.info("Starting nbconvert wirth args %s", args) from nbconvert import nbconvertapp as app - sys.argv = [""] + args - app.main() + try: + sys.argv = [""] + args + app.main() + except Exception as e: + self.log.info("Nbconvert error: %s", e) + raise def _start_notebook(self, args, cwd, env): from notebook import notebookapp as app diff --git a/pythonFiles/vscode_datascience_helpers/tests/logParser.py b/pythonFiles/vscode_datascience_helpers/tests/logParser.py new file mode 100644 index 000000000000..767f837c5136 --- /dev/null +++ b/pythonFiles/vscode_datascience_helpers/tests/logParser.py @@ -0,0 +1,96 @@ +from io import TextIOWrapper +import sys +import argparse +import os + +os.system("color") +from pathlib import Path +import re + +parser = argparse.ArgumentParser(description="Parse a test log into its parts") +parser.add_argument("testlog", type=str, nargs=1, help="Log to parse") +parser.add_argument( + "--testoutput", action="store_true", help="Show all failures and passes" +) +parser.add_argument( + "--split", + action="store_true", + help="Split into per process files. Each file will have the pid appended", +) +ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") +pid_regex = re.compile(r"(\d+).*") +timestamp_regex = re.compile(r"\d{4}-\d{2}-\d{2}T.*\dZ") + + +def stripTimestamp(line: str): + match = timestamp_regex.match(line) + if match: + return line[match.end() :] + return line + + +def readStripLines(f: TextIOWrapper): + return map(stripTimestamp, f.readlines()) + + +def printTestOutput(testlog): + # Find all the lines that don't have a PID in them. These are the test output + p = Path(testlog[0]) + with p.open() as f: + for line in readStripLines(f): + stripped = line.strip() + if len(stripped) > 2 and stripped[0] == "\x1B" and stripped[1] == "[": + print(line.rstrip()) # Should be a test line as it has color encoding + + +def splitByPid(testlog): + # Split testlog into prefixed logs based on pid + baseFile = os.path.splitext(testlog[0])[0] + p = Path(testlog[0]) + pids = set() + logs = {} + pid = None + with p.open() as f: + for line in readStripLines(f): + stripped = ansi_escape.sub("", line.strip()) + if len(stripped) > 0: + # Pull out the pid + match = pid_regex.match(stripped) + + # Pids are at least two digits + if match and len(match.group(1)) > 2: + # Pid is found + pid = int(match.group(1)) + + # See if we've created a log for this pid or not + if not pid in pids: + pids.add(pid) + logFile = "{}_{}.log".format(baseFile, pid) + print("Writing to new log: " + logFile) + logs[pid] = Path(logFile).open(mode="w") + + # Add this line to the log + if pid != None: + logs[pid].write(line) + # Close all of the open logs + for key in logs: + logs[key].close() + + +def doWork(args): + if not args.testlog: + print("Test log should be passed") + elif args.testoutput: + printTestOutput(args.testlog) + elif args.split: + splitByPid(args.testlog) + else: + parser.print_usage() + + +def main(): + doWork(parser.parse_args()) + + +if __name__ == "__main__": + main() diff --git a/src/client/common/process/baseDaemon.ts b/src/client/common/process/baseDaemon.ts index abd93668115d..b95991952fe4 100644 --- a/src/client/common/process/baseDaemon.ts +++ b/src/client/common/process/baseDaemon.ts @@ -177,14 +177,20 @@ export abstract class BasePythonDaemon { return Object.keys(options).every((item) => daemonSupportedSpawnOptions.indexOf(item as any) >= 0); } protected sendRequestWithoutArgs(type: RequestType0): Thenable { - return Promise.race([this.connection.sendRequest(type), this.connectionClosedDeferred.promise]); + if (this.proc && typeof this.proc.exitCode !== 'number') { + return Promise.race([this.connection.sendRequest(type), this.connectionClosedDeferred.promise]); + } + return this.connectionClosedDeferred.promise; } protected sendRequest(type: RequestType, params?: P): Thenable { - if (!this.isAlive) { + if (!this.isAlive || typeof this.proc.exitCode === 'number') { traceError('Daemon is handling a request after death.'); } - // Throw an error if the connection has been closed. - return Promise.race([this.connection.sendRequest(type, params), this.connectionClosedDeferred.promise]); + if (this.proc && typeof this.proc.exitCode !== 'number') { + // Throw an error if the connection has been closed. + return Promise.race([this.connection.sendRequest(type, params), this.connectionClosedDeferred.promise]); + } + return this.connectionClosedDeferred.promise; } protected throwIfRPCConnectionIsDead() { if (!this.isAlive) { diff --git a/src/client/datascience/common.ts b/src/client/datascience/common.ts index eda0fd8236cb..be8c9cd0fff3 100644 --- a/src/client/datascience/common.ts +++ b/src/client/datascience/common.ts @@ -38,7 +38,7 @@ const dummyExecuteResultObj: nbformat.IExecuteResult = { data: {}, metadata: {} }; -const AllowedKeys = { +export const AllowedCellOutputKeys = { ['stream']: new Set(Object.keys(dummyStreamObj)), ['error']: new Set(Object.keys(dummyErrorObj)), ['display_data']: new Set(Object.keys(dummyDisplayObj)), @@ -73,7 +73,7 @@ function fixupOutput(output: nbformat.IOutput): nbformat.IOutput { case 'error': case 'execute_result': case 'display_data': - allowedKeys = AllowedKeys[output.output_type]; + allowedKeys = AllowedCellOutputKeys[output.output_type]; break; default: return output; diff --git a/src/client/datascience/export/exportBase.ts b/src/client/datascience/export/exportBase.ts index f53457005fdd..3448c18414f0 100644 --- a/src/client/datascience/export/exportBase.ts +++ b/src/client/datascience/export/exportBase.ts @@ -56,7 +56,8 @@ export class ExportBase implements IExport { '--output', path.basename(tempTarget.filePath), '--output-dir', - path.dirname(tempTarget.filePath) + path.dirname(tempTarget.filePath), + '--debug' ]; const result = await service.execModule('jupyter', ['nbconvert'].concat(args), { throwOnStdErr: false, diff --git a/src/client/datascience/interactive-common/interactiveBase.ts b/src/client/datascience/interactive-common/interactiveBase.ts index 1ad06c52cc23..6c4c358c038a 100644 --- a/src/client/datascience/interactive-common/interactiveBase.ts +++ b/src/client/datascience/interactive-common/interactiveBase.ts @@ -123,7 +123,7 @@ export abstract class InteractiveBase extends WebviewPanelHost; + protected abstract setFileInKernel(file: string, cancelToken: CancellationToken | undefined): Promise; + protected async clearResult(id: string): Promise { await this.ensureConnectionAndNotebook(); if (this._notebook) { @@ -631,16 +633,9 @@ export abstract class InteractiveBase extends WebviewPanelHost { + // Native editor doesn't set this as the ipython file should be set for a notebook. + } + protected async close(): Promise { // Fire our event this.closedEvent.fire(this); diff --git a/src/client/datascience/interactive-window/interactiveWindow.ts b/src/client/datascience/interactive-window/interactiveWindow.ts index ae7b51c256ad..9abfef92df20 100644 --- a/src/client/datascience/interactive-window/interactiveWindow.ts +++ b/src/client/datascience/interactive-window/interactiveWindow.ts @@ -2,7 +2,8 @@ // Licensed under the MIT License. import type { nbformat } from '@jupyterlab/coreutils'; import * as path from 'path'; -import { Event, EventEmitter, Memento, Uri, ViewColumn } from 'vscode'; +import * as uuid from 'uuid'; +import { CancellationToken, Event, EventEmitter, Memento, Uri, ViewColumn } from 'vscode'; import { IPythonExtensionChecker } from '../../api/types'; import { IApplicationShell, @@ -422,6 +423,38 @@ export class InteractiveWindow extends InteractiveBase implements IInteractiveWi protected async closeBecauseOfFailure(_exc: Error): Promise { this.dispose(); } + + protected async setFileInKernel(file: string, cancelToken: CancellationToken | undefined): Promise { + // If in perFile mode, set only once + if (this.mode === 'perFile' && !this.fileInKernel && this.notebook && file !== Identifiers.EmptyFileName) { + this.fileInKernel = file; + await this.notebook.execute( + `__file__ = '${file.replace(/\\/g, '\\\\')}'`, + file, + 0, + uuid(), + cancelToken, + true + ); + } else if ( + (!this.fileInKernel || !this.fs.areLocalPathsSame(this.fileInKernel, file)) && + this.mode !== 'perFile' && + this.notebook && + file !== Identifiers.EmptyFileName + ) { + // Otherwise we need to reset it every time + this.fileInKernel = file; + await this.notebook.execute( + `__file__ = '${file.replace(/\\/g, '\\\\')}'`, + file, + 0, + uuid(), + cancelToken, + true + ); + } + } + protected ensureConnectionAndNotebook(): Promise { // Keep track of users who have used interactive window in a worksapce folder. // To be used if/when changing workflows related to startup of jupyter. diff --git a/src/client/datascience/jupyter/interpreter/jupyterInterpreterStateStore.ts b/src/client/datascience/jupyter/interpreter/jupyterInterpreterStateStore.ts index 751505141079..768a39febfb8 100644 --- a/src/client/datascience/jupyter/interpreter/jupyterInterpreterStateStore.ts +++ b/src/client/datascience/jupyter/interpreter/jupyterInterpreterStateStore.ts @@ -6,7 +6,7 @@ import { inject, injectable, named } from 'inversify'; import { Memento } from 'vscode'; import { IExtensionSingleActivationService } from '../../../activation/types'; -import { IPythonApiProvider } from '../../../api/types'; +import { IPythonApiProvider, IPythonExtensionChecker } from '../../../api/types'; import { GLOBAL_MEMENTO, IMemento } from '../../../common/types'; import { noop } from '../../../common/utils/misc'; @@ -47,12 +47,13 @@ export class JupyterInterpreterStateStore { export class MigrateJupyterInterpreterStateService implements IExtensionSingleActivationService { constructor( @inject(IPythonApiProvider) private readonly api: IPythonApiProvider, - @inject(IMemento) @named(GLOBAL_MEMENTO) private readonly memento: Memento + @inject(IMemento) @named(GLOBAL_MEMENTO) private readonly memento: Memento, + @inject(IPythonExtensionChecker) private readonly checker: IPythonExtensionChecker ) {} // Migrate the interpreter path selected for Jupyter server from the Python extension's globalState memento public async activate() { - if (!this.memento.get(key)) { + if (!this.memento.get(key) && this.checker.isPythonExtensionInstalled) { const api = await this.api.getApi(); const data = api.getInterpreterPathSelectedForJupyterServer(); await this.memento.update(key, data); diff --git a/src/client/datascience/jupyter/jupyterDebugger.ts b/src/client/datascience/jupyter/jupyterDebugger.ts index 90189c3095fd..09998b1f774a 100644 --- a/src/client/datascience/jupyter/jupyterDebugger.ts +++ b/src/client/datascience/jupyter/jupyterDebugger.ts @@ -331,7 +331,7 @@ export class JupyterDebugger implements IJupyterDebugger, ICellHashListener { const data = outputs[0].data; if (data && data.hasOwnProperty('text/plain')) { // tslint:disable-next-line:no-any - return (data as any)['text/plain']; + return concatMultilineString((data as any)['text/plain']); } if (outputs[0].output_type === 'stream') { const stream = outputs[0] as nbformat.IStream; diff --git a/src/client/datascience/jupyter/jupyterExporter.ts b/src/client/datascience/jupyter/jupyterExporter.ts index c78dcb8ac91c..07fdf69206c7 100644 --- a/src/client/datascience/jupyter/jupyterExporter.ts +++ b/src/client/datascience/jupyter/jupyterExporter.ts @@ -17,6 +17,7 @@ import { IConfigurationService } from '../../common/types'; import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; import { CellMatcher } from '../cellMatcher'; +import { pruneCell } from '../common'; import { CodeSnippets, Identifiers } from '../constants'; import { CellState, @@ -235,9 +236,11 @@ export class JupyterExporter implements INotebookExporter { }; private pruneCell = (cell: ICell, cellMatcher: CellMatcher): nbformat.IBaseCell => { + // Prune with the common pruning function first. + const copy = pruneCell({ ...cell.data }); + // Remove the #%% of the top of the source if there is any. We don't need // this to end up in the exported ipynb file. - const copy = { ...cell.data }; copy.source = this.pruneSource(cell.data.source, cellMatcher); return copy; }; diff --git a/src/client/datascience/jupyter/jupyterNotebook.ts b/src/client/datascience/jupyter/jupyterNotebook.ts index bcd483164dd8..14a7f03308fa 100644 --- a/src/client/datascience/jupyter/jupyterNotebook.ts +++ b/src/client/datascience/jupyter/jupyterNotebook.ts @@ -275,6 +275,8 @@ export class JupyterNotebookBase implements INotebook { this.ranInitialSetup = true; this._workingDirectory = undefined; + traceInfo(`Initial setup for ${this.identity.toString()} starting ...`); + try { // When we start our notebook initial, change to our workspace or user specified root directory await this.updateWorkingDirectoryAndPath(); diff --git a/src/client/datascience/jupyter/jupyterSession.ts b/src/client/datascience/jupyter/jupyterSession.ts index 71d4309cdd88..ff8f705a2895 100644 --- a/src/client/datascience/jupyter/jupyterSession.ts +++ b/src/client/datascience/jupyter/jupyterSession.ts @@ -23,6 +23,7 @@ import { reportAction } from '../progress/decorator'; import { ReportableAction } from '../progress/types'; import { IJupyterConnection, ISessionWithSocket } from '../types'; import { JupyterInvalidKernelError } from './jupyterInvalidKernelError'; +import { JupyterWaitForIdleError } from './jupyterWaitForIdleError'; import { JupyterWebSockets } from './jupyterWebSocket'; import { getNameOfKernelConnection } from './kernels/helpers'; import { KernelConnectionMetadata } from './kernels/types'; @@ -90,9 +91,13 @@ export class JupyterSession extends BaseJupyterSession { // Make sure it is idle before we return await this.waitForIdleOnSession(newSession, timeoutMS); } catch (exc) { - traceError('Failed to change kernel', exc); - // Throw a new exception indicating we cannot change. - throw new JupyterInvalidKernelError(kernelConnection); + if (exc instanceof JupyterWaitForIdleError) { + throw exc; + } else { + traceError('Failed to change kernel', exc); + // Throw a new exception indicating we cannot change. + throw new JupyterInvalidKernelError(kernelConnection); + } } return newSession; diff --git a/src/client/datascience/jupyter/kernels/kernelService.ts b/src/client/datascience/jupyter/kernels/kernelService.ts index a0c395924d93..5f1dd777f8fe 100644 --- a/src/client/datascience/jupyter/kernels/kernelService.ts +++ b/src/client/datascience/jupyter/kernels/kernelService.ts @@ -327,7 +327,7 @@ export class KernelService { 'ipykernel', ['install', '--user', '--name', name, '--display-name', interpreter.displayName], { - throwOnStdErr: true, + throwOnStdErr: false, encoding: 'utf8', token: cancelToken } diff --git a/src/client/datascience/kernel-launcher/kernelLauncher.ts b/src/client/datascience/kernel-launcher/kernelLauncher.ts index af63835d19f5..0833b43df9a9 100644 --- a/src/client/datascience/kernel-launcher/kernelLauncher.ts +++ b/src/client/datascience/kernel-launcher/kernelLauncher.ts @@ -2,11 +2,16 @@ // Licensed under the MIT License. 'use strict'; +import * as fsextra from 'fs-extra'; import { inject, injectable } from 'inversify'; +import * as os from 'os'; +import * as path from 'path'; import * as portfinder from 'portfinder'; import { promisify } from 'util'; import * as uuid from 'uuid/v4'; import { IPythonExtensionChecker } from '../../api/types'; +import { isTestExecution } from '../../common/constants'; +import { traceInfo } from '../../common/logger'; import { IProcessServiceFactory } from '../../common/process/types'; import { Resource } from '../../common/types'; import { captureTelemetry } from '../../telemetry'; @@ -17,14 +22,15 @@ import { KernelDaemonPool } from './kernelDaemonPool'; import { KernelProcess } from './kernelProcess'; import { IKernelConnection, IKernelLauncher, IKernelProcess } from './types'; -const PortToStartFrom = 9_000; +const PortFormatString = `kernelLauncherPortStart_{0}.tmp`; // Launches and returns a kernel process given a resource or python interpreter. // If the given interpreter is undefined, it will try to use the selected interpreter. // If the selected interpreter doesn't have a kernel, it will find a kernel on disk and use that. @injectable() export class KernelLauncher implements IKernelLauncher { - private static nextFreePortToTryAndUse = PortToStartFrom; + private static startPortPromise = KernelLauncher.computeStartPort(); + private static nextFreePortToTryAndUsePromise = KernelLauncher.startPortPromise; constructor( @inject(IProcessServiceFactory) private processExecutionFactory: IProcessServiceFactory, @inject(IFileSystem) private readonly fs: IFileSystem, @@ -32,6 +38,47 @@ export class KernelLauncher implements IKernelLauncher { @inject(IPythonExtensionChecker) private readonly extensionChecker: IPythonExtensionChecker ) {} + // This function is public so it can be called when a test shuts down + public static async cleanupStartPort() { + try { + // Destroy the file + const port = await KernelLauncher.startPortPromise; + traceInfo(`Cleaning up port start file : ${port}`); + + const filePath = path.join(os.tmpdir(), PortFormatString.format(port.toString())); + await fsextra.remove(filePath); + } catch (exc) { + // If it fails it doesn't really matter. Just a temp file + traceInfo(`Kernel port mutex failed to cleanup: `, exc); + } + } + + private static async computeStartPort(): Promise { + if (isTestExecution()) { + // Since multiple instances of a test may be running, write our best guess to a shared file + let portStart = 9_000; + let result = 0; + while (result === 0 && portStart < 65_000) { + try { + // Try creating a file with the port in the name + const filePath = path.join(os.tmpdir(), PortFormatString.format(portStart.toString())); + await fsextra.open(filePath, 'wx'); + + // If that works, we have our port + result = portStart; + } catch { + // If that fails, it should mean the file already exists + portStart += 1_000; + } + } + traceInfo(`Computed port start for KernelLauncher is : ${result}`); + + return result; + } else { + return 9_000; + } + } + @captureTelemetry(Telemetry.KernelLauncherPerf) public async launch( kernelConnectionMetadata: KernelSpecConnectionMetadata | PythonKernelConnectionMetadata, @@ -52,18 +99,28 @@ export class KernelLauncher implements IKernelLauncher { return kernelProcess; } - private async getKernelConnection(): Promise { + private async getConnectionPorts(): Promise { const getPorts = promisify(portfinder.getPorts); + + // Have to wait for static port lookup (it handles case where two VS code instances are running) + const nextFreePort = await KernelLauncher.nextFreePortToTryAndUsePromise; + const startPort = await KernelLauncher.startPortPromise; + // Ports may have been freed, hence start from begining. - const port = - KernelLauncher.nextFreePortToTryAndUse > PortToStartFrom + 1_000 - ? PortToStartFrom - : KernelLauncher.nextFreePortToTryAndUse; + const port = nextFreePort > startPort + 1_000 ? startPort : nextFreePort; + + // Then get the next set starting at that point const ports = await getPorts(5, { host: '127.0.0.1', port }); + // We launch restart kernels in the background, its possible other session hasn't started. // Ensure we do not use same ports. - KernelLauncher.nextFreePortToTryAndUse = Math.max(...ports) + 1; + KernelLauncher.nextFreePortToTryAndUsePromise = Promise.resolve(Math.max(...ports) + 1); + return ports; + } + + private async getKernelConnection(): Promise { + const ports = await this.getConnectionPorts(); return { version: 1, key: uuid(), diff --git a/src/client/datascience/raw-kernel/liveshare/hostRawNotebookProvider.ts b/src/client/datascience/raw-kernel/liveshare/hostRawNotebookProvider.ts index b8211a888fd5..48787fd9ec2b 100644 --- a/src/client/datascience/raw-kernel/liveshare/hostRawNotebookProvider.ts +++ b/src/client/datascience/raw-kernel/liveshare/hostRawNotebookProvider.ts @@ -136,6 +136,7 @@ export class HostRawNotebookProvider notebookMetadata?: nbformat.INotebookMetadata, cancelToken?: CancellationToken ): Promise { + traceInfo(`Creating raw notebook for ${identity.toString()}`); const notebookPromise = createDeferred(); this.setNotebook(identity, notebookPromise.promise); @@ -143,6 +144,7 @@ export class HostRawNotebookProvider ? this.progressReporter.createProgressIndicator(localize.DataScience.connectingIPyKernel()) : undefined; + traceInfo(`Computing working directory ${identity.toString()}`); const workingDirectory = await computeWorkingDirectory(resource, this.workspaceService); const rawSession = new RawJupyterSession( @@ -156,6 +158,8 @@ export class HostRawNotebookProvider try { const launchTimeout = this.configService.getSettings().jupyterLaunchTimeout; + traceInfo(`Getting preferred kernel for ${identity.toString()}`); + // We need to locate kernelspec and possible interpreter for this launch based on resource and notebook metadata const kernelConnectionMetadata = await this.kernelSelector.getPreferredKernelForLocalConnection( resource, @@ -173,6 +177,7 @@ export class HostRawNotebookProvider ) { notebookPromise.reject('Failed to find a kernelspec to use for ipykernel launch'); } else { + traceInfo(`Connecting to raw session for ${identity.toString()}`); await rawSession.connect(kernelConnectionMetadata, launchTimeout, cancelToken); // Get the execution info for our notebook diff --git a/src/client/logging/formatters.ts b/src/client/logging/formatters.ts index b3dd4e52761f..488c4c91ccaf 100644 --- a/src/client/logging/formatters.ts +++ b/src/client/logging/formatters.ts @@ -3,6 +3,7 @@ 'use strict'; import { format } from 'winston'; +import { isTestExecution } from '../common/constants'; import { getLevel, LogLevel, LogLevelName } from './levels'; const TIMESTAMP = 'YYYY-MM-DD HH:mm:ss'; @@ -37,13 +38,17 @@ function normalizeLevel(name: LogLevelName): string { // Return a log entry that can be emitted as-is. function formatMessage(level: LogLevelName, timestamp: string, message: string): string { const levelFormatted = normalizeLevel(level); - return `${levelFormatted} ${timestamp}: ${message}`; + return isTestExecution() + ? `${process.pid} ${levelFormatted} ${timestamp}: ${message}` + : `${levelFormatted} ${timestamp}: ${message}`; } // Return a log entry that can be emitted as-is. function formatLabeledMessage(level: LogLevelName, timestamp: string, label: string, message: string): string { const levelFormatted = normalizeLevel(level); - return `${levelFormatted} ${label} ${timestamp}: ${message}`; + return isTestExecution() + ? `${process.pid} ${levelFormatted} ${label} ${timestamp}: ${message}` + : `${levelFormatted} ${label} ${timestamp}: ${message}`; } // Return a minimal format object that can be used with a "winston" diff --git a/src/datascience-ui/interactive-common/cellOutput.tsx b/src/datascience-ui/interactive-common/cellOutput.tsx index 33cea77eecbb..fe9467110940 100644 --- a/src/datascience-ui/interactive-common/cellOutput.tsx +++ b/src/datascience-ui/interactive-common/cellOutput.tsx @@ -316,22 +316,6 @@ export class CellOutput extends React.Component { input = JSON.stringify(output.data); renderWithScrollbars = true; isText = true; - } else if ( - output.output_type === 'execute_result' && - input && - input.hasOwnProperty('text/plain') && - !input.hasOwnProperty('text/html') - ) { - // Plain text should actually be shown as html so that escaped HTML shows up correctly - mimeType = 'text/html'; - isText = true; - isError = false; - renderWithScrollbars = true; - // tslint:disable-next-line: no-any - const text = (input as any)['text/plain']; - input = { - 'text/html': lodashEscape(concatMultilineString(text)) - }; } else if (output.output_type === 'stream') { mimeType = 'text/html'; isText = true; @@ -341,7 +325,7 @@ export class CellOutput extends React.Component { const stream = output as nbformat.IStream; // NOSONAR const concatted = lodashEscape(concatMultilineString(stream.text)); input = { - 'text/html': concatted // XML tags should have already been escaped. + 'text/html': concatted }; // Output may have ascii colorization chars in it. @@ -401,12 +385,6 @@ export class CellOutput extends React.Component { data = fixMarkdown(concatMultilineString(data as nbformat.MultilineString, true), true); } - // Make sure text output is escaped (nteract texttransform won't) - if (mimeType === 'text/plain' && data) { - data = lodashEscape(data.toString()); - mimeType = 'text/html'; - } - return { isText, isError, diff --git a/src/test/datascience/Untitled-1.ipynb b/src/test/datascience/Untitled-1.ipynb deleted file mode 100644 index 603be536258d..000000000000 --- a/src/test/datascience/Untitled-1.ipynb +++ /dev/null @@ -1,47 +0,0 @@ -{ - "metadata": { - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.6-final" - }, - "orig_nbformat": 2 - }, - "nbformat": 4, - "nbformat_minor": 2, - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "output_type": "execute_result", - "data": { - "text/plain": "1" - }, - "metadata": {}, - "execution_count": 1 - } - ], - "source": [ - "a=1\n", - "a" - ] - } - ] -} \ No newline at end of file diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index a41155e628d1..8182ef62e21d 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -2,8 +2,6 @@ // Licensed under the MIT License. //tslint:disable:trailing-comma no-any import { ReactWrapper } from 'enzyme'; -import * as fs from 'fs-extra'; -import * as glob from 'glob'; import { interfaces } from 'inversify'; import * as os from 'os'; import * as path from 'path'; @@ -26,7 +24,6 @@ import { import * as vsls from 'vsls/vscode'; import { KernelDaemonPool } from '../../client/datascience/kernel-launcher/kernelDaemonPool'; -import { promisify } from 'util'; import { IExtensionSingleActivationService } from '../../client/activation/types'; import { PythonExtensionChecker } from '../../client/api/pythonApi'; import { ILanguageServerProvider, IPythonDebuggerPathProvider, IPythonExtensionChecker } from '../../client/api/types'; @@ -382,18 +379,6 @@ export class DataScienceIocContainer extends UnitTestIocContainer { // Make sure to disable all command handling during dispose. Don't want // anything to startup again. this.commandManager.dispose(); - try { - // Make sure to delete any temp files written by native editor storage - const globPr = promisify(glob); - const tempLocation = os.tmpdir; - const tempFiles = await globPr(`${tempLocation}/*.ipynb`); - if (tempFiles && tempFiles.length) { - await Promise.all(tempFiles.map((t) => fs.remove(t))); - } - } catch (exc) { - // tslint:disable-next-line: no-console - console.log(`Exception on cleanup: ${exc}`); - } await this.asyncRegistry.dispose(); await super.dispose(); this.disposed = true; diff --git a/src/test/datascience/dataviewer.functional.test.tsx b/src/test/datascience/dataviewer.functional.test.tsx index d10c18e81f6c..5bacad6dff5f 100644 --- a/src/test/datascience/dataviewer.functional.test.tsx +++ b/src/test/datascience/dataviewer.functional.test.tsx @@ -95,10 +95,6 @@ suite('DataScience DataViewer tests', () => { delete (global as any).ascquireVsCodeApi; }); - suiteTeardown(() => { - // asyncDump(); - }); - function createJupyterVariable(variable: string, type: string): IJupyterVariable { return { name: variable, diff --git a/src/test/datascience/debugger.functional.test.tsx b/src/test/datascience/debugger.functional.test.tsx index 1bf829df209d..d1bd283a0f2d 100644 --- a/src/test/datascience/debugger.functional.test.tsx +++ b/src/test/datascience/debugger.functional.test.tsx @@ -129,10 +129,6 @@ suite('DataScience Debugger tests', () => { } }); - suiteTeardown(() => { - // asyncDump(); - }); - async function debugCell( type: 'notebook' | 'interactive', code: string, diff --git a/src/test/datascience/interactiveWindow.functional.test.tsx b/src/test/datascience/interactiveWindow.functional.test.tsx index 684389661d59..2a8f21e29597 100644 --- a/src/test/datascience/interactiveWindow.functional.test.tsx +++ b/src/test/datascience/interactiveWindow.functional.test.tsx @@ -9,6 +9,7 @@ import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { Disposable, Memento, Selection, TextDocument, TextEditor, Uri } from 'vscode'; +import { nbformat } from '@jupyterlab/coreutils'; import { ReactWrapper } from 'enzyme'; import { anything, when } from 'ts-mockito'; import { IApplicationShell, IDocumentManager } from '../../client/common/application/types'; @@ -17,11 +18,12 @@ import { createDeferred, sleep, waitForPromise } from '../../client/common/utils import { noop } from '../../client/common/utils/misc'; import { EXTENSION_ROOT_DIR } from '../../client/constants'; import { generateCellsFromDocument } from '../../client/datascience/cellFactory'; +import { AllowedCellOutputKeys } from '../../client/datascience/common'; import { EditorContexts } from '../../client/datascience/constants'; import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; import { InteractiveWindow } from '../../client/datascience/interactive-window/interactiveWindow'; import { AskedForPerFileSettingKey } from '../../client/datascience/interactive-window/interactiveWindowProvider'; -import { IInteractiveWindowProvider } from '../../client/datascience/types'; +import { IFileSystem, IInteractiveWindowProvider } from '../../client/datascience/types'; import { IInterpreterService } from '../../client/interpreter/contracts'; import { concatMultilineString } from '../../datascience-ui/common'; import { InteractivePanel } from '../../datascience-ui/history-react/interactivePanel'; @@ -121,11 +123,6 @@ suite('DataScience Interactive Window output tests', () => { verifyHtmlOnCell(iw, 'InteractiveCell', html, cellIndex); } - // Uncomment this to debug hangs on exit - // suiteTeardown(() => { - // asyncDump(); - // }); - runTest( 'Simple text', async () => { @@ -685,53 +682,81 @@ for i in range(0, 100): return; } }; - let exportCalled = false; - const appShell = TypeMoq.Mock.ofType(); - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())) - .returns((e) => { - throw e; - }); - appShell - .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve('')); - appShell - .setup((a) => a.showSaveDialog(TypeMoq.It.isAny())) - .returns(() => { - exportCalled = true; - return Promise.resolve(undefined); - }); - appShell.setup((a) => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); - ioc.serviceManager.rebindInstance(IApplicationShell, appShell.object); - - // Make sure to create the interactive window after the rebind or it gets the wrong application shell. - await addCode(ioc, 'a=1\na'); - const { window, mount } = await getOrCreateInteractiveWindow(ioc); - - // Export should cause exportCalled to change to true - const exportPromise = mount.waitForMessage(InteractiveWindowMessages.ReturnAllCells); - window.exportCells(); - await exportPromise; - await sleep(100); // Give time for appshell to come up - assert.equal(exportCalled, true, 'Export is not being called during export'); + const dsfs = ioc.get(IFileSystem); + const tf = await dsfs.createTemporaryLocalFile('.ipynb'); + try { + let exportCalled = false; + const appShell = TypeMoq.Mock.ofType(); + appShell + .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())) + .returns((e) => { + throw e; + }); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve('')); + appShell + .setup((a) => a.showSaveDialog(TypeMoq.It.isAny())) + .returns(() => { + exportCalled = true; + return Promise.resolve(Uri.file(tf.filePath)); + }); + appShell.setup((a) => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); + ioc.serviceManager.rebindInstance(IApplicationShell, appShell.object); + const exportCode = ` +for i in range(100): + time.sleep(0.1) + raise Exception('test') +`; - // Remove the cell - const exportButton = findButton(mount.wrapper, InteractivePanel, 6); - const undo = findButton(mount.wrapper, InteractivePanel, 2); + // Make sure to create the interactive window after the rebind or it gets the wrong application shell. + addMockData(ioc, exportCode, 'NameError', 'type/error', 'error', [ + '\u001b[1;31m---------------------------------------------------------------------------\u001b[0m', + '\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)', + "\u001b[1;32md:\\Source\\Testing_3\\manualTestFile.py\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mi\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m100\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[0mtime\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msleep\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m0.1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 3\u001b[0m \u001b[1;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'test'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;31mNameError\u001b[0m: name 'time' is not defined" + ]); + await addCode(ioc, exportCode); + const { window, mount } = await getOrCreateInteractiveWindow(ioc); - // Now verify if we undo, we have no cells - const afterUndo = await getInteractiveCellResults(ioc, () => { - undo!.simulate('click'); - return Promise.resolve(); - }); + // Export should cause exportCalled to change to true + const exportPromise = mount.waitForMessage(InteractiveWindowMessages.ReturnAllCells); + window.exportCells(); + await exportPromise; + await sleep(100); // Give time for appshell to come up + assert.equal(exportCalled, true, 'Export is not being called during export'); + + // Read file contents into a jupyter structure. Make sure we have only the expected values + const contents = await dsfs.readLocalFile(tf.filePath); + const struct = JSON.parse(contents) as nbformat.INotebookContent; + assert.strictEqual(struct.cells.length, 1, 'Wrong number of cells'); + const outputs = struct.cells[0].outputs as nbformat.IOutput[]; + assert.strictEqual(outputs.length, 1, 'Not correct number of outputs'); + assert.strictEqual(outputs[0].output_type, 'error', 'Error not found'); + const allowedKeys = [...AllowedCellOutputKeys.error]; + const actualKeys = Object.keys(outputs[0]); + assert.deepStrictEqual(allowedKeys, actualKeys, 'Invalid keys in output'); + + // Remove the cell + const exportButton = findButton(mount.wrapper, InteractivePanel, 6); + const undo = findButton(mount.wrapper, InteractivePanel, 2); + + // Now verify if we undo, we have no cells + const afterUndo = await getInteractiveCellResults(ioc, () => { + undo!.simulate('click'); + return Promise.resolve(); + }); - assert.equal(afterUndo.length, 1, 'Undo should remove cells'); + assert.equal(afterUndo.length, 1, 'Undo should remove cells'); - // Then verify we cannot click the button (it should be disabled) - exportCalled = false; - exportButton!.simulate('click'); - await sleep(100); - assert.equal(exportCalled, false, 'Export should not be called when no cells visible'); + // Then verify we cannot click the button (it should be disabled) + exportCalled = false; + exportButton!.simulate('click'); + await sleep(100); + assert.equal(exportCalled, false, 'Export should not be called when no cells visible'); + } finally { + tf.dispose(); + } }, () => { return ioc; diff --git a/src/test/datascience/liveshare.functional.test.tsx b/src/test/datascience/liveshare.functional.test.tsx index 2339dc4ea595..e05a1e68e6d8 100644 --- a/src/test/datascience/liveshare.functional.test.tsx +++ b/src/test/datascience/liveshare.functional.test.tsx @@ -67,10 +67,6 @@ suite('DataScience LiveShare tests', () => { lastErrorMessage = undefined; }); - suiteTeardown(() => { - //asyncDump(); - }); - function createContainer(role: vsls.Role): DataScienceIocContainer { const result = new DataScienceIocContainer(); result.registerDataScienceTypes(); diff --git a/src/test/datascience/mockJupyterManager.ts b/src/test/datascience/mockJupyterManager.ts index 3eed9030c5db..4ac6233ec8c0 100644 --- a/src/test/datascience/mockJupyterManager.ts +++ b/src/test/datascience/mockJupyterManager.ts @@ -293,13 +293,13 @@ export class MockJupyterManager implements IJupyterSessionManager { this.makeActive(interpreter); } - public addError(code: string, message: string) { + public addError(code: string, message: string, traceback?: string[]) { // Turn the message into an nbformat.IError const result: nbformat.IError = { output_type: 'error', ename: message, evalue: message, - traceback: [message] + traceback: traceback ? traceback : [message] }; this.addCell(code, result); diff --git a/src/test/datascience/nativeEditor.functional.test.tsx b/src/test/datascience/nativeEditor.functional.test.tsx index 829e842d27fd..9ee6288812c9 100644 --- a/src/test/datascience/nativeEditor.functional.test.tsx +++ b/src/test/datascience/nativeEditor.functional.test.tsx @@ -52,7 +52,6 @@ import { IMonacoEditorState, MonacoEditor } from '../../datascience-ui/react-com import { waitForCondition } from '../common'; import { createTemporaryFile } from '../utils/fs'; import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { takeSnapshot, writeDiffSnapshot } from './helpers'; import { MockCustomEditorService } from './mockCustomEditorService'; import { MockDocumentManager } from './mockDocumentManager'; import { IMountedWebView, WaitForMessageOptions } from './mountedWebView'; @@ -112,7 +111,6 @@ suite('DataScience Native Editor', () => { [false, true].forEach((useCustomEditorApi) => { //import { asyncDump } from '../common/asyncDump'; - let snapshot: any; suite(`${useCustomEditorApi ? 'With' : 'Without'} Custom Editor API`, () => { function createFileCell(cell: any, data: any): ICell { const newCell = { @@ -134,12 +132,6 @@ suite('DataScience Native Editor', () => { return newCell; } - suiteSetup(() => { - snapshot = takeSnapshot(); - }); - suiteTeardown(() => { - writeDiffSnapshot(snapshot, `Native ${useCustomEditorApi}`); - }); suite('Editor tests', () => { const disposables: Disposable[] = []; let ioc: DataScienceIocContainer; @@ -211,11 +203,6 @@ suite('DataScience Native Editor', () => { } }); - // Uncomment this to debug hangs on exit - // suiteTeardown(() => { - // asyncDump(); - // }); - runMountedTest('Simple text', async () => { // Create an editor so something is listening to messages const { mount } = await createNewEditor(ioc); diff --git a/src/test/datascience/notebook.functional.test.ts b/src/test/datascience/notebook.functional.test.ts index 3192ec9b04ba..3372a35d7ffb 100644 --- a/src/test/datascience/notebook.functional.test.ts +++ b/src/test/datascience/notebook.functional.test.ts @@ -51,7 +51,6 @@ import { generateTestState, ICellViewModel } from '../../datascience-ui/interact import { sleep } from '../core'; import { InterpreterService } from '../interpreters/interpreterService'; import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { takeSnapshot, writeDiffSnapshot } from './helpers'; import { SupportedCommands } from './mockJupyterManager'; import { MockPythonService } from './mockPythonService'; import { createPythonService, startRemoteServer } from './remoteTestHelpers'; @@ -66,7 +65,6 @@ suite('DataScience notebook tests', () => { let ioc: DataScienceIocContainer; let modifiedConfig = false; const baseUri = Uri.file('foo.py'); - let snapshot: any; // tslint:disable-next-line: no-function-expression setup(async function () { @@ -82,14 +80,6 @@ suite('DataScience notebook tests', () => { notebookProvider = ioc.get(INotebookProvider); }); - suiteSetup(() => { - snapshot = takeSnapshot(); - }); - - suiteTeardown(() => { - writeDiffSnapshot(snapshot, `Notebook ${useRawKernel}`); - }); - teardown(async () => { try { if (modifiedConfig) { @@ -214,10 +204,10 @@ suite('DataScience notebook tests', () => { ); const actualMimeType = data.hasOwnProperty(mimeType) ? mimeType : 'text/plain'; assert.ok((data as any)[actualMimeType], `${index}: Cell mime type not correct`); - verifyValue((data as any)[actualMimeType]); + verifyValue(concatMultilineString((data as any)[actualMimeType])); } if (text) { - verifyValue(text); + verifyValue(concatMultilineString(text as any)); } } else if (cellType === 'markdown') { assert.equal(cells[0].data.cell_type, cellType, `${index}: Wrong type of cell returned`); @@ -478,7 +468,7 @@ suite('DataScience notebook tests', () => { runTest('Remote Password', async () => { const pythonService = await createPythonService(ioc); - if (pythonService && !useRawKernel && os.platform() !== 'darwin') { + if (pythonService && !useRawKernel && os.platform() !== 'darwin' && os.platform() !== 'linux') { const configFile = path.join( EXTENSION_ROOT_DIR, 'src', @@ -649,8 +639,12 @@ suite('DataScience notebook tests', () => { assert.ok(/#\s*%%/.test(results), 'No cells in returned import'); assert.ok(!results.includes('tpl'), 'Formatted template with wrong arguments'); } finally { - importer.dispose(); - temp.dispose(); + try { + importer.dispose(); + temp.dispose(); + } catch (exc) { + console.log(exc); + } } }); diff --git a/src/test/datascience/plotViewer.functional.test.tsx b/src/test/datascience/plotViewer.functional.test.tsx index 970d3b15d357..4ce147a5437b 100644 --- a/src/test/datascience/plotViewer.functional.test.tsx +++ b/src/test/datascience/plotViewer.functional.test.tsx @@ -137,10 +137,6 @@ suite('DataScience PlotViewer tests', () => { delete (global as any).ascquireVsCodeApi; }); - suiteTeardown(() => { - // asyncDump(); - }); - async function waitForPlot(wrapper: ReactWrapper, React.Component>, svg: string): Promise { // Get a render promise with the expected number of renders const renderPromise = waitForUpdate(wrapper, MainPanel, 1); diff --git a/src/test/datascience/raw-kernel/rawKernel.functional.test.ts b/src/test/datascience/raw-kernel/rawKernel.functional.test.ts index 930ee2c0ca59..e57cb0658c58 100644 --- a/src/test/datascience/raw-kernel/rawKernel.functional.test.ts +++ b/src/test/datascience/raw-kernel/rawKernel.functional.test.ts @@ -120,13 +120,23 @@ suite('DataScience raw kernel tests', () => { // Hence timeout is a test failure. const longCellExecutionRequest = requestExecute( rawKernel, - 'import time\nfor i in range(300):\n time.sleep(1)', + ` +import time +import sys +for i in range(3000): + sys.stdout.write('.') + sys.stdout.flush() + time.sleep(0.1) + sys.stdout.write('\\\r')`, executionStarted ); // Wait until the execution has started (cuz we cannot interrupt until exec has started). await executionStarted.promise; + // Give it a bit to start running + await sleep(300); + // Then throw the interrupt await rawKernel.interrupt(); diff --git a/src/test/datascience/testHelpers.tsx b/src/test/datascience/testHelpers.tsx index fbd453e00ad4..27f75eaa31ef 100644 --- a/src/test/datascience/testHelpers.tsx +++ b/src/test/datascience/testHelpers.tsx @@ -89,11 +89,12 @@ export function addMockData( code: string, result: string | number | undefined | string[], mimeType?: string | string[], - cellType?: string + cellType?: string, + traceback?: string[] ) { if (ioc.mockJupyter) { if (cellType && cellType === 'error') { - ioc.mockJupyter.addError(code, result ? result.toString() : ''); + ioc.mockJupyter.addError(code, result ? result.toString() : '', traceback); } else { if (result) { ioc.mockJupyter.addCell(code, result, mimeType); diff --git a/src/test/datascience/trustedNotebooks.functional.test.tsx b/src/test/datascience/trustedNotebooks.functional.test.tsx index 302ad724e214..39c6207f15c4 100644 --- a/src/test/datascience/trustedNotebooks.functional.test.tsx +++ b/src/test/datascience/trustedNotebooks.functional.test.tsx @@ -6,6 +6,7 @@ import * as chaiAsPromised from 'chai-as-promised'; import { ReactWrapper } from 'enzyme'; import * as fs from 'fs-extra'; import { Disposable } from 'vscode'; +import { sleep } from '../../client/common/utils/async'; import { noop } from '../../client/common/utils/misc'; import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; import { INotebookEditor, INotebookEditorProvider, ITrustService } from '../../client/datascience/types'; @@ -43,7 +44,7 @@ function waitForMessage(ioc: DataScienceIocContainer, message: string, options?: .waitForMessage(message, options); } // tslint:disable:no-any no-multiline-string -suite('Notebook trust', () => { +suite('DataScience Notebook trust', () => { let wrapper: ReactWrapper, React.Component>; let ne: { editor: INotebookEditor; mount: IMountedWebView }; const disposables: Disposable[] = []; @@ -440,10 +441,14 @@ suite('Notebook trust', () => { // Reopen const newNativeEditor = await openEditor(ioc, baseFile, notebookFile.filePath); const newWrapper = newNativeEditor.mount.wrapper; + assert.ok(newNativeEditor.editor.model.isTrusted, 'Editor did not open as trusted'); + + // Wait a bit. UI doesn't seem to update right away + await sleep(500); // Verify notebook is now trusted const after = newWrapper.find(TrustMessage); - assert.equal(after.text(), 'Trusted'); + assert.equal(after.text(), 'Trusted', 'Notebook UI not reflecting trust state'); }); }); }); diff --git a/src/test/datascience/uiTests/helpers.ts b/src/test/datascience/uiTests/helpers.ts index b6cd14e2ceca..6d2616e419db 100644 --- a/src/test/datascience/uiTests/helpers.ts +++ b/src/test/datascience/uiTests/helpers.ts @@ -44,11 +44,15 @@ export class BaseWebUI implements IAsyncDisposable { private webServer?: IWebServer; private browser?: playwright.ChromiumBrowser; public async dispose() { - while (this.disposables.length) { - this.disposables.shift()?.dispose(); // NOSONAR + try { + while (this.disposables.length) { + this.disposables.shift()?.dispose(); // NOSONAR + } + await this.browser?.close(); + await this.page?.close(); + } catch { + // Failure on dispose doesn't really matter. } - await this.browser?.close(); - await this.page?.close(); } public async type(text: string): Promise { await this.page?.keyboard.type(text); diff --git a/src/test/datascience/uiTests/ipywidget.ui.functional.test.ts b/src/test/datascience/uiTests/ipywidget.ui.functional.test.ts index 7cafcb4c9842..67aeb22c2e92 100644 --- a/src/test/datascience/uiTests/ipywidget.ui.functional.test.ts +++ b/src/test/datascience/uiTests/ipywidget.ui.functional.test.ts @@ -29,7 +29,8 @@ const retryIfFail = (fn: () => Promise) => retryIfFailOriginal(fn, wait use(chaiAsPromised); -[false, true].forEach((useRawKernel) => { +// When using jupyter server, ipywidget tests seem to be a lot flakier. Always use raw kernel +[true].forEach((useRawKernel) => { //import { asyncDump } from '../common/asyncDump'; suite(`DataScience IPyWidgets (${useRawKernel ? 'With Direct Kernel' : 'With Jupyter Server'})`, () => { const disposables: Disposable[] = []; diff --git a/src/test/datascience/variableexplorer.functional.test.tsx b/src/test/datascience/variableexplorer.functional.test.tsx index 8974bc644a2c..4f0df97849af 100644 --- a/src/test/datascience/variableexplorer.functional.test.tsx +++ b/src/test/datascience/variableexplorer.functional.test.tsx @@ -9,7 +9,6 @@ import { Experiments } from '../../client/common/experiments/groups'; import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; import { IJupyterVariable } from '../../client/datascience/types'; import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { takeSnapshot, writeDiffSnapshot } from './helpers'; import { addCode, getOrCreateInteractiveWindow } from './interactiveWindowTestHelpers'; import { addCell, createNewEditor } from './nativeEditorTestHelpers'; import { openVariableExplorer, runDoubleTest, runInteractiveTest, waitForVariablesUpdated } from './testHelpers'; @@ -24,10 +23,8 @@ const rangeInclusive = require('range-inclusive'); const disposables: Disposable[] = []; let ioc: DataScienceIocContainer; let createdNotebook = false; - let snapshot: any; suiteSetup(function () { - snapshot = takeSnapshot(); // These test require python, so only run with a non-mocked jupyter const isRollingBuild = process.env ? process.env.VSCODE_PYTHON_ROLLING !== undefined : false; if (!isRollingBuild) { @@ -37,7 +34,6 @@ const rangeInclusive = require('range-inclusive'); this.skip(); } }); - setup(async () => { ioc = new DataScienceIocContainer(); ioc.setExperimentState(Experiments.RunByLine, runByLine); @@ -60,12 +56,6 @@ const rangeInclusive = require('range-inclusive'); await ioc.dispose(); }); - // Uncomment this to debug hangs on exit - suiteTeardown(() => { - // asyncDump(); - writeDiffSnapshot(snapshot, `Variable Explorer ${runByLine}`); - }); - async function addCodeImpartial( wrapper: ReactWrapper, React.Component>, code: string, diff --git a/src/test/unittests.ts b/src/test/unittests.ts index 65ab07ef721f..87f5619d5668 100644 --- a/src/test/unittests.ts +++ b/src/test/unittests.ts @@ -82,4 +82,13 @@ if (process.argv.indexOf('--fast') === -1) { setupTranspile(); } +exports.mochaHooks = { + afterAll() { + const kernelLauncherMod = require('../client/datascience/kernel-launcher/kernelLauncher'); + + // After all tests run, clean up the kernel launcher mutex files + return kernelLauncherMod.KernelLauncher.cleanupStartPort(); + } +}; + initialize();