Skip to content

Commit

Permalink
Show coverage results in testing UI
Browse files Browse the repository at this point in the history
Signed-off-by: worksofliam <mrliamallan@live.co.uk>
  • Loading branch information
worksofliam committed Nov 26, 2024
1 parent 84215ff commit 699b20f
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 36 deletions.
58 changes: 52 additions & 6 deletions src/testing/coverage.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,39 @@
import IBMi from "../api/IBMi";

export type CollectorGroup = CoverageCollector<any>[];
export interface CapturedMethods { [key: string]: number };

const IGNORE_METHODS = [`constructor`];

export class CoverageCollector<T> {
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;
Expand All @@ -25,4 +51,24 @@ export class CoverageCollector<T> {
}
}
}

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;
}
}
27 changes: 17 additions & 10 deletions src/testing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -71,10 +72,7 @@ const testSuitesSimultaneously = env.simultaneous === `true`;
const testIndividually = env.individual === `true`;
const testSpecific = env.specific;

const coverages: {
'Connection': CoverageCollector<IBMi>|undefined,
'Content': CoverageCollector<IBMiContent>|undefined
} = { Connection: undefined, Content: undefined };
let coverages: CollectorGroup = [];

let testSuitesTreeProvider: TestSuitesTreeProvider;
export function initialise(context: vscode.ExtensionContext) {
Expand All @@ -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`);
Expand All @@ -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) => {
Expand Down Expand Up @@ -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();
Expand All @@ -280,7 +286,7 @@ async function runTest(test: TestCase) {
}
finally {
test.duration = +(new Date()) - start;
testSuitesTreeProvider.refresh(test);
testSuitesTreeProvider.refresh();
}
} else {
test.status = undefined;
Expand All @@ -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;
Expand Down
100 changes: 80 additions & 20 deletions src/testing/testCasesTree.ts
Original file line number Diff line number Diff line change
@@ -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<CoolTreeItem[]>;
}

export class TestSuitesTreeProvider implements vscode.TreeDataProvider<TestObject>{
private readonly emitter: vscode.EventEmitter<TestObject | undefined | null | void> = new vscode.EventEmitter();
readonly onDidChangeTreeData: vscode.Event<void | TestObject | TestObject[] | null | undefined> = this.emitter.event;
export class TestSuitesTreeProvider implements vscode.TreeDataProvider<CoolTreeItem>{
private readonly emitter: vscode.EventEmitter<CoolTreeItem | undefined | null | void> = new vscode.EventEmitter();
readonly onDidChangeTreeData: vscode.Event<void | CoolTreeItem | CoolTreeItem[] | null | undefined> = 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<vscode.TreeItem> {
return element;
}

getTreeItem(element: TestObject): vscode.TreeItem | Thenable<vscode.TreeItem> {
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<CoolTreeItem[]> {
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<TestObject[]> {
if (element && "tests" in element) {
return element.tests;
class CoverageCollectionItem extends CoolTreeItem {
constructor(readonly collector: CoverageCollector<any>) {
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}`;
Expand All @@ -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;
Expand Down

0 comments on commit 699b20f

Please sign in to comment.