diff --git a/package-lock.json b/package-lock.json index 3d3f00d..45b223f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "dotnet-test-explorer", - "version": "0.5.5", + "version": "0.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -52,6 +52,11 @@ "json-schema-traverse": "0.3.1" } }, + "amd-loader": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/amd-loader/-/amd-loader-0.0.8.tgz", + "integrity": "sha1-ECKSgEDlZ+jmpvtYvaBS+BkYkgY=" + }, "ansi-cyan": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", @@ -709,6 +714,7 @@ "anymatch": "2.0.0", "async-each": "1.0.1", "braces": "2.3.2", + "fsevents": "1.2.4", "glob-parent": "3.1.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -1335,6 +1341,11 @@ "map-cache": "0.2.2" } }, + "freeport": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/freeport/-/freeport-1.0.5.tgz", + "integrity": "sha1-JV6KuEFwwzuoXZkOghrl9KGpvF0=" + }, "from": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", @@ -1355,6 +1366,468 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "fsevents": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "optional": true, + "requires": { + "nan": "2.11.1", + "node-pre-gyp": "0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.5.1", + "bundled": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "optional": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "optional": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "bundled": true, + "optional": true, + "requires": { + "safer-buffer": "2.1.2" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "minimatch": "3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "optional": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "minipass": { + "version": "2.2.4", + "bundled": true, + "requires": { + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "minizlib": { + "version": "1.1.0", + "bundled": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "needle": { + "version": "2.2.0", + "bundled": true, + "optional": true, + "requires": { + "debug": "2.6.9", + "iconv-lite": "0.4.21", + "sax": "1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.0", + "bundled": true, + "optional": true, + "requires": { + "detect-libc": "1.0.3", + "mkdirp": "0.5.1", + "needle": "2.2.0", + "nopt": "4.0.1", + "npm-packlist": "1.1.10", + "npmlog": "4.1.2", + "rc": "1.2.7", + "rimraf": "2.6.2", + "semver": "5.5.0", + "tar": "4.4.1" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "optional": true, + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.5" + } + }, + "npm-bundled": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "bundled": true, + "optional": true, + "requires": { + "ignore-walk": "3.0.1", + "npm-bundled": "1.0.3" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "rc": { + "version": "1.2.7", + "bundled": true, + "optional": true, + "requires": { + "deep-extend": "0.5.1", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "optional": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "optional": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "bundled": true, + "optional": true, + "requires": { + "chownr": "1.0.1", + "fs-minipass": "1.2.5", + "minipass": "2.2.4", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true + } + } + }, "fstream": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", @@ -1596,10 +2069,10 @@ "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", + "core-util-is": "1.0.2", + "inherits": "2.0.3", "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "string_decoder": "0.10.31" } }, "string_decoder": { @@ -1614,8 +2087,8 @@ "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", "dev": true, "requires": { - "readable-stream": ">=1.0.33-1 <1.1.0-0", - "xtend": ">=4.0.0 <4.1.0-0" + "readable-stream": "1.0.34", + "xtend": "4.0.1" } } } @@ -2349,16 +2822,6 @@ "mime-db": "1.36.0" } }, - "minimatch": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", - "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", - "dev": true, - "requires": { - "lru-cache": "2.7.3", - "sigmund": "1.0.1" - } - }, "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", @@ -2418,6 +2881,16 @@ "inherits": "2.0.3", "minimatch": "0.3.0" } + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } } } }, @@ -2449,6 +2922,12 @@ } } }, + "nan": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", + "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==", + "optional": true + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -3803,6 +4282,11 @@ "integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=", "dev": true }, + "typescript-collections": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/typescript-collections/-/typescript-collections-1.3.2.tgz", + "integrity": "sha512-Frfvtwym0VebbueXWEJlVkGiWjKEFStsRwusuzjh8lX8OEJ9ZbFqpYLNfPvZcxw/+nqW0cRNBeBq6SVoTjymcQ==" + }, "union-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", diff --git a/package.json b/package.json index 4fe92b5..944276a 100644 --- a/package.json +++ b/package.json @@ -274,6 +274,9 @@ "fkill": "^5.3.0", "glob": "^7.1.2", "ts-node": "^7.0.1", - "xmldom": "^0.1.27" + "xmldom": "^0.1.27", + "typescript-collections": "^1.3.2", + "freeport": "^1.0.5", + "amd-loader": "^0.0.8" } } diff --git a/src/dotnetTestExplorer.ts b/src/dotnetTestExplorer.ts index 83b6176..93745b1 100644 --- a/src/dotnetTestExplorer.ts +++ b/src/dotnetTestExplorer.ts @@ -10,6 +10,7 @@ import { TestNode } from "./testNode"; import { ITestResult, TestResult } from "./testResult"; import { TestResultsFile } from "./testResultsFile"; import { Utility } from "./utility"; +import { TestManager } from "./vsTestPlatform/vsCode/vsTest/vsTestManager"; export class DotnetTestExplorer implements TreeDataProvider { @@ -39,6 +40,16 @@ export class DotnetTestExplorer implements TreeDataProvider { public refreshTestExplorer(): void { this.testCommands.discoverTests(); + // VTP testing code + TestManager.initialize(this.context, vscode.workspace.rootPath).then(() => { + // this.isTestExplorerInitialized = true; + // this._onDidChangeTreeData.fire(); + const testManagerInstance = TestManager.getInstance(); + const testService = testManagerInstance.getTestService(); + Logger.Log("Test Manager initialised", "vsTest"); + this.testCommands.vsDiscoverTests(testService); + }); + AppInsightsClient.sendEvent("refreshTestExplorer"); } diff --git a/src/extension.ts b/src/extension.ts index e72210f..3991ed9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -17,6 +17,7 @@ import { TestNode } from "./testNode"; import { TestResultsFile } from "./testResultsFile"; import { TestStatusCodeLensProvider } from "./testStatusCodeLensProvider"; import { Utility } from "./utility"; +import { TestManager } from "./vsTestPlatform/vsCode/vsTest/vsTestManager"; import { Watch } from "./watch"; export function activate(context: vscode.ExtensionContext) { @@ -56,6 +57,15 @@ export function activate(context: vscode.ExtensionContext) { testCommands.discoverTests(); + TestManager.initialize(this.context, vscode.workspace.rootPath).then(() => { + // this.isTestExplorerInitialized = true; + // this._onDidChangeTreeData.fire(); + const testManagerInstance = TestManager.getInstance(); + const testService = testManagerInstance.getTestService(); + Logger.Log("Test Manager initialised", "vsTest"); + testCommands.vsDiscoverTests(testService); + }); + const codeLensProvider = new TestStatusCodeLensProvider(testCommands); context.subscriptions.push(codeLensProvider); context.subscriptions.push(vscode.languages.registerCodeLensProvider( @@ -83,7 +93,7 @@ export function activate(context: vscode.ExtensionContext) { })); context.subscriptions.push(vscode.commands.registerTextEditorCommand("dotnet-test-explorer.runTestInContext", (editor: vscode.TextEditor) => { - findTestInContext.find(editor.document, editor.selection.start).then( (testRunContext) => { + findTestInContext.find(editor.document, editor.selection.start).then((testRunContext) => { testCommands.runTestByName(testRunContext.testName, testRunContext.isSingleTest); }); })); diff --git a/src/testCommands.ts b/src/testCommands.ts index 65661e8..ed54b86 100644 --- a/src/testCommands.ts +++ b/src/testCommands.ts @@ -1,4 +1,5 @@ import * as path from "path"; +import * as vscode from "vscode"; import { commands, Event, EventEmitter } from "vscode"; import { AppInsightsClient } from "./appInsightsClient"; import { Executor } from "./executor"; @@ -9,6 +10,7 @@ import { TestNode } from "./testNode"; import { ITestResult, TestResult } from "./testResult"; import { TestResultsFile } from "./testResultsFile"; import { Utility } from "./utility"; +import { VSTestServiceIDE } from "./vsTestPlatform/vsCode/vsTest/vsTestServiceIDE"; export interface ITestRunContext { testName: string; @@ -39,7 +41,7 @@ export class TestCommands { try { for (const dir of this.testDirectories.getTestDirectories()) { const testsForDir: IDiscoverTestsResult = await discoverTests(dir, Utility.additionalArgumentsOption); - this.testDirectories.addTestsForDirectory(testsForDir.testNames.map( (tn) => ({dir, name: tn}))); + this.testDirectories.addTestsForDirectory(testsForDir.testNames.map((tn) => ({ dir, name: tn }))); discoveredTests.push(testsForDir); } @@ -93,6 +95,23 @@ export class TestCommands { } } + public vsDiscoverTests(testService: VSTestServiceIDE) { + const date1 = new Date(); + Logger.Log(date1.toLocaleString() + " - Start discovery VTP2"); + testService.discoveryTests(vscode.workspace.rootPath).then((result) => { + if (result) { + Logger.Log(result.TotalTests.toString() + " tests discovered using VTP"); + const date2 = new Date(); + Logger.Log(date1.toLocaleString() + " - End discovery VTP2 in " + (date2.getMilliseconds() - date1.getMilliseconds()).toString() + "ms"); + Logger.Log(""); + // this._onDidChangeTreeData.fire(); + } else { + // tslint:disable-next-line:no-console + console.info("vsTestPlatform: No tests discovered."); + } + }); + } + private runTestCommand(testName: string, isSingleTest: boolean): void { commands.executeCommand("workbench.view.extension.test", "workbench.view.extension.test"); @@ -107,12 +126,20 @@ export class TestCommands { const runSeq = async () => { try { + const date1 = new Date(); + Logger.Log(date1.toLocaleString() + " - Start execution via cli"); + for (let i = 0; i < testDirectories.length; i++) { testResults.push(await this.runTestCommandForSpecificDirectory(testDirectories[i], testName, isSingleTest, i)); } const merged = [].concat(...testResults); - this.sendNewTestResults({ testName, testResults: merged}); + + const date2 = new Date(); + Logger.Log(date2.toLocaleString() + " - End execution via cli in " + (date2.getMilliseconds() - date1.getMilliseconds()).toString() + "ms"); + Logger.Log(""); + + this.sendNewTestResults({ testName, testResults: merged }); } catch (err) { Logger.Log(`Error while executing test command: ${err}`); this.discoverTests(); @@ -122,7 +149,7 @@ export class TestCommands { runSeq(); } - private runBuildCommandForSpecificDirectory(testDirectoryPath: string): Promise { + private runBuildCommandForSpecificDirectory(testDirectoryPath: string): Promise { return new Promise((resolve, reject) => { Logger.Log(`Executing dotnet build in ${testDirectoryPath}`); @@ -140,7 +167,7 @@ export class TestCommands { const trxTestName = index + ".trx"; - const textContext = {testName, isSingleTest}; + const textContext = { testName, isSingleTest }; return new Promise((resolve, reject) => { const testResultFile = path.join(Utility.pathForResultFile, "test-explorer", trxTestName); @@ -159,7 +186,7 @@ export class TestCommands { this.onTestRunEmitter.fire(textContext); this.runBuildCommandForSpecificDirectory(testDirectoryPath) - .then( () => { + .then(() => { Logger.Log(`Executing ${command} in ${testDirectoryPath}`); return Executor.exec(command, (err, stdout: string) => { @@ -171,12 +198,12 @@ export class TestCommands { Logger.Log(stdout); - this.resultsFile.parseResults(testResultFile).then( (result) => { + this.resultsFile.parseResults(testResultFile).then((result) => { resolve(result); }); }, testDirectoryPath, true); }) - .catch( (err) => { + .catch((err) => { reject(err); }); }); diff --git a/src/testDiscovery.ts b/src/testDiscovery.ts index 0504090..91ae2ec 100644 --- a/src/testDiscovery.ts +++ b/src/testDiscovery.ts @@ -47,6 +47,9 @@ function executeDotnetTest(testDirectoryPath: string, dotnetTestOptions: string) return new Promise((resolve, reject) => { const command = `dotnet test -t -v=q${dotnetTestOptions}`; + const date1 = new Date(); + Logger.Log(date1.toLocaleString() + " - Start discovery via cli"); + Logger.Log(`Executing ${command} in ${testDirectoryPath}`); Executor.exec(command, (err: Error, stdout: string, stderr: string) => { @@ -56,9 +59,12 @@ function executeDotnetTest(testDirectoryPath: string, dotnetTestOptions: string) reject(err); return; } - + const date2 = new Date(); + Logger.Log(date2.toLocaleString() + " - End discovery via cli in " + (date2.getMilliseconds() - date1.getMilliseconds()).toString() + "ms"); + Logger.Log(""); resolve(stdout); }, testDirectoryPath); + }); } diff --git a/src/vsTestPlatform/README.md b/src/vsTestPlatform/README.md new file mode 100644 index 0000000..7e9195c --- /dev/null +++ b/src/vsTestPlatform/README.md @@ -0,0 +1,87 @@ +# Evaluation TODO + +- [x] Get vstest platform code minimally working + - [x] convert spawn to macOS + - [x] vstest server spawns + - [x] can make version request and get response successfully +- [x] Add license from source to all files +- [x] Test discovery works and can see result data in debug mode + - [x] Mstest project + - [x] Nunit project + - [x] Xunit projet +- [ ] Test execution works and can see result data in debug mode + - [ ] Mstest project + - [ ] Nunit project + - [ ] Xunit project +- [ ] Test discovery performs better than running `dotnet vstest` and parsing TRX? + - with a 100+ tests, Discovery seem to be slightly (20-30ms) quicker for the first run. Refershes are quicker with the cli. Not sure why. VTP is the same-ish as the first run. +- [ ] Test exection performs better than running with the cli +- [x] Check if test **discovery** results data schema are the smae across test frameworks for key data? + - displayName value is FQN for Xunit but not for Nunit and Mstest. See examples below. Believe this is the same as TRX. +- [ ] Check if test **execution** results data schema are the smae across test frameworks for key data? +- [ ] Check what new data do we have available? +- [ ] Can it handle multiple test projects in a single workspace? + +## additional npm dependencies + + "typescript-collections": "^1.3.2", + "freeport": "^1.0.5", + "amd-loader": "^0.0.8" + +## Configuring the test project + +This is currently required manual editting of `/vsTestPlatform/vsCode/config.ts` + +## Check before shipping TODO + +- Works cross platform +- Unit tests for VS Test Platform +- Auto discover config for execution host i.e. what is currently hardcoded in config.ts +- TSLint currently ignores **/src/vsTestPlatform/**/**.*. Undo and make TSLint happy (there'll be a lot). Leave winbase* excluded. + +## Test Discovery data responses + +The following are key/value from the json + +### Nunit + +```javascript +isRunning:false +plainObject:Object +fullyQualifiedName:"NunitTests.TestClass1.AnotherPass" +executorUri:"executor://NUnit3TestExecutor" +source:"/Users/janaka/code-projects/vscode-extentions/vscode-dotnet-test-explorer/test/nunit/bin/debug/netcoreapp2.0/NunitTests.dll" +codeFilePath:"/Users/janaka/code-projects/vscode-extentions/vscode-dotnet-test-explorer/test/nunit/TestClass1.cs" +displayName:"AnotherPass" +id:"8b95092a-43d6-e6c1-8755-e9886078ed34" +lineNumber:18 +``` + +### Xunit + +```javascript +isRunning:false +plainObject:Object +fullyQualifiedName:"XunitTests.TestClass1.Pass" +executorUri:"executor://xunit/VsTestRunner2/netcoreapp" +source:"/Users/janaka/code-projects/vscode-extentions/vscode-dotnet-test-explorer/test/xunittests/bin/debug/netcoreapp2.0/XunitTests.dll" +codeFilePath:"/Users/janaka/code-projects/vscode-extentions/vscode-dotnet-test-explorer/test/xunittests/TestClass1.cs" +displayName:"XunitTests.TestClass1.Pass" +id:"bd43f0f0-1d15-11da-f753-733a2d22cb4b" +lineNumber:11 +``` + +### Mstest + +```javascript +isRunning:false +plainObject:Object +fullyQualifiedName:"MsTestTests.TestClass1.Pass" +executorUri:"executor://MSTestAdapter/v2" +source:"/Users/janaka/code-projects/vscode-extentions/vscode-dotnet-test-explorer/test/mstest/bin/debug/netcoreapp2.0/MsTestTests.dll" +codeFilePath:"/Users/janaka/code-projects/vscode-extentions/vscode-dotnet-test-explorer/test/mstest/TestClass1.cs" +displayName:"Pass" +id:"5736fbf0-a673-f686-0c30-35bd962d1f20" +lineNumber:12 +testClassName:"MsTestTests.TestClass1" +``` \ No newline at end of file diff --git a/src/vsTestPlatform/vsCode/commands/testDiscoveryCommands.ts b/src/vsTestPlatform/vsCode/commands/testDiscoveryCommands.ts new file mode 100644 index 0000000..7e3882b --- /dev/null +++ b/src/vsTestPlatform/vsCode/commands/testDiscoveryCommands.ts @@ -0,0 +1,19 @@ +import * as vscode from "vscode"; +import { VSTestServiceIDE } from "../vsTest/vsTestServiceIDE" +import { TestManager } from "../vsTest/vsTestManager"; +import { TestModel, Test, TestResult, TestOutcome } from "../../vsTest/vsTestModel" + +export function RegisterTestDiscoveryCommands(context: vscode.ExtensionContext) { + new TestDiscoveryCommands(context); +} + +export class TestDiscoveryCommands { + private testService: VSTestServiceIDE; + constructor(private context: vscode.ExtensionContext) { + //this.testService = TestManager.getInstance().getTestService(); + + //const runCommand = vscode.commands.registerCommand("vstest.execution.run", + // test => this.runTests([test])); + //context.subscriptions.push(runCommand); + } +} \ No newline at end of file diff --git a/src/vsTestPlatform/vsCode/commands/testExecutionCommands.ts b/src/vsTestPlatform/vsCode/commands/testExecutionCommands.ts new file mode 100644 index 0000000..c1aa1d9 --- /dev/null +++ b/src/vsTestPlatform/vsCode/commands/testExecutionCommands.ts @@ -0,0 +1,32 @@ +import * as vscode from "vscode"; +import { VSTestServiceIDE } from "../vsTest/vsTestServiceIDE" +import { TestManager } from "../vsTest/vsTestManager"; +import { TestModel, Test, TestResult, TestOutcome } from "../../vsTest/vsTestModel" + +export function RegisterTestExecutionCommands(context: vscode.ExtensionContext) { + new TestExecutionCommands(context); +} + +export class TestExecutionCommands { + private testService: VSTestServiceIDE; + constructor(private context: vscode.ExtensionContext) { + //this.testService = TestManager.getInstance().getTestService(); + + + + //const runAllCommand = vscode.commands.registerCommand("vstest.execution.runAll", + // test => this.runTests(this.testService.getModel().getTests())); + //context.subscriptions.push(runAllCommand); + + //const debugAllCommand = vscode.commands.registerCommand("vstest.execution.debugAll", + // test => this.runTests(this.testService.getModel().getTests(), true)); + //context.subscriptions.push(debugAllCommand); + } + + + runTests(test: Array, debuggingEnabled: boolean = false): void { + this.testService.runTests(test, debuggingEnabled); + } + + +} \ No newline at end of file diff --git a/src/vsTestPlatform/vsCode/config.ts b/src/vsTestPlatform/vsCode/config.ts new file mode 100644 index 0000000..9d2fc75 --- /dev/null +++ b/src/vsTestPlatform/vsCode/config.ts @@ -0,0 +1,31 @@ +import * as vscode from "vscode"; +import { IVSTestConfig } from "../vsTest/vsTestConfig"; + +export function getCurrentAdapterName() { + // return vscode.workspace.getConfiguration("vstest.adapterName"); + return "dotnet"; +} + +export function getConfigurationForAdatper(): IVSTestConfig { + // return vscode.workspace.getConfiguration(`vstest.${getCurrentAdapterName()}`) as any; + // FIXME: fix hardcoded config. Why do we need these? Should be able to deduce these values. + return { + "output": "bin/debug", // isn't this the convention + "framework": "netcoreapp2.0", // used to construct path. can come from *.test.csproj + // "outputFileName": "NunitTests.dll" + "outputFileName": "XunitTests.dll" + //"outputFileName": "MsTestTests.dll" + } +} + +export function isExtensionEnabled(): boolean { + const configuration = vscode.workspace.getConfiguration(`vstest`); + const value = configuration.get("enable"); + return value == true || !value; +} + +export function isAutoInitializeEnabled(): boolean { + const configuration = vscode.workspace.getConfiguration(`vstest`); + const value = configuration.get("autoInitialize"); + return value == true; +} \ No newline at end of file diff --git a/src/vsTestPlatform/vsCode/console/testOutputChannel.ts b/src/vsTestPlatform/vsCode/console/testOutputChannel.ts new file mode 100644 index 0000000..96cc9a5 --- /dev/null +++ b/src/vsTestPlatform/vsCode/console/testOutputChannel.ts @@ -0,0 +1,9 @@ +import * as vscode from "vscode"; + +export class TestOutputChannel { + private outputChannel = vscode.window.createOutputChannel("vsTest"); + + public appendData(value : string) { + this.outputChannel.appendLine(value); + } +} \ No newline at end of file diff --git a/src/vsTestPlatform/vsCode/testTreeDataProvider/groupByFilter.ts b/src/vsTestPlatform/vsCode/testTreeDataProvider/groupByFilter.ts new file mode 100644 index 0000000..aa4a19d --- /dev/null +++ b/src/vsTestPlatform/vsCode/testTreeDataProvider/groupByFilter.ts @@ -0,0 +1,68 @@ +import * as vscode from "vscode"; + +export enum GroupByQuickPickItemType { + Duration, + Outcome, + Class, +} + +class GroupByQuickPickItem implements vscode.QuickPickItem { + type: GroupByQuickPickItemType; + /** + * A human readable string which is rendered prominent. + */ + label: string; + + /** + * A human readable string which is rendered less prominent. + */ + description: string; + + /** + * A human readable string which is rendered less prominent. + */ + detail?: string; + + constructor(type, label, description, detail?) { + this.type = type; + this.label = label; + this.description = description; + this.detail = detail; + } +} + +export class GroupByFilter { + private items: Array = new Array(); + + private selected: GroupByQuickPickItem; + + constructor() { + const groupByOutcome = new GroupByQuickPickItem(GroupByQuickPickItemType.Outcome, "Outcome", "Groups tests by execution results: Failed Tests, Skipped Tests, Passed Tests.") + this.items.push(groupByOutcome); + this.items.push(new GroupByQuickPickItem(GroupByQuickPickItemType.Duration, "Duration", "Groups test by execution time: Fast, Medium, and Slow.")); + this.items.push(new GroupByQuickPickItem(GroupByQuickPickItemType.Class, "Class", "Groups tests by method class")); + + this.selected = groupByOutcome; + } + + getQuickPickOptions(): vscode.QuickPickOptions { + return { placeHolder: "Select how you would like to group you test files" }; + } + + show(): Promise { + + return new Promise((resolve, reject) => { + vscode.window.showQuickPick(this.items, this.getQuickPickOptions()).then((value: GroupByQuickPickItem) => { + if (value != null) { + this.selected = value; + } + return resolve(this.selected); + }); + }); + } + + getSelected(): GroupByQuickPickItem { + return this.selected; + } + +} \ No newline at end of file diff --git a/src/vsTestPlatform/vsCode/testTreeDataProvider/testTreeDataProvider.ts b/src/vsTestPlatform/vsCode/testTreeDataProvider/testTreeDataProvider.ts new file mode 100644 index 0000000..42cd65d --- /dev/null +++ b/src/vsTestPlatform/vsCode/testTreeDataProvider/testTreeDataProvider.ts @@ -0,0 +1,507 @@ +import * as vscode from "vscode"; +import * as Utils from "../Utils"; +import * as Collections from "typescript-collections"; +import { TestManager } from "../vsTest/vsTestManager"; +import { TestModel, Test, TestResult, TestOutcome } from "../../vsTest/vsTestModel" +import { VSTestServiceIDE } from "../vsTest/vsTestServiceIDE" +import { VSTestServiceStatus } from "../../vsTest/vsTestService" +import { getConfigurationForAdatper, getCurrentAdapterName, isAutoInitializeEnabled } from "../config"; +import { GroupByFilter, GroupByQuickPickItemType } from "./groupByFilter" + +export function RegisterVSTestTreeProvider(context: vscode.ExtensionContext) { + let testTreeDataProvider: TestTreeDataProvider; + testTreeDataProvider = new TestTreeDataProvider(context); + vscode.window.registerTreeDataProvider("vstest.explorer.vsTestTree", testTreeDataProvider); +} + +/** + * A class to handle the group lables of the tests + */ +class Label { + private tests: Array; + + private displayName: string; + + private outcome: TestOutcome; + + + + constructor(displayName: string, outcome: TestOutcome, tests: Array = null) { + this.displayName = displayName; + this.tests = tests; + this.outcome = outcome; + } + + public getChildrenLenght(): number { + return this.getChildren() ? this.getChildren().length : 0; + } + + public getDisplayName() { + return `${this.displayName} (${this.getChildrenLenght()})`; + } + + public setTests(tests: Array) { + this.tests = tests; + } + + public getChildren(): Array { + return this.tests; + } + + public getOutcome(): TestOutcome { + return this.outcome; + } + + public getId(): string { + return this.displayName; + } +} + +/** + * Type that the tree provider handles + */ +type TestTreeType = Label | Test; + +/** + * Additional data to help the tree data provider + */ +class TestAdditionalData { + collapsibleState: vscode.TreeItemCollapsibleState; +} + +export class TestTreeDataProvider implements vscode.TreeDataProvider { + public _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + private groupByFilter: GroupByFilter = new GroupByFilter(); + + private testService: VSTestServiceIDE; + + private testsAdditionalData: Collections.Dictionary = new Collections.Dictionary(); + + private selectedItem: TestTreeType = null; + + + + private isTestExplorerInitialized = false; + + constructor(private context: vscode.ExtensionContext) { + + const disposable = vscode.commands.registerCommand("vstest.explorer.open", event => this.goToTestLocation(event)); + context.subscriptions.push(disposable); + + const runCommand = vscode.commands.registerCommand("vstest.execution.runSelected", + (item) => { + if (item) { + this.runTests(item); + } + else { + this.runTests(this.selectedItem); + } + }); + context.subscriptions.push(runCommand); + + const debugCommand = vscode.commands.registerCommand("vstest.execution.debugSelected", + (item) => { + if (item) { + this.debugTests(item); + } + else { + this.debugTests(this.selectedItem); + } + }); + context.subscriptions.push(debugCommand); + + const restartExplorerCommand = vscode.commands.registerCommand("vstest.explorer.restart", + () => this.restart()); + context.subscriptions.push(restartExplorerCommand); + + const groupByExplorerCommand = vscode.commands.registerCommand("vstest.explorer.groupBy", + () => this.selectGroupBy()); + context.subscriptions.push(groupByExplorerCommand); + + const refreshExplorerCommand = vscode.commands.registerCommand("vstest.explorer.refresh", + () => this.discoveryTests()); + context.subscriptions.push(refreshExplorerCommand); + + const runAllTestCommand = vscode.commands.registerCommand("vstest.execution.runAll", + () => this.runAllTests()); + context.subscriptions.push(runAllTestCommand); + + const showTestResult = vscode.commands.registerCommand("vstest.explorer.showResult", event => this.showTestResult(event)); + context.subscriptions.push(showTestResult); + + const initializeTestExplorer = vscode.commands.registerCommand("vstest.explorer.initialize", event => this.initialize()); + context.subscriptions.push(showTestResult); + + vscode.workspace.onDidChangeConfiguration(() => { + this.testService.updateConfiguration(getCurrentAdapterName(), getConfigurationForAdatper()); + this.registerTestModelListeners(); + this.refrehTestExplorer(null); + this.discoveryTests(); + }); + + if (isAutoInitializeEnabled()) { + this.initialize(); + } + } + + private restart(): void { + this.isTestExplorerInitialized = false; + TestManager.getInstance().restart(vscode.workspace.rootPath).then(() => { + this.testService = TestManager.getInstance().getTestService(); + this.isTestExplorerInitialized = true; + this.registerTestServiceListeners(); + this.registerTestModelListeners(); + this.refrehTestExplorer(null); + this.discoveryTests(); + }); + } + + private discoveryTests() { + this.testService.discoveryTests(vscode.workspace.rootPath).then((result) => { + if (result) { + this._onDidChangeTreeData.fire(); + } + else { + } + }); + } + + private _createLaunchConfiguration(program: string, args: string, cwd: string, debuggerEventsPipeName: string) { + let debugOptions = vscode.workspace.getConfiguration('csharp').get('unitTestDebuggingOptions'); + + // Get the initial set of options from the workspace setting + let result: any; + if (typeof debugOptions === "object") { + // clone the options object to avoid changing it + result = JSON.parse(JSON.stringify(debugOptions)); + } else { + result = {}; + } + + if (!result.type) { + result.type = "coreclr"; + } + + // Now fill in the rest of the options + result.name = ".NET Test Launch"; + result.request = "launch"; + result.debuggerEventsPipeName = debuggerEventsPipeName; + result.program = program; + result.args = args; + result.cwd = cwd; + + return result; + } + + private registerTestServiceListeners() { + this.testService = TestManager.getInstance().getTestService(); + + /*this.testService.onDidTestServiceStatusChanged((status) => { + if (status == VSTestServiceStatus.Connected) { + this.isTestExplorerInitialized = true; + this._onDidChangeTreeData.fire(); + //this.discoveryTests(); + } + });*/ + + this.testService.onDidTestServiceDebugLaunchRequest((event) => { + const response = event.Payload; + const config = this._createLaunchConfiguration(response.FileName, response.Arguments, response.WorkingDirectory, null); + vscode.commands.executeCommand('vscode.startDebug', config) + }); + + + } + + private registerTestModelListeners() { + this.testService.getModel().onDidTestChanged((test: Test) => { + this._onDidChangeTreeData.fire(); + //this.refrehTestExplorer(null); + }); + } + + private initialize() { + // only allow to initialize if not initialized yet + if (this.isTestExplorerInitialized) { + return; + } + // initilize the test manager + TestManager.initialize(this.context, vscode.workspace.rootPath).then(() => { + this.isTestExplorerInitialized = true; + this._onDidChangeTreeData.fire(); + this.discoveryTests(); + }); + + this.registerTestServiceListeners(); + this.registerTestModelListeners(); + } + + private runTests(test: TestTreeType, debuggingEnabled: boolean = false): void { + if (!test) { + vscode.window.showWarningMessage("You need to select a test or a test group first."); + } + if (test instanceof Test) { + this.testService.runTests([test], debuggingEnabled); + } + else if (test instanceof Label) { + this.testService.runTests((