diff --git a/src/testing/coverage.ts b/src/testing/coverage.ts index 00e7ef274..919c9d779 100644 --- a/src/testing/coverage.ts +++ b/src/testing/coverage.ts @@ -1,13 +1,39 @@ import IBMi from "../api/IBMi"; +export type CollectorGroup = CoverageCollector[]; +export interface CapturedMethods { [key: string]: number }; + +const IGNORE_METHODS = [`constructor`]; + export class CoverageCollector { - private methodNames: string[]; - private captured: { [key: string]: number } = {}; - constructor(private instanceClass: T) { - // T is a class, so get a list of methods - const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(instanceClass)); + private name: string = `Unknown`; + private methodNames: string[] = []; + private captured: CapturedMethods = {}; + constructor(private instanceClass: T, fixedName?: string) { + if ('constructor' in (instanceClass as object)) { + this.name = (instanceClass as object).constructor.name; + } + + const isObject = this.name === `Object`; + let methods = []; + + if (isObject) { + // T is an object, so get a list of methods + + if (!fixedName) { + throw new Error(`CoverageCollector: Object must have a fixed name`); + } + + this.name = fixedName; + methods = Object.keys(instanceClass as object); + this.methodNames = methods.filter(prop => IGNORE_METHODS.includes(prop) === false && typeof instanceClass[prop as keyof T] === 'function'); + + } else { + // T is a class, so get a list of methods + methods = Object.getOwnPropertyNames(Object.getPrototypeOf(instanceClass)); - this.methodNames = methods.filter(prop => typeof instanceClass[prop as keyof T] === 'function'); + this.methodNames = methods.filter(prop => IGNORE_METHODS.includes(prop) === false && typeof instanceClass[prop as keyof T] === 'function'); + } for (const func of this.methodNames) { this.captured[func] = 0; @@ -25,4 +51,24 @@ export class CoverageCollector { } } } + + getName() { + return this.name; + } + + reset() { + for (const method of this.methodNames) { + this.captured[method] = 0; + } + } + + getPercentCoverage() { + const totalMethods = this.methodNames.length; + const capturedMethods = Object.keys(this.captured).filter(method => this.captured[method] > 0).length; + return Math.round((capturedMethods / totalMethods) * 100); + } + + getCoverage() { + return this.captured; + } } \ No newline at end of file diff --git a/src/testing/index.ts b/src/testing/index.ts index b8bf8f70b..841d3f96e 100644 --- a/src/testing/index.ts +++ b/src/testing/index.ts @@ -15,9 +15,10 @@ import { StorageSuite } from "./storage"; import { TestSuitesTreeProvider } from "./testCasesTree"; import { ToolsSuite } from "./tools"; import { Server } from "../typings"; -import { CoverageCollector } from "./coverage"; +import { CollectorGroup, CoverageCollector } from "./coverage"; import IBMi from "../api/IBMi"; import IBMiContent from "../api/IBMiContent"; +import { Tools } from "../api/Tools"; const suites: TestSuite[] = [ ActionSuite, @@ -71,10 +72,7 @@ const testSuitesSimultaneously = env.simultaneous === `true`; const testIndividually = env.individual === `true`; const testSpecific = env.specific; -const coverages: { - 'Connection': CoverageCollector|undefined, - 'Content': CoverageCollector|undefined -} = { Connection: undefined, Content: undefined }; +let coverages: CollectorGroup = []; let testSuitesTreeProvider: TestSuitesTreeProvider; export function initialise(context: vscode.ExtensionContext) { @@ -83,13 +81,21 @@ export function initialise(context: vscode.ExtensionContext) { instance.subscribe(context, 'connected', 'Run tests', () => { if (instance.getConnection()) { - coverages.Connection = new CoverageCollector(instance.getConnection()!); + coverages.push(new CoverageCollector(instance.getConnection()!)); } if (instance.getContent()) { - coverages.Content = new CoverageCollector(instance.getContent()!); + coverages.push(new CoverageCollector(instance.getContent()!)); } + if (instance.getContent()) { + coverages.push(new CoverageCollector(Tools, `Tools`)); + } + + coverages.forEach(c => c.reset()); + + testSuitesTreeProvider.refresh(); + if (!testIndividually) { if (configuringFixture) { console.log(`Not running tests as configuring fixture`); @@ -100,7 +106,7 @@ export function initialise(context: vscode.ExtensionContext) { }); instance.subscribe(context, 'disconnected', 'Reset tests', resetTests); - testSuitesTreeProvider = new TestSuitesTreeProvider(suites); + testSuitesTreeProvider = new TestSuitesTreeProvider(suites, coverages); context.subscriptions.push( vscode.window.createTreeView("testingView", { treeDataProvider: testSuitesTreeProvider, showCollapseAll: true }), vscode.commands.registerCommand(`code-for-ibmi.testing.specific`, (suiteName: string, testName: string) => { @@ -266,7 +272,7 @@ async function runTest(test: TestCase) { if (connection) { console.log(`Running ${test.name}`); test.status = "running"; - testSuitesTreeProvider.refresh(test); + testSuitesTreeProvider.refresh(); const start = +(new Date()); try { await test.test(); @@ -280,7 +286,7 @@ async function runTest(test: TestCase) { } finally { test.duration = +(new Date()) - start; - testSuitesTreeProvider.refresh(test); + testSuitesTreeProvider.refresh(); } } else { test.status = undefined; @@ -290,6 +296,7 @@ async function runTest(test: TestCase) { } function resetTests() { + coverages.splice(0, coverages.length); suites.flatMap(ts => ts.tests).forEach(tc => { tc.status = undefined; tc.failure = undefined; diff --git a/src/testing/testCasesTree.ts b/src/testing/testCasesTree.ts index 5e0106669..3b1fb69df 100644 --- a/src/testing/testCasesTree.ts +++ b/src/testing/testCasesTree.ts @@ -1,40 +1,96 @@ import vscode from "vscode"; import { TestCase, TestSuite } from "."; +import { CollectorGroup, CoverageCollector } from "./coverage"; -type TestObject = TestSuite | TestCase; +class CoolTreeItem extends vscode.TreeItem { + constructor(readonly label: string, readonly collapsibleState: vscode.TreeItemCollapsibleState = vscode.TreeItemCollapsibleState.None) { + super(label, collapsibleState); + } + + getChildren?(): Thenable; +} -export class TestSuitesTreeProvider implements vscode.TreeDataProvider{ - private readonly emitter: vscode.EventEmitter = new vscode.EventEmitter(); - readonly onDidChangeTreeData: vscode.Event = this.emitter.event; +export class TestSuitesTreeProvider implements vscode.TreeDataProvider{ + private readonly emitter: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this.emitter.event; - constructor(readonly testSuites: TestSuite[]) { + constructor(readonly testSuites: TestSuite[], readonly coverageCollection: CollectorGroup) {} + refresh(element?: CoolTreeItem|TestSuite) { + this.emitter.fire(); } - refresh(element?: TestObject) { - this.emitter.fire(element); + getTreeItem(element: CoolTreeItem): vscode.TreeItem | Thenable { + return element; } - getTreeItem(element: TestObject): vscode.TreeItem | Thenable { - if("tests" in element){ - return new TestSuiteItem(element); - } - else{ - return new TestCaseItem(this.testSuites.find(ts => ts.tests.includes(element))!, element); + getChildren(element?: CoolTreeItem): vscode.ProviderResult { + if (element && element.getChildren) { + return element.getChildren(); + } else { + return [ + new CoverageListItem(this.coverageCollection), + new TestSuitesItem(this.testSuites) + ] } } +} + +class CoverageListItem extends CoolTreeItem { + constructor(readonly coverages: CollectorGroup) { + super("Coverage", vscode.TreeItemCollapsibleState.Expanded); + } + + async getChildren() { + return this.coverages.map(collector => new CoverageCollectionItem(collector)); + } +} - getChildren(element?: TestObject): vscode.ProviderResult { - if (element && "tests" in element) { - return element.tests; +class CoverageCollectionItem extends CoolTreeItem { + constructor(readonly collector: CoverageCollector) { + super(collector.getName(), vscode.TreeItemCollapsibleState.Collapsed); + const percentage = collector.getPercentCoverage(); + this.description = `${percentage}% covered`; + + if (percentage <= 1) { + this.iconPath = new vscode.ThemeIcon("circle-slash"); + } else if (percentage < 100) { + this.iconPath = new vscode.ThemeIcon("warning"); + } else { + this.iconPath = new vscode.ThemeIcon("pass", new vscode.ThemeColor("testing.iconPassed")); } - else { - return this.testSuites.sort((ts1, ts2) => ts1.name.localeCompare(ts2.name)); + } + + async getChildren() { + const coverage = this.collector.getCoverage(); + return Object.keys(coverage).map(method => new CoverageMethodCountItem(method, coverage[method])); + } +} + +class CoverageMethodCountItem extends CoolTreeItem { + constructor(readonly method: string, readonly count: number) { + super(method, vscode.TreeItemCollapsibleState.None); + this.description = `${count}`; + + if (count > 0) { + this.iconPath = new vscode.ThemeIcon("pass", new vscode.ThemeColor("testing.iconPassed")); + } else { + this.iconPath = new vscode.ThemeIcon("symbol-method"); } } } -class TestSuiteItem extends vscode.TreeItem { +class TestSuitesItem extends CoolTreeItem { + constructor(readonly testSuites: TestSuite[]) { + super("Test Suites", vscode.TreeItemCollapsibleState.Expanded); + } + + async getChildren() { + return this.testSuites.map(suite => new TestSuiteItem(suite)); + } +} + +class TestSuiteItem extends CoolTreeItem { constructor(readonly testSuite: TestSuite) { super(testSuite.name, vscode.TreeItemCollapsibleState.Expanded); this.description = `${this.testSuite.tests.filter(tc => tc.status === "pass").length}/${this.testSuite.tests.length}`; @@ -52,9 +108,13 @@ class TestSuiteItem extends vscode.TreeItem { this.iconPath = new vscode.ThemeIcon(testSuite.status === "running" ? "gear~spin" : "beaker", new vscode.ThemeColor(color)); this.tooltip = this.testSuite.failure; } + + async getChildren() { + return this.testSuite.tests.map(tc => new TestCaseItem(this.testSuite, tc)); + } } -class TestCaseItem extends vscode.TreeItem { +class TestCaseItem extends CoolTreeItem { constructor(readonly testSuite: TestSuite, readonly testCase: TestCase) { super(testCase.name, vscode.TreeItemCollapsibleState.None); let icon;