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

fix TestExplorer status update and testRun glitches #916

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
97 changes: 69 additions & 28 deletions src/test-provider/test-item-data.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as vscode from 'vscode';
import { extensionId } from '../appGlobals';
import { JestRunEvent } from '../JestExt';
import { JestRunEvent, RunEventBase } from '../JestExt';
import { TestSuiteResult } from '../TestResults';
import * as path from 'path';
import { JestExtRequestType } from '../JestExt/process-session';
Expand All @@ -21,6 +21,7 @@ interface WithUri {
}

type JestTestRunRequest = JestExtRequestType & { run: JestTestRun };
type TypedRunEvent = RunEventBase & { type: string };

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isJestTestRunRequest = (arg: any): arg is JestTestRunRequest =>
Expand Down Expand Up @@ -54,15 +55,15 @@ abstract class TestItemDataBase implements TestItemData, JestRunable, WithUri {
const jestRequest = this.getJestRunRequest();
run.item = this.item;

this.deepItemState(this.item, run.vscodeRun.enqueued);
this.deepItemState(this.item, run.enqueued);

const process = this.context.ext.session.scheduleProcess({
...jestRequest,
run,
});
if (!process) {
const msg = `failed to schedule test for ${this.item.id}`;
run.vscodeRun.errored(this.item, new vscode.TestMessage(msg));
run.errored(this.item, new vscode.TestMessage(msg));
run.write(msg, 'error');
run.end();
}
Expand Down Expand Up @@ -137,11 +138,12 @@ export class WorkspaceRoot extends TestItemDataBase {
};

private createRun = (options?: JestTestRunOptions): JestTestRun => {
const target = options?.item ?? this.item;
return this.context.createTestRun(new vscode.TestRunRequest([target]), {
const item = options?.item ?? this.item;
const request = options?.request ?? new vscode.TestRunRequest([item]);
return this.context.createTestRun(request, {
...options,
name: options?.name ?? target.id,
item: target,
name: options?.name ?? item.id,
item,
});
};

Expand Down Expand Up @@ -224,7 +226,7 @@ export class WorkspaceRoot extends TestItemDataBase {
private onTestSuiteChanged = (event: TestSuitChangeEvent): void => {
switch (event.type) {
case 'assertions-updated': {
const run = this.getJestRun(event.process) ?? this.createRun({ name: event.process.id });
const run = this.getJestRun(event.process) ?? this.createRunForEvent(event);

this.log(
'debug',
Expand All @@ -248,6 +250,12 @@ export class WorkspaceRoot extends TestItemDataBase {

/** get test item from jest process. If running tests from source file, will return undefined */
private getItemFromProcess = (process: JestProcessInfo): vscode.TestItem | undefined => {
// the TestExplorer triggered run should already have item associated
if (isJestTestRunRequest(process.request) && process.request.run.item) {
return process.request.run.item;
}

// should only come here for autoRun processes
let fileName;
switch (process.request.type) {
case 'watch-tests':
Expand All @@ -267,19 +275,29 @@ export class WorkspaceRoot extends TestItemDataBase {
return this.testDocuments.get(fileName)?.item;
};

private createJestTestRun = (event: JestRunEvent): JestTestRun => {
private createRunForEvent = (event: TypedRunEvent): JestTestRun => {
const item = this.getItemFromProcess(event.process) ?? this.item;
const [request, name] = isJestTestRunRequest(event.process.request)
? [event.process.request.run.request, event.process.request.run.name]
: [];
const run = this.createRun({
name: `${event.type}:${event.process.id}`,
name: name ?? `${event.type}:${event.process.id}`,
item,
onEnd: () => this.cachedRun.delete(event.process.id),
request,
});

this.cachedRun.set(event.process.id, run);
return run;
};
private getJestRun = (process: JestProcessInfo): JestTestRun | undefined =>
isJestTestRunRequest(process.request) ? process.request.run : this.cachedRun.get(process.id);
/** return a valid run from process or process-run-cache. return undefined if run is closed. */
private getJestRun = (process: JestProcessInfo): JestTestRun | undefined => {
const run = isJestTestRunRequest(process.request)
? process.request.run
: this.cachedRun.get(process.id);

return run?.isClosed() ? undefined : run;
};

private runLog(type: string): void {
const d = new Date();
Expand All @@ -299,22 +317,24 @@ export class WorkspaceRoot extends TestItemDataBase {
switch (event.type) {
case 'scheduled': {
if (!run) {
run = this.createJestTestRun(event);
this.deepItemState(run.item, run.vscodeRun.enqueued);
run = this.createRunForEvent(event);
this.deepItemState(run.item, run.enqueued);
}

break;
}
case 'data': {
run = run ?? this.createJestTestRun(event);
const text = event.raw ?? event.text;
const opt = event.isError ? 'error' : event.newLine ? 'new-line' : undefined;
run.write(text, opt);
if (text && text.length > 0) {
run = run ?? this.createRunForEvent(event);
const opt = event.isError ? 'error' : event.newLine ? 'new-line' : undefined;
run.write(text, opt);
}
break;
}
case 'start': {
run = run ?? this.createJestTestRun(event);
this.deepItemState(run.item, run.vscodeRun.started);
run = run ?? this.createRunForEvent(event);
this.deepItemState(run.item, run.started);
this.runLog('started');
break;
}
Expand All @@ -325,13 +345,11 @@ export class WorkspaceRoot extends TestItemDataBase {
}
case 'exit': {
if (event.error) {
if (!run || run.vscodeRun.token.isCancellationRequested) {
run = this.createJestTestRun(event);
}
run = run ?? this.createRunForEvent(event);
const type = getExitErrorDef(event.code) ?? GENERIC_ERROR;
run.write(event.error, type);
if (run.item) {
run.vscodeRun.errored(run.item, new vscode.TestMessage(event.error));
run.errored(run.item, new vscode.TestMessage(event.error));
}
}
this.runLog('exited');
Expand Down Expand Up @@ -397,6 +415,22 @@ const isAssertDataNode = (arg: ItemNodeType): arg is DataNode<TestAssertionStatu
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isDataNode(arg) && (arg.data as any).fullName;

const isEmpty = (node?: ItemNodeType): boolean => {
if (!node) {
return true;
}
if (isDataNode(node)) {
return false;
}
if (
(node.childData && node.childData.length > 0) ||
(node.childContainers && node.childContainers.length > 0)
) {
return false;
}
return true;
};

// type AssertNode = NodeType<TestAssertionStatus>;
abstract class TestResultData extends TestItemDataBase {
constructor(readonly context: JestTestProviderContext, name: string) {
Expand All @@ -414,11 +448,11 @@ abstract class TestResultData extends TestItemDataBase {
const status = result.status;
switch (status) {
case 'KnownSuccess':
run.vscodeRun.passed(this.item);
run.passed(this.item);
break;
case 'KnownSkip':
case 'KnownTodo':
run.vscodeRun.skipped(this.item);
run.skipped(this.item);
break;
case 'KnownFail': {
if (this.context.ext.settings.testExplorer.showInlineError) {
Expand All @@ -427,9 +461,9 @@ abstract class TestResultData extends TestItemDataBase {
message.location = errorLocation;
}

run.vscodeRun.failed(this.item, message);
run.failed(this.item, message);
} else {
run.vscodeRun.failed(this.item, []);
run.failed(this.item, []);
}
break;
}
Expand Down Expand Up @@ -546,7 +580,14 @@ export class TestDocumentRoot extends TestResultData {

public updateResultState(run: JestTestRun): void {
const suiteResult = this.context.ext.testResolveProvider.getTestSuiteResult(this.item.id);
this.updateItemState(run, suiteResult);

// only update suite status if the assertionContainer is empty, which can occur when
// test file has syntax error or failed to run for whatever reason.
// In this case we should mark the suite itself as TestExplorer won't be able to
// aggregate from the children list
if (isEmpty(suiteResult?.assertionContainer)) {
this.updateItemState(run, suiteResult);
}

this.item.children.forEach((childItem) =>
this.context.getData<TestData>(childItem)?.updateResultState(run)
Expand Down
106 changes: 94 additions & 12 deletions src/test-provider/test-provider-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { JestExtExplorerContext, TestItemData } from './types';

export type TagIdType = 'run' | 'debug';

let RunSeq = 0;
export class JestTestProviderContext {
private testItemData: WeakMap<vscode.TestItem, TestItemData>;

Expand Down Expand Up @@ -76,8 +77,10 @@ export class JestTestProviderContext {
};

createTestRun = (request: vscode.TestRunRequest, options?: JestTestRunOptions): JestTestRun => {
const vscodeRun = this.controller.createTestRun(request, options?.name ?? 'unknown');
return new JestTestRun(this, vscodeRun, options);
const name = options?.name ?? `run-${RunSeq++}`;
const opt = { ...(options ?? {}), request, name };
const vscodeRun = this.controller.createTestRun(request, name);
return new JestTestRun(this, vscodeRun, opt);
};

// tags
Expand All @@ -88,36 +91,115 @@ export class JestTestProviderContext {
export interface JestTestRunOptions {
name?: string;
item?: vscode.TestItem;
request?: vscode.TestRunRequest;

// in addition to the regular end() method
onEnd?: () => void;
// if true, when the run ends, we will not end the vscodeRun, this is used when multiple test items
// in a single request, that the run should be closed when all items are done.
disableVscodeRunEnd?: boolean;

// replace the end function
end?: () => void;
}

export class JestTestRun implements JestExtOutput {
export type TestRunProtocol = Pick<
vscode.TestRun,
'name' | 'enqueued' | 'started' | 'errored' | 'failed' | 'passed' | 'skipped' | 'end'
>;
export type ParentRun = vscode.TestRun | JestTestRun;
const isVscodeRun = (arg: ParentRun | undefined): arg is vscode.TestRun =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
arg != null && typeof (arg as any).appendOutput === 'function';
const isJestTestRun = (arg: ParentRun | undefined): arg is JestTestRun => !isVscodeRun(arg);

/** a wrapper for vscode.TestRun or another JestTestRun */
export class JestTestRun implements JestExtOutput, TestRunProtocol {
private output: JestOutputTerminal;
public item?: vscode.TestItem;
private parentRun?: ParentRun;

constructor(
context: JestTestProviderContext,
public vscodeRun: vscode.TestRun,
parentRun: ParentRun,
private options?: JestTestRunOptions
) {
this.parentRun = parentRun;
this.output = context.output;
this.item = options?.item;
}

end(): void {
if (this.options?.disableVscodeRunEnd !== true) {
this.vscodeRun.end();
get vscodeRun(): vscode.TestRun | undefined {
if (!this.parentRun) {
return;
}
this.options?.onEnd?.();
if (isVscodeRun(this.parentRun)) {
return this.parentRun;
}
return this.parentRun.vscodeRun;
}

write(msg: string, opt?: OutputOptions): string {
const text = this.output.write(msg, opt);
this.vscodeRun.appendOutput(text);
this.vscodeRun?.appendOutput(text);
return text;
}

isClosed(): boolean {
return this.vscodeRun === undefined;
}
get request(): vscode.TestRunRequest | undefined {
return (
this.options?.request ?? (isJestTestRun(this.parentRun) ? this.parentRun.request : undefined)
);
}

private updateState = (f: (pRun: ParentRun) => void): void => {
if (!this.parentRun || !this.vscodeRun) {
throw new Error(`run "${this.name}" has already closed`);
}
f(this.parentRun);
};

// TestRunProtocol
public get name(): string | undefined {
return this.options?.name;
}
public enqueued = (test: vscode.TestItem): void => {
this.updateState((pRun) => pRun.enqueued(test));
};
public started = (test: vscode.TestItem): void => {
this.updateState((pRun) => pRun.started(test));
};
public errored = (
test: vscode.TestItem,
message: vscode.TestMessage | readonly vscode.TestMessage[],
duration?: number | undefined
): void => {
this.updateState((pRun) => pRun.errored(test, message, duration));
};
public failed = (
test: vscode.TestItem,
message: vscode.TestMessage | readonly vscode.TestMessage[],
duration?: number | undefined
): void => {
this.updateState((pRun) => pRun.failed(test, message, duration));
};
public passed = (test: vscode.TestItem, duration?: number | undefined): void => {
this.updateState((pRun) => pRun.passed(test, duration));
};
public skipped = (test: vscode.TestItem): void => {
this.updateState((pRun) => pRun.skipped(test));
};
public end = (): void => {
if (this.options?.end) {
return this.options.end();
}

if (this.parentRun) {
this.parentRun.end();
if (isVscodeRun(this.parentRun)) {
this.parentRun = undefined;
}
}

this.options?.onEnd?.();
};
}
Loading