From 5a0bf3751abfaefb44d1d83d168aefddf056b032 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 18 Feb 2021 17:29:39 -0800 Subject: [PATCH] testing: add method to publish extension-provided results --- src/vs/base/common/iterator.ts | 33 ++++++-- src/vs/base/test/common/iterator.test.ts | 7 ++ src/vs/vscode.proposed.d.ts | 24 +++++- .../api/browser/mainThreadTesting.ts | 20 +++-- .../workbench/api/common/extHost.api.impl.ts | 4 + .../workbench/api/common/extHost.protocol.ts | 1 + src/vs/workbench/api/common/extHostTesting.ts | 35 +++----- .../api/common/extHostTypeConverters.ts | 69 +++++++++++++++- .../contrib/testing/common/testCollection.ts | 2 +- .../testing/common/testResultService.ts | 81 +++++++++++++++---- .../test/common/testResultService.test.ts | 48 ++++++++++- 11 files changed, 263 insertions(+), 61 deletions(-) diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index adfbdc906ab..8b60c8757dc 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -94,19 +94,19 @@ export namespace Iterable { /** * Returns an iterable slice of the array, with the same semantics as `array.slice()`. */ - export function* slice(iterable: ReadonlyArray, from: number, to = iterable.length): Iterable { + export function* slice(arr: ReadonlyArray, from: number, to = arr.length): Iterable { if (from < 0) { - from += iterable.length; + from += arr.length; } if (to < 0) { - to += iterable.length; - } else if (to > iterable.length) { - to = iterable.length; + to += arr.length; + } else if (to > arr.length) { + to = arr.length; } for (; from < to; from++) { - yield iterable[from]; + yield arr[from]; } } @@ -135,4 +135,25 @@ export namespace Iterable { return [consumed, { [Symbol.iterator]() { return iterator; } }]; } + + /** + * Returns whether the iterables are the same length and all items are + * equal using the comparator function. + */ + export function equals(a: Iterable, b: Iterable, comparator = (at: T, bt: T) => at === bt) { + const ai = a[Symbol.iterator](); + const bi = b[Symbol.iterator](); + while (true) { + const an = ai.next(); + const bn = bi.next(); + + if (an.done !== bn.done) { + return false; + } else if (an.done) { + return true; + } else if (!comparator(an.value, bn.value)) { + return false; + } + } + } } diff --git a/src/vs/base/test/common/iterator.test.ts b/src/vs/base/test/common/iterator.test.ts index 7f32bc3eb6a..a7cdd692c6f 100644 --- a/src/vs/base/test/common/iterator.test.ts +++ b/src/vs/base/test/common/iterator.test.ts @@ -25,4 +25,11 @@ suite('Iterable', function () { assert.equal(Iterable.first(customIterable), 'one'); // fresh }); + test('equals', () => { + assert.strictEqual(Iterable.equals([1, 2], [1, 2]), true); + assert.strictEqual(Iterable.equals([1, 2], [1]), false); + assert.strictEqual(Iterable.equals([1], [1, 2]), false); + assert.strictEqual(Iterable.equals([2, 1], [1, 2]), false); + }); + }); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index bf1b8c284e1..3d74824e6a2 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2227,6 +2227,22 @@ declare module 'vscode' { */ export function createDocumentTestObserver(document: TextDocument): TestObserver; + /** + * Inserts custom test results into the VS Code UI. The results are + * inserted and sorted based off the `completedAt` timestamp. If the + * results are being read from a file, for example, the `completedAt` + * time should generally be the modified time of the file if not more + * specific time is available. + * + * This will no-op if the inserted results are deeply equal to an + * existing result. + * + * @param results test results + * @param persist whether the test results should be saved by VS Code + * and persisted across reloads. Defaults to true. + */ + export function publishTestResult(results: TestResults, persist?: boolean): void; + /** * List of test results stored by VS Code, sorted in descnding * order by their `completedAt` time. @@ -2553,7 +2569,7 @@ declare module 'vscode' { * may be out of date. If the test still exists in the workspace, consumers * can use its `id` to correlate the result instance with the living test. * - * @todo coverage and other info may eventually live here + * @todo coverage and other info may eventually be provided here */ export interface TestResults { /** @@ -2573,6 +2589,12 @@ declare module 'vscode' { * provided in {@link TestResult} interfaces. */ export interface TestItemWithResults extends TestItem { + /** + * ID of the test result, this is required in order to correlate the result + * with the live test item. + */ + readonly id: string; + /** * Current result of the test. */ diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index 496ffe00d82..b33a86eae8e 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -9,8 +9,8 @@ import { isDefined } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { getTestSubscriptionKey, ITestState, RunTestsRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; -import { ITestResultService, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResultService'; +import { getTestSubscriptionKey, ISerializedTestResults, ITestState, RunTestsRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { HydratedTestResult, ITestResultService, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; @@ -48,11 +48,10 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh } this._register(resultService.onResultsChanged(evt => { - if ('completed' in evt) { - const serialized = evt.completed.toJSON(); - if (serialized) { - this.proxy.$publishTestResults([serialized]); - } + const results = 'completed' in evt ? evt.completed : ('inserted' in evt ? evt.inserted : undefined); + const serialized = results?.toJSON(); + if (serialized) { + this.proxy.$publishTestResults([serialized]); } })); @@ -63,6 +62,13 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh } } + /** + * @inheritdoc + */ + $publishExtensionProvidedResults(results: ISerializedTestResults, persist: boolean): void { + this.resultService.push(new HydratedTestResult(results, persist)); + } + /** * @inheritdoc */ diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index b383152bc59..5f83c9abe57 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -339,6 +339,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostTesting.runTests(provider); }, + publishTestResult(results, persist = true) { + checkProposedApiEnabled(extension); + return extHostTesting.publishExtensionProvidedResults(results, persist); + }, get onDidChangeTestResults() { checkProposedApiEnabled(extension); return extHostTesting.onResultsChanged; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index a05d8beb07b..eba04409256 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1864,6 +1864,7 @@ export interface MainThreadTestingShape { $publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void; $updateTestStateInRun(runId: string, testId: string, state: ITestState): void; $runTests(req: RunTestsRequest, token: CancellationToken): Promise; + $publishExtensionProvidedResults(results: ISerializedTestResults, persist: boolean): void; $retireTest(extId: string): void; } diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index b24ae70ab53..78f4d26bc8e 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -17,11 +17,11 @@ import { ExtHostTestingResource, ExtHostTestingShape, MainContext, MainThreadTes import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { TestItem, TestState } from 'vs/workbench/api/common/extHostTypeConverters'; +import { TestItem, TestResults, TestState } from 'vs/workbench/api/common/extHostTypeConverters'; import { Disposable } from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { OwnedTestCollection, SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; -import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, RunTestForProviderRequest, SerializedTestResultItem, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, RunTestForProviderRequest, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import type * as vscode from 'vscode'; const getTestSubscriptionKey = (resource: ExtHostTestingResource, uri: URI) => `${resource}:${uri.toString()}`; @@ -101,6 +101,12 @@ export class ExtHostTesting implements ExtHostTestingShape { }, token); } + /** + * Implements vscode.test.publishTestResults + */ + public publishExtensionProvidedResults(results: vscode.TestResults, persist: boolean): void { + this.proxy.$publishExtensionProvidedResults(TestResults.from(generateUuid(), results), persist); + } /** * Updates test results shown to extensions. @@ -109,7 +115,7 @@ export class ExtHostTesting implements ExtHostTestingShape { public $publishTestResults(results: ISerializedTestResults[]): void { this.results = Object.freeze( results - .map(r => deepFreeze(convertTestResults(r))) + .map(r => deepFreeze(TestResults.to(r))) .concat(this.results) .sort((a, b) => b.completedAt - a.completedAt) .slice(0, 32), @@ -355,29 +361,6 @@ export class ExtHostTesting implements ExtHostTestingShape { } } -const convertTestResultItem = (item: SerializedTestResultItem, byInternalId: Map): vscode.TestItemWithResults => ({ - ...TestItem.toShallow(item.item), - result: TestState.to(item.state), - children: item.children - .map(c => byInternalId.get(c)) - .filter(isDefined) - .map(c => convertTestResultItem(c, byInternalId)), -}); - -const convertTestResults = (r: ISerializedTestResults): vscode.TestResults => { - const roots: SerializedTestResultItem[] = []; - const byInternalId = new Map(); - for (const item of r.items) { - byInternalId.set(item.id, item); - if (item.direct) { - roots.push(item); - } - } - - return { completedAt: r.completedAt, results: roots.map(r => convertTestResultItem(r, byInternalId)) }; -}; - - /* * A class which wraps a vscode.TestItem that provides the ability to filter a TestItem's children * to only the children that are located in a certain vscode.Uri. diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 68915d73309..d0403e39625 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -22,7 +22,7 @@ import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; import { MarkerSeverity, IRelatedInformation, IMarkerData, MarkerTag } from 'vs/platform/markers/common/markers'; import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { isString, isNumber } from 'vs/base/common/types'; +import { isString, isNumber, isDefined } from 'vs/base/common/types'; import * as marked from 'vs/base/common/marked/marked'; import { parse } from 'vs/base/common/marshalling'; import { cloneAndChange } from 'vs/base/common/objects'; @@ -32,7 +32,7 @@ import { RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { ITestItem, ITestState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ISerializedTestResults, ITestItem, ITestState, SerializedTestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; export interface PositionLike { line: number; @@ -1622,7 +1622,6 @@ export namespace TestState { } } - export namespace TestItem { export function from(item: vscode.TestItem, parentExtId?: string): ITestItem { return { @@ -1649,3 +1648,67 @@ export namespace TestItem { }; } } + +export namespace TestResults { + export function from(id: string, results: vscode.TestResults): ISerializedTestResults { + const serialized: ISerializedTestResults = { + completedAt: results.completedAt, + id, + items: [], + }; + + const queue: [parent: SerializedTestResultItem | null, children: Iterable][] = [ + [null, results.results], + ]; + + let counter = 0; + while (queue.length) { + const [parent, children] = queue.pop()!; + for (const item of children) { + const serializedItem: SerializedTestResultItem = { + children: item.children?.map(c => c.id) ?? [], + computedState: item.result.state, + id: `${id}-${counter++}`, + item: TestItem.from(item, parent?.item.extId), + state: TestState.from(item.result), + retired: undefined, + parent: parent?.id ?? null, + providerId: '', + direct: !parent, + }; + + serialized.items.push(serializedItem); + if (item.children) { + queue.push([serializedItem, item.children]); + } + } + } + + return serialized; + } + + const convertTestResultItem = (item: SerializedTestResultItem, byInternalId: Map): vscode.TestItemWithResults => ({ + ...TestItem.toShallow(item.item), + result: TestState.to(item.state), + children: item.children + .map(c => byInternalId.get(c)) + .filter(isDefined) + .map(c => convertTestResultItem(c, byInternalId)), + }); + + export function to(serialized: ISerializedTestResults): vscode.TestResults { + const roots: SerializedTestResultItem[] = []; + const byInternalId = new Map(); + for (const item of serialized.items) { + byInternalId.set(item.id, item); + if (item.direct) { + roots.push(item); + } + } + + return { + completedAt: serialized.completedAt, + results: roots.map(r => convertTestResultItem(r, byInternalId)), + }; + } +} diff --git a/src/vs/workbench/contrib/testing/common/testCollection.ts b/src/vs/workbench/contrib/testing/common/testCollection.ts index 0a08e9d4f37..7d4ede8fc71 100644 --- a/src/vs/workbench/contrib/testing/common/testCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testCollection.ts @@ -90,7 +90,7 @@ export interface TestResultItem extends IncrementalTestCollectionItem { /** True if the test is outdated */ retired: boolean; /** True if the test was directly requested by the run (is not a child or parent) */ - direct?: true; + direct?: boolean; } export type SerializedTestResultItem = Omit & { children: string[], retired: undefined }; diff --git a/src/vs/workbench/contrib/testing/common/testResultService.ts b/src/vs/workbench/contrib/testing/common/testResultService.ts index ed6e98446a2..85bd7cd44b7 100644 --- a/src/vs/workbench/contrib/testing/common/testResultService.ts +++ b/src/vs/workbench/contrib/testing/common/testResultService.ts @@ -3,8 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { findFirstInSorted } from 'vs/base/common/arrays'; import { Emitter, Event } from 'vs/base/common/event'; +import { Iterable } from 'vs/base/common/iterator'; import { Lazy } from 'vs/base/common/lazy'; +import { equals } from 'vs/base/common/objects'; import { isDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; @@ -409,7 +412,7 @@ export class LiveTestResult implements ITestResult { /** * Test results hydrated from a previously-serialized test run. */ -class HydratedTestResult implements ITestResult { +export class HydratedTestResult implements ITestResult { /** * @inheritdoc */ @@ -434,7 +437,7 @@ class HydratedTestResult implements ITestResult { private readonly byExtId = new Map(); - constructor(private readonly serialized: ISerializedTestResults) { + constructor(private readonly serialized: ISerializedTestResults, private readonly persist = true) { this.id = serialized.id; this.completedAt = serialized.completedAt; @@ -467,14 +470,15 @@ class HydratedTestResult implements ITestResult { /** * @inheritdoc */ - public toJSON(): ISerializedTestResults { - return this.serialized; + public toJSON(): ISerializedTestResults | undefined { + return this.persist ? this.serialized : undefined; } } export type ResultChangeEvent = | { completed: LiveTestResult } | { started: LiveTestResult } + | { inserted: ITestResult } | { removed: ITestResult[] }; export interface ITestResultService { @@ -502,7 +506,7 @@ export interface ITestResultService { /** * Adds a new test result to the collection. */ - push(result: LiveTestResult): LiveTestResult; + push(result: T): T; /** * Looks up a set of test results by ID. @@ -519,6 +523,14 @@ export const ITestResultService = createDecorator('testResul const RETAIN_LAST_RESULTS = 64; +/** + * Returns if the tests in the results are exactly equal. Check the counts + * first as a cheap check before starting to iterate. + */ +const resultsEqual = (a: ITestResult, b: ITestResult) => + a.completedAt === b.completedAt && equals(a.counts, b.counts) && Iterable.equals(a.tests, b.tests, + (at, bt) => equals(at.state, bt.state) && equals(at.item, bt.item)); + export class TestResultService implements ITestResultService { declare _serviceBrand: undefined; private changeResultEmitter = new Emitter(); @@ -579,17 +591,47 @@ export class TestResultService implements ITestResultService { /** * @inheritdoc */ - public push(result: LiveTestResult): LiveTestResult { - this.results.unshift(result); + public push(result: T): T { + if (result.completedAt === undefined) { + this.results.unshift(result); + } else { + const index = findFirstInSorted(this.results, r => r.completedAt !== undefined && r.completedAt <= result.completedAt!); + const prev = this.results[index]; + if (prev && resultsEqual(result, prev)) { + return result; + } + + this.results.splice(index, 0, result); + this.persist(); + } + if (this.results.length > RETAIN_LAST_RESULTS) { this.results.pop(); } - result.onComplete(() => this.onComplete(result)); - result.onChange(this.testChangeEmitter.fire, this.testChangeEmitter); - this.isRunning.set(true); - this.changeResultEmitter.fire({ started: result }); - result.setAllToState(queuedState, () => true); + if (result instanceof LiveTestResult) { + result.onComplete(() => this.onComplete(result)); + result.onChange(this.testChangeEmitter.fire, this.testChangeEmitter); + this.isRunning.set(true); + this.changeResultEmitter.fire({ started: result }); + result.setAllToState(queuedState, () => true); + } else { + this.changeResultEmitter.fire({ inserted: result }); + // If this is not a new result, go through each of its tests. For each + // test for which the new result is the most recently inserted, fir + // a change event so that UI updates. + for (const item of result.tests) { + for (const otherResult of this.results) { + if (otherResult === result) { + this.testChangeEmitter.fire({ item, result, reason: TestResultItemChangeReason.ComputedStateChange }); + break; + } else if (otherResult.getStateByExtId(item.item.extId) !== undefined) { + break; + } + } + } + } + return result; } @@ -615,19 +657,26 @@ export class TestResultService implements ITestResultService { } this.results = keep; - this.serializedResults.store(this.results.map(r => r.toJSON()).filter(isDefined)); + this.persist(); this.changeResultEmitter.fire({ removed }); } private onComplete(result: LiveTestResult) { - // move the complete test run down behind any still-running ones this.resort(); - this.isRunning.set(this.results.length > 0 && this.results[0].completedAt === undefined); - this.serializedResults.store(this.results.map(r => r.toJSON()).filter(isDefined)); + this.updateIsRunning(); + this.persist(); this.changeResultEmitter.fire({ completed: result }); } private resort() { this.results.sort((a, b) => (b.completedAt ?? Number.MAX_SAFE_INTEGER) - (a.completedAt ?? Number.MAX_SAFE_INTEGER)); } + + private updateIsRunning() { + this.isRunning.set(this.results.length > 0 && this.results[0].completedAt === undefined); + } + + private persist() { + this.serializedResults.store(this.results.map(r => r.toJSON()).filter(isDefined)); + } } diff --git a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts index bf5deca24c4..8c55a1f3d44 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection'; -import { LiveTestResult, makeEmptyCounts, TestResultItemChange, TestResultItemChangeReason, TestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; +import { HydratedTestResult, LiveTestResult, makeEmptyCounts, TestResultItemChange, TestResultItemChangeReason, TestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { ReExportedTestRunState as TestRunState } from 'vs/workbench/contrib/testing/common/testStubs'; import { getInitializedMainTestCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; @@ -190,5 +190,51 @@ suite('Workbench - Test Results Service', () => { r.markComplete(); assert.deepStrictEqual(results.results, [r, r2]); }); + + const makeHydrated = (completedAt = 42, state = TestRunState.Passed) => new HydratedTestResult({ + completedAt, + id: 'some-id', + items: [{ + ...getInitializedMainTestCollection().getNodeById('2')!, + state: { state: TestRunState.Passed, duration: 0, messages: [] }, + computedState: TestRunState.Passed, + retired: undefined, + children: [], + }] + }); + + test('pushes hydrated results', () => { + results.push(r); + const hydrated = makeHydrated(); + results.push(hydrated); + assert.deepStrictEqual(results.results, [r, hydrated]); + }); + + test('deduplicates identical results', () => { + results.push(r); + const hydrated1 = makeHydrated(); + results.push(hydrated1); + const hydrated2 = makeHydrated(); + results.push(hydrated2); + assert.deepStrictEqual(results.results, [r, hydrated1]); + }); + + test('does not deduplicate if different completedAt', () => { + results.push(r); + const hydrated1 = makeHydrated(); + results.push(hydrated1); + const hydrated2 = makeHydrated(30); + results.push(hydrated2); + assert.deepStrictEqual(results.results, [r, hydrated1, hydrated2]); + }); + + test('does not deduplicate if different tests', () => { + results.push(r); + const hydrated1 = makeHydrated(); + results.push(hydrated1); + const hydrated2 = makeHydrated(undefined, TestRunState.Failed); + results.push(hydrated2); + assert.deepStrictEqual(results.results, [r, hydrated1, hydrated2]); + }); }); });