Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fill test explorer with mutations on startup #1

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 10 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
"categories": [
"Other"
],
"activationEvents": [],
"activationEvents": [
"onStartupFinished"
],
"main": "./out/extension.js",
"contributes": {
"commands": [
Expand All @@ -28,14 +30,17 @@
"test": "vscode-test"
},
"devDependencies": {
"@types/vscode": "^1.86.0",
"@types/mocha": "^10.0.6",
"@types/node": "18.x",
"@types/vscode": "^1.86.0",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"eslint": "^8.56.0",
"typescript": "^5.3.3",
"@vscode/test-cli": "^0.0.4",
"@vscode/test-electron": "^2.3.9"
"@vscode/test-electron": "^2.3.9",
"eslint": "^8.56.0",
"typescript": "^5.3.3"
},
"dependencies": {
"mutation-testing-report-schema": "^3.0.2"
}
}
4 changes: 4 additions & 0 deletions src/api/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Config {
currentWorkingDirectory: string;
jsonReporterFilename: string;
}
8 changes: 8 additions & 0 deletions src/api/test-item-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { MutantResult } from "mutation-testing-report-schema";

export interface TestItemNode {
name: string;
children: TestItemNode[];
mutants: MutantResult[];
fullPath?: string;
}
7 changes: 7 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Config } from "./api/config.js";
import * as vscode from 'vscode';

export const config: Config = {
currentWorkingDirectory: vscode.workspace.workspaceFolders![0].uri.fsPath,
jsonReporterFilename: '/reports/mutation/mutation.json',
};
29 changes: 11 additions & 18 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';
import { PlatformFactory } from './platforms/platform-factory.js';
import { TestControllerHandler } from './utils/test-controller-handler.js';

// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
export async function activate(context: vscode.ExtensionContext) {

// Use the console to output diagnostic information (console.log) and errors (console.error)
// This line of code will only be executed once when your extension is activated
console.log('Congratulations, your extension "stryker-mutator" is now active!');
vscode.window.showInformationMessage('Starting mutation testing instrumentation run');
const controller = vscode.tests.createTestController('stryker-mutator', 'Stryker Mutator');
const testControllerHandler = new TestControllerHandler(controller);

// The command has been defined in the package.json file
// Now provide the implementation of the command with registerCommand
// The commandId parameter must match the command field in package.json
let disposable = vscode.commands.registerCommand('stryker-mutator.helloWorld', () => {
// The code you place here will be executed every time your command is executed
// Display a message box to the user
vscode.window.showInformationMessage('Hello World from Stryker Mutator!');
});
const platform = new PlatformFactory().getPlatform();

context.subscriptions.push(disposable);
platform.instrumentationRun().then((result) => {
testControllerHandler.updateTestExplorer(result);
vscode.window.showInformationMessage('Instrumentation run completed');
danny12321 marked this conversation as resolved.
Show resolved Hide resolved
});
}

// This method is called when your extension is deactivated
export function deactivate() {}
11 changes: 11 additions & 0 deletions src/platforms/platform-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Platform } from "./platform.js";
import { StrykerJs } from "./strykerjs.js";

export class PlatformFactory {

public getPlatform(): Platform {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kan dit niet een static function zijn? En moeten we hier uberhaupt een class van maken?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kan dit niet een static function zijn?

Ja, klopt. Ik heb het nu aangepast.

En moeten we hier uberhaupt een class van maken?

Ik verwacht dat deze factory later uitgebreid wordt met methodes om te bepalen welk Stryker-platform aangeroepen moet worden. Om die reden had ik hem al aangemaakt, maar een klasse is nu niet direct nodig. Ik kan hem eventueel weghalen als je wilt, maar het was alvast een opzet voor later.

// Here we can add more Stryker flavours in the future, depending on the opened workspace
return new StrykerJs();
}

}
6 changes: 6 additions & 0 deletions src/platforms/platform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { MutationTestResult } from "mutation-testing-report-schema";

export interface Platform {
instrumentationRun(): Promise<MutationTestResult>;
}

32 changes: 32 additions & 0 deletions src/platforms/strykerjs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { executeCommand } from "../utils/executor.js";
import { Platform } from "./platform.js";
import { errorNotification } from "../utils/reporter.js";
import { FileUtil } from "../utils/file.js";
import { ProgressLocation, window } from "vscode";
import { MutationTestResult } from "mutation-testing-report-schema";

export class StrykerJs implements Platform {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deze naam is nu wel heel generiek. Als we in de class alle functionaliteit van StrykerJS gaan zetten lijkt het mij een groot bestand te worden.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ik wil deze klasse enkel gebruiken voor de verschillende aanroepen van de executable, en vervolgens de resultaten daaruit afhandelen in andere klassen. Ik denk dat hij daarom niet groot zal worden. Ook kan ik nog functionaliteit afsplitsen naar de base class Platform, want ik verwacht dat het gros vergelijkbaar zal zijn tussen de Stryker-platformen.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ik heb nu ook het e.e.a. verplaatst naar de base abstract class


// temp path to the unpublished stryker executable while in development
// TODO: Make this configurable/autodetect
executable = '/home/jasper/repos/stryker-js/packages/core/bin/stryker.js';

async instrumentationRun(): Promise<MutationTestResult> {
return await window.withProgress({
location: ProgressLocation.Window,
title: 'Stryker: Instrumenting code',
danny12321 marked this conversation as resolved.
Show resolved Hide resolved
cancellable: false
danny12321 marked this conversation as resolved.
Show resolved Hide resolved
}, async () => {
try {
const command = `${this.executable} run --instrumentRunOnly --checkers ""` +
` --buildCommand "" --plugins "" --testRunner "" --logLevel off --reporters json`;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ik wil voorstellen om dit net zo op te zetten als het spawen van een childprocess. Een string is lastiger te manipuleren dan een array (van argumenten).

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes dat is beter! Ik heb het aangepast.
Ik gebruik nu ook spawn i.p.v. exec omdat dit een stuk sneller blijkt te zijn.

await executeCommand(command);

return await FileUtil.readMutationReport(FileUtil.getMutationReportUri());
} catch (error) {
errorNotification('Stryker: Error running instrumentation');
throw new Error('Stryker: Error running instrumentation');
}
});
}
}
15 changes: 0 additions & 15 deletions src/test/extension.test.ts

This file was deleted.

120 changes: 120 additions & 0 deletions src/test/test-controller-handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import * as assert from 'assert';
import { TestControllerHandler } from '../utils/test-controller-handler';
import * as vscode from 'vscode';
import { MutantResult, MutationTestResult } from 'mutation-testing-report-schema';

suite('test-controller-handler', () => {

let mutationReport: MutationTestResult = {
schemaVersion: "1.0",
thresholds: {
high: 80,
low: 60
},
files: {
"src/services/myService.js": {
language: "javascript",
mutants: [
{
id: "1",
location: {
start: { line: 1, column: 1 },
end: { line: 1, column: 2 }
},
mutatorName: "BinaryOperator",
status: "Killed",
},
{
id: "2",
location: {
start: { line: 2, column: 1 },
end: { line: 2, column: 2 }
},
mutatorName: "LogicalOperator",
status: "Survived",
}
],
source: "console.log('Hello, world!');"
},
"src/services/mySecondService.js": {
language: "javascript",
mutants: [
{
id: "1",
location: {
start: { line: 1, column: 1 },
end: { line: 1, column: 2 }
},
mutatorName: "BinaryOperator",
status: "Killed",
},
],
source: "console.log('Test myService');"
},
"src/util.js": {
language: "javascript",
mutants: [
{
id: "1",
location: {
start: { line: 1, column: 1 },
end: { line: 1, column: 2 }
},
mutatorName: "BinaryOperator",
status: "Killed",
},
],
source: "console.log('Cool util');"
}
}
};

let testController: vscode.TestController = vscode.tests.createTestController("id", "label");

test('should create test item', () => {
const testControllerHandler = new TestControllerHandler(testController);

const mutantResult: MutantResult = {

id: "1",
location: {
start: { line: 1, column: 1 },
end: { line: 1, column: 2 }
},
mutatorName: "BinaryOperator",
status: "Killed",
replacement: "+"
};

const testItem = testControllerHandler.createTestItem(mutantResult, vscode.Uri.file("src/services/myService.js"));

assert.strictEqual(testItem.id, "BinaryOperator(1:1-1:2) (+)");
assert.strictEqual(testItem.label, "BinaryOperator (1:1)");
assert.strictEqual(testItem.range?.end.line, 1);
assert.strictEqual(testItem.range?.end.character, 2);
assert.strictEqual(testItem.range?.start.line, 1);
assert.strictEqual(testItem.range?.start.character, 1);
});

test('should create test item tree node', () => {
const testControllerHandler = new TestControllerHandler(testController);

const testItemNodes = testControllerHandler.createTestItemNodeTree(mutationReport.files);

assert.strictEqual(testItemNodes.length, 1);
assert.strictEqual(testItemNodes[0].name, "src");
assert.strictEqual(testItemNodes[0].children.length, 2);
assert.strictEqual(testItemNodes[0].children[0].name, "services");
assert.strictEqual(testItemNodes[0].children[0].children.length, 2);
assert.strictEqual(testItemNodes[0].children[0].children[0].name, "myService.js");
assert.strictEqual(testItemNodes[0].children[0].children[0].mutants.length, 2);
assert.strictEqual(testItemNodes[0].children[0].children[0].fullPath, "src/services/myService.js");
assert.strictEqual(testItemNodes[0].children[0].children[0].mutants[0].id, "1");
assert.strictEqual(testItemNodes[0].children[0].children[1].name, "mySecondService.js");
assert.strictEqual(testItemNodes[0].children[0].children[1].mutants.length, 1);
assert.strictEqual(testItemNodes[0].children[1].name, "util.js");
assert.strictEqual(testItemNodes[0].children[1].mutants.length, 1);
assert.strictEqual(testItemNodes[0].mutants.length, 0);
assert.strictEqual(testItemNodes[0].fullPath, undefined);
});
});
10 changes: 10 additions & 0 deletions src/utils/executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { promisify } from 'util';
import { exec as execCallback } from 'child_process';
import { config } from '../config.js';

const exec = promisify(execCallback);

export function executeCommand(command: string) {
console.log(config.currentWorkingDirectory);
return exec(command, { cwd: config.currentWorkingDirectory });
}
19 changes: 19 additions & 0 deletions src/utils/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as vscode from 'vscode';
import { config } from '../config.js';
import { MutationTestResult } from 'mutation-testing-report-schema';

export class FileUtil {
static async readMutationReport(file: vscode.Uri): Promise<MutationTestResult> {
try {
const contents = await vscode.workspace.fs.readFile(file);
return JSON.parse(contents.toString());
} catch (error) {
console.error('Error reading mutation report:', error);
throw error;
}
}

static getMutationReportUri(): vscode.Uri {
return vscode.Uri.file(config.currentWorkingDirectory + config.jsonReporterFilename);
}
}
13 changes: 13 additions & 0 deletions src/utils/reporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as vscode from 'vscode';

export function errorNotification(message: string): void {
vscode.window.showErrorMessage(message);
}

export function infoNotification(message: string): void {
vscode.window.showInformationMessage(message);
}

export function warningNotification(message: string): void {
vscode.window.showWarningMessage(message);
}
Loading