Skip to content

Commit

Permalink
refactor: test exporer
Browse files Browse the repository at this point in the history
  • Loading branch information
elonmallin committed Dec 4, 2023
1 parent 3ce800f commit a52cc30
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 130 deletions.
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@
"default": false,
"title": "PREVIEW: Enable/disable the test explorer",
"description": "This is a preview feature and may change in the future.\nRestart VSCode for changes to take effect."
},
"phpunit.testExplorer.include": {
"type": "string",
"default": "**/tests/**/*Test.php",
"title": "Glob pattern for test files",
"description": "Only tests matching this glob will be added to the test explorer. The top-most common directory will be the root in the test explorer."
}
}
},
Expand Down
287 changes: 157 additions & 130 deletions src/TestProvider/TestExplorerFeature.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as vscode from "vscode";
import { CancellationToken, EventEmitter, ExtensionContext, GlobPattern, RelativePattern, TestController, TestItem, TestRunProfile, TestRunProfileKind, TestRunRequest, TextDocument, Uri, tests, workspace } from "vscode";
import {
ITestCase,
createOrUpdateFromPath,
Expand All @@ -8,95 +8,102 @@ import {
} from "./TestCases";
import path = require("path");

let controller: vscode.TestController;

export async function addTestExplorerFeature(context: vscode.ExtensionContext) {
const testExplorerEnabled = vscode.workspace
.getConfiguration("phpunit")
.get<boolean>("testExplorer.enabled", false);
if (!testExplorerEnabled) {
return;
}
class TestExplorerFeature {
private watchingTests = new Map<
TestItem | "ALL",
TestRunProfile | undefined
>();
private subscriptions: { dispose(): any; }[] = [];

const ctrl = vscode.tests.createTestController(
"phpunitTestController",
"Phpunit",
);
controller = ctrl;
context.subscriptions.push(ctrl);
constructor(private ctrl: TestController) {
this.subscriptions.push(this.ctrl);

const fileChangedEmitter = new vscode.EventEmitter<vscode.Uri>();
const watchingTests = new Map<
vscode.TestItem | "ALL",
vscode.TestRunProfile | undefined
>();
fileChangedEmitter.event((uri) => {
if (watchingTests.has("ALL")) {
startTestRun(
new vscode.TestRunRequest(
undefined,
undefined,
watchingTests.get("ALL"),
true,
this.ctrl.refreshHandler = async () => {
await Promise.all(
getWorkspaceTestPatterns().map(({ pattern, exclude }) =>
findInitialTests(ctrl, pattern, exclude),
),
);
return;
}

const include: vscode.TestItem[] = [];
let profile: vscode.TestRunProfile | undefined;
for (const [item, thisProfile] of watchingTests) {
const cast = item as vscode.TestItem;
if (cast.uri?.toString() == uri.toString()) {
include.push(cast);
profile = thisProfile;
};

this.ctrl.createRunProfile(
"Run Tests",
TestRunProfileKind.Run,
this.runHandler,
true,
undefined,
true,
);

const fileChangedEmitter = this.createFileChangeEmitter();
this.ctrl.resolveHandler = async (item) => {
if (!item) {
this.subscriptions.push(
...startWatchingWorkspace(ctrl, fileChangedEmitter),
);
}
}
};

if (include.length) {
startTestRun(
new vscode.TestRunRequest(include, undefined, profile, true),
);
for (const document of workspace.textDocuments) {
updateNodeForDocument(this.ctrl, document);
}
});

this.subscriptions.push(
workspace.onDidOpenTextDocument((d) => updateNodeForDocument(this.ctrl, d)),
workspace.onDidChangeTextDocument(async (e) => {
deleteFromUri(this.ctrl, this.ctrl.items, e.document.uri);
await updateNodeForDocument(this.ctrl, e.document);
}),
);
}

const runHandler = (
request: vscode.TestRunRequest,
cancellation: vscode.CancellationToken,
) => {
if (!request.continuous) {
return startTestRun(request);
}
createFileChangeEmitter() {
const fileChangedEmitter = new EventEmitter<Uri>();

fileChangedEmitter.event((uri) => {
if (this.watchingTests.has("ALL")) {
this.startTestRun(
new TestRunRequest(
undefined,
undefined,
this.watchingTests.get("ALL"),
true,
),
);
return;
}

const include: TestItem[] = [];
let profile: TestRunProfile | undefined;
for (const [item, thisProfile] of this.watchingTests) {
const cast = item as TestItem;
if (cast.uri?.toString() == uri.toString()) {
include.push(cast);
profile = thisProfile;
}
}

if (include.length) {
this.startTestRun(
new TestRunRequest(include, undefined, profile, true),
);
}
});

if (request.include === undefined) {
watchingTests.set("ALL", request.profile);
cancellation.onCancellationRequested(() => watchingTests.delete("ALL"));
} else {
request.include.forEach((item) =>
watchingTests.set(item, request.profile),
);
cancellation.onCancellationRequested(() =>
request.include!.forEach((item) => watchingTests.delete(item)),
);
}
};
return fileChangedEmitter;
}

const startTestRun = (request: vscode.TestRunRequest) => {
const queue: { test: vscode.TestItem; data: ITestCase }[] = [];
const run = ctrl.createTestRun(request);
startTestRun = (request: TestRunRequest) => {
const queue: { test: TestItem; data: ITestCase }[] = [];
const run = this.ctrl.createTestRun(request);

const discoverTests = async (tests: Iterable<vscode.TestItem>) => {
const discoverTests = async (tests: Iterable<TestItem>) => {
for (const test of tests) {
if (request.exclude?.includes(test)) {
continue;
}

const data = testData.get(test)!;
// if (!data.isResolved) {
// if (data instanceof TestDirectory) {
// await findTestsInDirectory(ctrl, test.uri!, test);
// }
// }
run.enqueued(test);
queue.push({ test, data });
}
Expand All @@ -118,76 +125,96 @@ export async function addTestExplorerFeature(context: vscode.ExtensionContext) {
run.end();
};

discoverTests(request.include ?? gatherTestItems(ctrl.items)).then(
discoverTests(request.include ?? gatherTestItems(this.ctrl.items)).then(
runTestQueue,
);
};

ctrl.refreshHandler = async () => {
await Promise.all(
getWorkspaceTestPatterns().map(({ pattern, exclude }) =>
findInitialTests(ctrl, pattern, exclude),
),
);
};

ctrl.createRunProfile(
"Run Tests",
vscode.TestRunProfileKind.Run,
runHandler,
true,
undefined,
true,
);
runHandler = (
request: TestRunRequest,
cancellation: CancellationToken,
) => {
if (!request.continuous) {
return this.startTestRun(request);
}

ctrl.resolveHandler = async (item) => {
if (!item) {
context.subscriptions.push(
...startWatchingWorkspace(ctrl, fileChangedEmitter),
if (request.include === undefined) {
this.watchingTests.set("ALL", request.profile);
cancellation.onCancellationRequested(() => this.watchingTests.delete("ALL"));
} else {
request.include.forEach((item) =>
this.watchingTests.set(item, request.profile),
);
cancellation.onCancellationRequested(() =>
request.include!.forEach((item) => this.watchingTests.delete(item)),
);
return;
}

// const data = testData.get(item)!;
// if (item.canResolveChildren) {
// await findTestsInDirectory(ctrl, item.uri!, item);
// }
// TODO: implement this
// await data.updateFromDisk(ctrl, item);
};

dispose() {
this.subscriptions.forEach((s) => s.dispose());
this.ctrl.dispose();
}
}

export async function addTestExplorerFeature(context: ExtensionContext) {
let testExplorerFeature: TestExplorerFeature | null = null;

for (const document of vscode.workspace.textDocuments) {
updateNodeForDocument(document);
const testExplorerEnabled = workspace
.getConfiguration("phpunit")
.get<boolean>("testExplorer.enabled", false);
if (testExplorerEnabled) {
testExplorerFeature = new TestExplorerFeature(tests.createTestController(
"phpunitTestController",
"Phpunit",
));
context.subscriptions.push(testExplorerFeature);
}

context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument(updateNodeForDocument),
vscode.workspace.onDidChangeTextDocument(async (e) => {
deleteFromUri(controller, controller.items, e.document.uri);
await updateNodeForDocument(e.document);
}),
);
workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration("phpunit.testExplorer.enabled")) {
const testExplorerEnabled = workspace
.getConfiguration("phpunit")
.get("testExplorer.enabled");
if (testExplorerEnabled && !testExplorerFeature) {
testExplorerFeature = new TestExplorerFeature(tests.createTestController(
"phpunitTestController",
"Phpunit",
));
context.subscriptions.push(testExplorerFeature);
} else if (testExplorerFeature) {
const idx = context.subscriptions.findIndex(t => t == testExplorerFeature);
context.subscriptions.splice(idx, 1);
testExplorerFeature.dispose();
testExplorerFeature = null;
}
}
});
}

function getWorkspaceTestPatterns() {
if (!vscode.workspace.workspaceFolders) {
if (!workspace.workspaceFolders) {
return [];
}

return vscode.workspace.workspaceFolders.map((workspaceFolder) => ({
const testExplorerPattern = workspace
.getConfiguration("phpunit")
.get("testExplorer.include", "**/tests/**/*Test.php");

return workspace.workspaceFolders.map((workspaceFolder) => ({
workspaceFolder,
pattern: new vscode.RelativePattern(
pattern: new RelativePattern(
workspaceFolder,
"**/tests/**/*Test.php",
testExplorerPattern,
),
exclude: new vscode.RelativePattern(
exclude: new RelativePattern(
workspaceFolder,
"**/{.git,node_modules,vendor}/**",
),
}));
}

async function updateNodeForDocument(e: vscode.TextDocument) {
async function updateNodeForDocument(controller: TestController, e: TextDocument) {
if (e.uri.scheme !== "file") {
return;
}
Expand All @@ -211,9 +238,9 @@ async function updateNodeForDocument(e: vscode.TextDocument) {
}

async function findInitialTests(
controller: vscode.TestController,
pattern: vscode.GlobPattern,
exclude: vscode.GlobPattern,
controller: TestController,
pattern: GlobPattern,
exclude: GlobPattern,
) {
const { files, commonDirectory } = await getFilesAndCommonDirectory(
pattern,
Expand All @@ -226,10 +253,10 @@ async function findInitialTests(
}

async function getFilesAndCommonDirectory(
pattern: vscode.GlobPattern,
exclude: vscode.GlobPattern,
pattern: GlobPattern,
exclude: GlobPattern,
) {
const files = await vscode.workspace.findFiles(pattern, exclude);
const files = await workspace.findFiles(pattern, exclude);
const directories = files.map((file) => path.dirname(file.fsPath));
const commonDirectory = directories.reduce((common, dir) => {
let i = 0;
Expand All @@ -243,23 +270,23 @@ async function getFilesAndCommonDirectory(
}

function startWatchingWorkspace(
controller: vscode.TestController,
fileChangedEmitter: vscode.EventEmitter<vscode.Uri>,
controller: TestController,
fileChangedEmitter: EventEmitter<Uri>,
) {
return getWorkspaceTestPatterns().map(
({ workspaceFolder, pattern, exclude }) => {
const watcher = vscode.workspace.createFileSystemWatcher(pattern);
const watcher = workspace.createFileSystemWatcher(pattern);

watcher.onDidCreate(async (uri) => {
const document = await vscode.workspace.openTextDocument(uri);
updateNodeForDocument(document);
const document = await workspace.openTextDocument(uri);
updateNodeForDocument(controller, document);
fileChangedEmitter.fire(uri);
});
watcher.onDidChange(async (uri) => {
deleteFromUri(controller, controller.items, uri);

const document = await vscode.workspace.openTextDocument(uri);
await updateNodeForDocument(document);
const document = await workspace.openTextDocument(uri);
await updateNodeForDocument(controller, document);

fileChangedEmitter.fire(uri);
});
Expand Down

0 comments on commit a52cc30

Please sign in to comment.