Skip to content

Commit

Permalink
fix status update and testRun glitches (#916)
Browse files Browse the repository at this point in the history
  • Loading branch information
connectdotz authored Oct 9, 2022
1 parent c567e4e commit d28d253
Show file tree
Hide file tree
Showing 6 changed files with 524 additions and 177 deletions.
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

0 comments on commit d28d253

Please sign in to comment.