Path: blob/main/src/vs/workbench/api/test/browser/extHostTesting.test.ts
5250 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import * as sinon from 'sinon';7import { timeout } from '../../../../base/common/async.js';8import { VSBuffer } from '../../../../base/common/buffer.js';9import { CancellationTokenSource } from '../../../../base/common/cancellation.js';10import { Event } from '../../../../base/common/event.js';11import { Iterable } from '../../../../base/common/iterator.js';12import { URI } from '../../../../base/common/uri.js';13import { mock, mockObject, MockObject } from '../../../../base/test/common/mock.js';14import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';15import * as editorRange from '../../../../editor/common/core/range.js';16import { ExtensionIdentifier, IExtensionDescription } from '../../../../platform/extensions/common/extensions.js';17import { NullLogService } from '../../../../platform/log/common/log.js';18import { MainThreadTestingShape } from '../../common/extHost.protocol.js';19import { ExtHostCommands } from '../../common/extHostCommands.js';20import { ExtHostDocumentsAndEditors } from '../../common/extHostDocumentsAndEditors.js';21import { IExtHostTelemetry } from '../../common/extHostTelemetry.js';22import { ExtHostTesting, TestRunCoordinator, TestRunDto, TestRunProfileImpl } from '../../common/extHostTesting.js';23import { ExtHostTestItemCollection, TestItemImpl } from '../../common/extHostTestItem.js';24import * as convert from '../../common/extHostTypeConverters.js';25import { Location, Position, Range, TestMessage, TestRunProfileKind, TestRunRequest as TestRunRequestImpl, TestTag } from '../../common/extHostTypes.js';26import { AnyCallRPCProtocol } from '../common/testRPCProtocol.js';27import { TestId } from '../../../contrib/testing/common/testId.js';28import { TestDiffOpType, TestItemExpandState, TestMessageType, TestsDiff } from '../../../contrib/testing/common/testTypes.js';29import { nullExtensionDescription } from '../../../services/extensions/common/extensions.js';30import type { TestController, TestItem, TestRunProfile, TestRunRequest } from 'vscode';3132const simplify = (item: TestItem) => ({33id: item.id,34label: item.label,35uri: item.uri,36range: item.range,37});3839const assertTreesEqual = (a: TestItemImpl | undefined, b: TestItemImpl | undefined) => {40if (!a) {41throw new assert.AssertionError({ message: 'Expected a to be defined', actual: a });42}4344if (!b) {45throw new assert.AssertionError({ message: 'Expected b to be defined', actual: b });46}4748assert.deepStrictEqual(simplify(a), simplify(b));4950const aChildren = [...a.children].map(([_, c]) => c.id).sort();51const bChildren = [...b.children].map(([_, c]) => c.id).sort();52assert.strictEqual(aChildren.length, bChildren.length, `expected ${a.label}.children.length == ${b.label}.children.length`);53aChildren.forEach(key => assertTreesEqual(a.children.get(key) as TestItemImpl, b.children.get(key) as TestItemImpl));54};5556// const assertTreeListEqual = (a: ReadonlyArray<TestItem>, b: ReadonlyArray<TestItem>) => {57// assert.strictEqual(a.length, b.length, `expected a.length == n.length`);58// a.forEach((_, i) => assertTreesEqual(a[i], b[i]));59// };6061// class TestMirroredCollection extends MirroredTestCollection {62// public changeEvent!: TestChangeEvent;6364// constructor() {65// super();66// this.onDidChangeTests(evt => this.changeEvent = evt);67// }6869// public get length() {70// return this.items.size;71// }72// }7374suite('ExtHost Testing', () => {75class TestExtHostTestItemCollection extends ExtHostTestItemCollection {76public setDiff(diff: TestsDiff) {77this.diff = diff;78}79}8081teardown(() => {82sinon.restore();83});8485const ds = ensureNoDisposablesAreLeakedInTestSuite();8687let single: TestExtHostTestItemCollection;88let resolveCalls: (string | undefined)[] = [];89setup(() => {90resolveCalls = [];91single = ds.add(new TestExtHostTestItemCollection('ctrlId', 'root', {92getDocument: () => undefined,93} as Partial<ExtHostDocumentsAndEditors> as ExtHostDocumentsAndEditors));94single.resolveHandler = item => {95resolveCalls.push(item?.id);96if (item === undefined) {97const a = new TestItemImpl('ctrlId', 'id-a', 'a', URI.file('/'));98a.canResolveChildren = true;99const b = new TestItemImpl('ctrlId', 'id-b', 'b', URI.file('/'));100single.root.children.add(a);101single.root.children.add(b);102} else if (item.id === 'id-a') {103item.children.add(new TestItemImpl('ctrlId', 'id-aa', 'aa', URI.file('/')));104item.children.add(new TestItemImpl('ctrlId', 'id-ab', 'ab', URI.file('/')));105}106};107108ds.add(single.onDidGenerateDiff(d => single.setDiff(d /* don't clear during testing */)));109});110111suite('OwnedTestCollection', () => {112test('adds a root recursively', async () => {113await single.expand(single.root.id, Infinity);114const a = single.root.children.get('id-a') as TestItemImpl;115const b = single.root.children.get('id-b') as TestItemImpl;116assert.deepStrictEqual(single.collectDiff(), [117{118op: TestDiffOpType.Add,119item: { controllerId: 'ctrlId', expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(single.root) } }120},121{122op: TestDiffOpType.Add,123item: { controllerId: 'ctrlId', expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(a) } }124},125{126op: TestDiffOpType.Add,127item: { controllerId: 'ctrlId', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(a.children.get('id-aa') as TestItemImpl) }128},129{130op: TestDiffOpType.Add,131item: { controllerId: 'ctrlId', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(a.children.get('id-ab') as TestItemImpl) }132},133{134op: TestDiffOpType.Update,135item: { extId: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.Expanded }136},137{138op: TestDiffOpType.Add,139item: { controllerId: 'ctrlId', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(b) }140},141{142op: TestDiffOpType.Update,143item: { extId: single.root.id, expand: TestItemExpandState.Expanded }144},145]);146});147148test('parents are set correctly', () => {149single.expand(single.root.id, Infinity);150single.collectDiff();151152const a = single.root.children.get('id-a')!;153const ab = a.children.get('id-ab')!;154assert.strictEqual(a.parent, undefined);155assert.strictEqual(ab.parent, a);156});157158test('can add an item with same ID as root', () => {159single.collectDiff();160161const child = new TestItemImpl('ctrlId', 'ctrlId', 'c', undefined);162single.root.children.add(child);163assert.deepStrictEqual(single.collectDiff(), [164{165op: TestDiffOpType.Add,166item: { controllerId: 'ctrlId', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(child) },167}168]);169});170171test('no-ops if items not changed', () => {172single.collectDiff();173assert.deepStrictEqual(single.collectDiff(), []);174});175176test('watches property mutations', () => {177single.expand(single.root.id, Infinity);178single.collectDiff();179single.root.children.get('id-a')!.description = 'Hello world'; /* item a */180181assert.deepStrictEqual(single.collectDiff(), [182{183op: TestDiffOpType.Update,184item: { extId: new TestId(['ctrlId', 'id-a']).toString(), item: { description: 'Hello world' } },185}186]);187});188189test('removes children', () => {190single.expand(single.root.id, Infinity);191single.collectDiff();192single.root.children.delete('id-a');193194assert.deepStrictEqual(single.collectDiff(), [195{ op: TestDiffOpType.Remove, itemId: new TestId(['ctrlId', 'id-a']).toString() },196]);197assert.deepStrictEqual(198[...single.tree.keys()].sort(),199[single.root.id, new TestId(['ctrlId', 'id-b']).toString()],200);201assert.strictEqual(single.tree.size, 2);202});203204test('adds new children', () => {205single.expand(single.root.id, Infinity);206single.collectDiff();207const child = new TestItemImpl('ctrlId', 'id-ac', 'c', undefined);208single.root.children.get('id-a')!.children.add(child);209210assert.deepStrictEqual(single.collectDiff(), [211{212op: TestDiffOpType.Add, item: {213controllerId: 'ctrlId',214expand: TestItemExpandState.NotExpandable,215item: convert.TestItem.from(child),216}217},218]);219assert.deepStrictEqual(220[...single.tree.values()].map(n => n.actual.id).sort(),221[single.root.id, 'id-a', 'id-aa', 'id-ab', 'id-ac', 'id-b'],222);223assert.strictEqual(single.tree.size, 6);224});225226test('manages tags correctly', () => {227single.expand(single.root.id, Infinity);228single.collectDiff();229const tag1 = new TestTag('tag1');230const tag2 = new TestTag('tag2');231const tag3 = new TestTag('tag3');232const child = new TestItemImpl('ctrlId', 'id-ac', 'c', undefined);233child.tags = [tag1, tag2];234single.root.children.get('id-a')!.children.add(child);235236assert.deepStrictEqual(single.collectDiff(), [237{ op: TestDiffOpType.AddTag, tag: { id: 'ctrlId\0tag1' } },238{ op: TestDiffOpType.AddTag, tag: { id: 'ctrlId\0tag2' } },239{240op: TestDiffOpType.Add, item: {241controllerId: 'ctrlId',242expand: TestItemExpandState.NotExpandable,243item: convert.TestItem.from(child),244}245},246]);247248child.tags = [tag2, tag3];249assert.deepStrictEqual(single.collectDiff(), [250{ op: TestDiffOpType.AddTag, tag: { id: 'ctrlId\0tag3' } },251{252op: TestDiffOpType.Update, item: {253extId: new TestId(['ctrlId', 'id-a', 'id-ac']).toString(),254item: { tags: ['ctrlId\0tag2', 'ctrlId\0tag3'] }255}256},257{ op: TestDiffOpType.RemoveTag, id: 'ctrlId\0tag1' },258]);259260const a = single.root.children.get('id-a')!;261a.tags = [tag2];262a.children.replace([]);263assert.deepStrictEqual(single.collectDiff().filter(t => t.op === TestDiffOpType.RemoveTag), [264{ op: TestDiffOpType.RemoveTag, id: 'ctrlId\0tag3' },265]);266});267268test('replaces on uri change', () => {269single.expand(single.root.id, Infinity);270single.collectDiff();271272const oldA = single.root.children.get('id-a') as TestItemImpl;273const uri = single.root.children.get('id-a')!.uri?.with({ path: '/different' });274const newA = new TestItemImpl('ctrlId', 'id-a', 'Hello world', uri);275newA.children.replace([...oldA.children].map(([_, item]) => item));276single.root.children.replace([...single.root.children].map(([id, i]) => id === 'id-a' ? newA : i));277278assert.deepStrictEqual(single.collectDiff(), [279{ op: TestDiffOpType.Remove, itemId: new TestId(['ctrlId', 'id-a']).toString() },280{281op: TestDiffOpType.Add,282item: { controllerId: 'ctrlId', expand: TestItemExpandState.NotExpandable, item: { ...convert.TestItem.from(newA) } }283},284{285op: TestDiffOpType.Add,286item: { controllerId: 'ctrlId', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(newA.children.get('id-aa') as TestItemImpl) }287},288{289op: TestDiffOpType.Add,290item: { controllerId: 'ctrlId', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(newA.children.get('id-ab') as TestItemImpl) }291},292]);293});294295test('treats in-place replacement as mutation', () => {296single.expand(single.root.id, Infinity);297single.collectDiff();298299const oldA = single.root.children.get('id-a') as TestItemImpl;300const uri = single.root.children.get('id-a')!.uri;301const newA = new TestItemImpl('ctrlId', 'id-a', 'Hello world', uri);302newA.children.replace([...oldA.children].map(([_, item]) => item));303single.root.children.replace([304newA,305new TestItemImpl('ctrlId', 'id-b', single.root.children.get('id-b')!.label, uri),306]);307308assert.deepStrictEqual(single.collectDiff(), [309{310op: TestDiffOpType.Update,311item: { extId: new TestId(['ctrlId', 'id-a']).toString(), item: { label: 'Hello world' } },312},313{314op: TestDiffOpType.DocumentSynced,315docv: undefined,316uri: uri317}318]);319320newA.label = 'still connected';321assert.deepStrictEqual(single.collectDiff(), [322{323op: TestDiffOpType.Update,324item: { extId: new TestId(['ctrlId', 'id-a']).toString(), item: { label: 'still connected' } }325},326]);327328oldA.label = 'no longer connected';329assert.deepStrictEqual(single.collectDiff(), []);330});331332suite('expandibility restoration', () => {333const doReplace = async (canResolveChildren = true) => {334const uri = single.root.children.get('id-a')!.uri;335const newA = new TestItemImpl('ctrlId', 'id-a', 'Hello world', uri);336newA.canResolveChildren = canResolveChildren;337single.root.children.replace([338newA,339new TestItemImpl('ctrlId', 'id-b', single.root.children.get('id-b')!.label, uri),340]);341await timeout(0); // drain microtasks342};343344test('does not restore an unexpanded state', async () => {345await single.expand(single.root.id, 0);346assert.deepStrictEqual(resolveCalls, [undefined]);347await doReplace();348assert.deepStrictEqual(resolveCalls, [undefined]);349});350351test('restores resolve state on replacement', async () => {352await single.expand(single.root.id, Infinity);353assert.deepStrictEqual(resolveCalls, [undefined, 'id-a']);354await doReplace();355assert.deepStrictEqual(resolveCalls, [undefined, 'id-a', 'id-a']);356});357358test('does not expand if new child is not expandable', async () => {359await single.expand(single.root.id, Infinity);360assert.deepStrictEqual(resolveCalls, [undefined, 'id-a']);361await doReplace(false);362assert.deepStrictEqual(resolveCalls, [undefined, 'id-a']);363});364});365366test('treats in-place replacement as mutation deeply', () => {367single.expand(single.root.id, Infinity);368single.collectDiff();369370const oldA = single.root.children.get('id-a')!;371const uri = oldA.uri;372const newA = new TestItemImpl('ctrlId', 'id-a', single.root.children.get('id-a')!.label, uri);373const oldAA = oldA.children.get('id-aa')!;374const oldAB = oldA.children.get('id-ab')!;375const newAB = new TestItemImpl('ctrlId', 'id-ab', 'Hello world', uri);376newA.children.replace([oldAA, newAB]);377single.root.children.replace([newA, single.root.children.get('id-b')!]);378379assert.deepStrictEqual(single.collectDiff(), [380{381op: TestDiffOpType.Update,382item: { extId: TestId.fromExtHostTestItem(oldAB, 'ctrlId').toString(), item: { label: 'Hello world' } },383},384{385op: TestDiffOpType.DocumentSynced,386docv: undefined,387uri: uri388}389]);390391oldAA.label = 'still connected1';392newAB.label = 'still connected2';393oldAB.label = 'not connected3';394assert.deepStrictEqual(single.collectDiff(), [395{396op: TestDiffOpType.Update,397item: { extId: new TestId(['ctrlId', 'id-a', 'id-aa']).toString(), item: { label: 'still connected1' } }398},399{400op: TestDiffOpType.Update,401item: { extId: new TestId(['ctrlId', 'id-a', 'id-ab']).toString(), item: { label: 'still connected2' } }402},403]);404405assert.strictEqual(newAB.parent, newA);406assert.strictEqual(oldAA.parent, newA);407assert.deepStrictEqual(newA.parent, undefined);408});409410test('moves an item to be a new child', async () => {411await single.expand(single.root.id, 0);412single.collectDiff();413const b = single.root.children.get('id-b') as TestItemImpl;414const a = single.root.children.get('id-a') as TestItemImpl;415a.children.add(b);416assert.deepStrictEqual(single.collectDiff(), [417{418op: TestDiffOpType.Remove,419itemId: new TestId(['ctrlId', 'id-b']).toString(),420},421{422op: TestDiffOpType.Add,423item: { controllerId: 'ctrlId', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(b) }424},425]);426427b.label = 'still connected';428assert.deepStrictEqual(single.collectDiff(), [429{430op: TestDiffOpType.Update,431item: { extId: new TestId(['ctrlId', 'id-a', 'id-b']).toString(), item: { label: 'still connected' } }432},433]);434435assert.deepStrictEqual([...single.root.children].map(([_, item]) => item), [single.root.children.get('id-a')]);436assert.deepStrictEqual(b.parent, a);437});438439test('sends document sync events', async () => {440await single.expand(single.root.id, 0);441single.collectDiff();442443const a = single.root.children.get('id-a') as TestItemImpl;444a.range = new Range(new Position(0, 0), new Position(1, 0));445446assert.deepStrictEqual(single.collectDiff(), [447{448op: TestDiffOpType.DocumentSynced,449docv: undefined,450uri: URI.file('/')451},452{453op: TestDiffOpType.Update,454item: {455extId: new TestId(['ctrlId', 'id-a']).toString(),456item: {457range: editorRange.Range.lift({458endColumn: 1,459endLineNumber: 2,460startColumn: 1,461startLineNumber: 1462})463}464},465},466]);467468// sends on replace even if it's a no-op469a.range = a.range;470assert.deepStrictEqual(single.collectDiff(), [471{472op: TestDiffOpType.DocumentSynced,473docv: undefined,474uri: URI.file('/')475},476]);477478// sends on a child replacement479const uri = URI.file('/');480const a2 = new TestItemImpl('ctrlId', 'id-a', 'a', uri);481a2.range = a.range;482single.root.children.replace([a2, single.root.children.get('id-b')!]);483assert.deepStrictEqual(single.collectDiff(), [484{485op: TestDiffOpType.DocumentSynced,486docv: undefined,487uri488},489]);490});491});492493494suite('MirroredTestCollection', () => {495// todo@connor4312: re-renable when we figure out what observing looks like we async children496// let m: TestMirroredCollection;497// setup(() => m = new TestMirroredCollection());498499// test('mirrors creation of the root', () => {500// const tests = testStubs.nested();501// single.addRoot(tests, 'pid');502// single.expand(single.root.id, Infinity);503// m.apply(single.collectDiff());504// assertTreesEqual(m.rootTestItems[0], owned.getTestById(single.root.id)![1].actual);505// assert.strictEqual(m.length, single.itemToInternal.size);506// });507508// test('mirrors node deletion', () => {509// const tests = testStubs.nested();510// single.addRoot(tests, 'pid');511// m.apply(single.collectDiff());512// single.expand(single.root.id, Infinity);513// tests.children!.splice(0, 1);514// single.onItemChange(tests, 'pid');515// single.expand(single.root.id, Infinity);516// m.apply(single.collectDiff());517518// assertTreesEqual(m.rootTestItems[0], owned.getTestById(single.root.id)![1].actual);519// assert.strictEqual(m.length, single.itemToInternal.size);520// });521522// test('mirrors node addition', () => {523// const tests = testStubs.nested();524// single.addRoot(tests, 'pid');525// m.apply(single.collectDiff());526// tests.children![0].children!.push(stubTest('ac'));527// single.onItemChange(tests, 'pid');528// m.apply(single.collectDiff());529530// assertTreesEqual(m.rootTestItems[0], owned.getTestById(single.root.id)![1].actual);531// assert.strictEqual(m.length, single.itemToInternal.size);532// });533534// test('mirrors node update', () => {535// const tests = testStubs.nested();536// single.addRoot(tests, 'pid');537// m.apply(single.collectDiff());538// tests.children![0].description = 'Hello world'; /* item a */539// single.onItemChange(tests, 'pid');540// m.apply(single.collectDiff());541542// assertTreesEqual(m.rootTestItems[0], owned.getTestById(single.root.id)![1].actual);543// });544545// suite('MirroredChangeCollector', () => {546// let tests = testStubs.nested();547// setup(() => {548// tests = testStubs.nested();549// single.addRoot(tests, 'pid');550// m.apply(single.collectDiff());551// });552553// test('creates change for root', () => {554// assertTreeListEqual(m.changeEvent.added, [555// tests,556// tests.children[0],557// tests.children![0].children![0],558// tests.children![0].children![1],559// tests.children[1],560// ]);561// assertTreeListEqual(m.changeEvent.removed, []);562// assertTreeListEqual(m.changeEvent.updated, []);563// });564565// test('creates change for delete', () => {566// const rm = tests.children.shift()!;567// single.onItemChange(tests, 'pid');568// m.apply(single.collectDiff());569570// assertTreeListEqual(m.changeEvent.added, []);571// assertTreeListEqual(m.changeEvent.removed, [572// { ...rm },573// { ...rm.children![0] },574// { ...rm.children![1] },575// ]);576// assertTreeListEqual(m.changeEvent.updated, []);577// });578579// test('creates change for update', () => {580// tests.children[0].label = 'updated!';581// single.onItemChange(tests, 'pid');582// m.apply(single.collectDiff());583584// assertTreeListEqual(m.changeEvent.added, []);585// assertTreeListEqual(m.changeEvent.removed, []);586// assertTreeListEqual(m.changeEvent.updated, [tests.children[0]]);587// });588589// test('is a no-op if a node is added and removed', () => {590// const nested = testStubs.nested('id2-');591// tests.children.push(nested);592// single.onItemChange(tests, 'pid');593// tests.children.pop();594// single.onItemChange(tests, 'pid');595// const previousEvent = m.changeEvent;596// m.apply(single.collectDiff());597// assert.strictEqual(m.changeEvent, previousEvent);598// });599600// test('is a single-op if a node is added and changed', () => {601// const child = stubTest('c');602// tests.children.push(child);603// single.onItemChange(tests, 'pid');604// child.label = 'd';605// single.onItemChange(tests, 'pid');606// m.apply(single.collectDiff());607608// assertTreeListEqual(m.changeEvent.added, [child]);609// assertTreeListEqual(m.changeEvent.removed, []);610// assertTreeListEqual(m.changeEvent.updated, []);611// });612613// test('gets the common ancestor (1)', () => {614// tests.children![0].children![0].label = 'za';615// tests.children![0].children![1].label = 'zb';616// single.onItemChange(tests, 'pid');617// m.apply(single.collectDiff());618619// });620621// test('gets the common ancestor (2)', () => {622// tests.children![0].children![0].label = 'za';623// tests.children![1].label = 'ab';624// single.onItemChange(tests, 'pid');625// m.apply(single.collectDiff());626// });627// });628});629630suite('TestRunTracker', () => {631let proxy: MockObject<MainThreadTestingShape>;632let c: TestRunCoordinator;633let cts: CancellationTokenSource;634let configuration: TestRunProfileImpl;635636let req: TestRunRequest;637638let dto: TestRunDto;639// eslint-disable-next-line local/code-no-any-casts640const ext: IExtensionDescription = {} as any;641642teardown(() => {643for (const { id } of c.trackers) {644c.disposeTestRun(id);645}646});647648setup(async () => {649proxy = mockObject<MainThreadTestingShape>()();650cts = new CancellationTokenSource();651c = new TestRunCoordinator(proxy, new NullLogService());652653configuration = new TestRunProfileImpl(mockObject<MainThreadTestingShape>()(), new Map(), new Set(), Event.None, 'ctrlId', 42, 'Do Run', TestRunProfileKind.Run, () => { }, false);654655await single.expand(single.root.id, Infinity);656single.collectDiff();657658req = {659include: undefined,660exclude: [single.root.children.get('id-b')!],661profile: configuration,662preserveFocus: false,663};664665dto = TestRunDto.fromInternal({666controllerId: 'ctrl',667profileId: configuration.profileId,668excludeExtIds: ['id-b'],669runId: 'run-id',670testIds: [single.root.id],671}, single);672});673674test('tracks a run started from a main thread request', () => {675const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token));676assert.strictEqual(tracker.hasRunningTasks, false);677678const task1 = c.createTestRun(ext, 'ctrl', single, req, 'run1', true);679const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true);680assert.strictEqual(proxy.$startedExtensionTestRun.called, false);681assert.strictEqual(tracker.hasRunningTasks, true);682683task1.appendOutput('hello');684const taskId = proxy.$appendOutputToRun.args[0]?.[1];685assert.deepStrictEqual([['run-id', taskId, VSBuffer.fromString('hello'), undefined, undefined]], proxy.$appendOutputToRun.args);686task1.end();687688assert.strictEqual(proxy.$finishedExtensionTestRun.called, false);689assert.strictEqual(tracker.hasRunningTasks, true);690691task2.end();692693assert.strictEqual(proxy.$finishedExtensionTestRun.called, false);694assert.strictEqual(tracker.hasRunningTasks, false);695});696697test('run cancel force ends after a timeout', () => {698const clock = sinon.useFakeTimers();699try {700const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token));701const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true);702const onEnded = sinon.stub();703ds.add(tracker.onEnd(onEnded));704705assert.strictEqual(task.token.isCancellationRequested, false);706assert.strictEqual(tracker.hasRunningTasks, true);707tracker.cancel();708709assert.strictEqual(task.token.isCancellationRequested, true);710assert.strictEqual(tracker.hasRunningTasks, true);711712clock.tick(9999);713assert.strictEqual(tracker.hasRunningTasks, true);714assert.strictEqual(onEnded.called, false);715716clock.tick(1);717assert.strictEqual(onEnded.called, true);718assert.strictEqual(tracker.hasRunningTasks, false);719} finally {720clock.restore();721}722});723724test('run cancel force ends on second cancellation request', () => {725const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token));726const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true);727const onEnded = sinon.stub();728ds.add(tracker.onEnd(onEnded));729730assert.strictEqual(task.token.isCancellationRequested, false);731assert.strictEqual(tracker.hasRunningTasks, true);732tracker.cancel();733734assert.strictEqual(task.token.isCancellationRequested, true);735assert.strictEqual(tracker.hasRunningTasks, true);736assert.strictEqual(onEnded.called, false);737tracker.cancel();738739assert.strictEqual(tracker.hasRunningTasks, false);740assert.strictEqual(onEnded.called, true);741});742743test('tracks a run started from an extension request', () => {744const task1 = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false);745746const tracker = Iterable.first(c.trackers)!;747assert.strictEqual(tracker.hasRunningTasks, true);748assert.deepStrictEqual(proxy.$startedExtensionTestRun.args, [749[{750profile: { group: 2, id: 42 },751controllerId: 'ctrl',752id: tracker.id,753include: [single.root.id],754exclude: [new TestId(['ctrlId', 'id-b']).toString()],755persist: false,756continuous: false,757preserveFocus: false,758}]759]);760761const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true);762const task3Detached = c.createTestRun(ext, 'ctrl', single, { ...req }, 'task3Detached', true);763764task1.end();765assert.strictEqual(proxy.$finishedExtensionTestRun.called, false);766assert.strictEqual(tracker.hasRunningTasks, true);767768task2.end();769assert.deepStrictEqual(proxy.$finishedExtensionTestRun.args, [[tracker.id]]);770assert.strictEqual(tracker.hasRunningTasks, false);771772task3Detached.end();773});774775test('adds tests to run smartly', () => {776const task1 = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false);777const tracker = Iterable.first(c.trackers)!;778const expectedArgs: unknown[][] = [];779assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs);780781task1.passed(single.root.children.get('id-a')!.children.get('id-aa')!);782expectedArgs.push([783'ctrlId',784tracker.id,785[786convert.TestItem.from(single.root),787convert.TestItem.from(single.root.children.get('id-a') as TestItemImpl),788convert.TestItem.from(single.root.children.get('id-a')!.children.get('id-aa') as TestItemImpl),789]790]);791assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs);792793task1.enqueued(single.root.children.get('id-a')!.children.get('id-ab')!);794expectedArgs.push([795'ctrlId',796tracker.id,797[798convert.TestItem.from(single.root.children.get('id-a') as TestItemImpl),799convert.TestItem.from(single.root.children.get('id-a')!.children.get('id-ab') as TestItemImpl),800],801]);802assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs);803804task1.passed(single.root.children.get('id-a')!.children.get('id-ab')!);805assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs);806807task1.end();808});809810test('adds test messages to run', () => {811const test1 = new TestItemImpl('ctrlId', 'id-c', 'test c', URI.file('/testc.txt'));812const test2 = new TestItemImpl('ctrlId', 'id-d', 'test d', URI.file('/testd.txt'));813test1.range = test2.range = new Range(new Position(0, 0), new Position(1, 0));814single.root.children.replace([test1, test2]);815const task = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false);816817const message1 = new TestMessage('some message');818message1.location = new Location(URI.file('/a.txt'), new Position(0, 0));819task.failed(test1, message1);820821const args = proxy.$appendTestMessagesInRun.args[0];822assert.deepStrictEqual(proxy.$appendTestMessagesInRun.args[0], [823args[0],824args[1],825new TestId(['ctrlId', 'id-c']).toString(),826[{827message: 'some message',828type: TestMessageType.Error,829expected: undefined,830contextValue: undefined,831actual: undefined,832location: convert.location.from(message1.location),833stackTrace: undefined,834}]835]);836837// should use test location as default838task.failed(test2, new TestMessage('some message'));839assert.deepStrictEqual(proxy.$appendTestMessagesInRun.args[1], [840args[0],841args[1],842new TestId(['ctrlId', 'id-d']).toString(),843[{844message: 'some message',845type: TestMessageType.Error,846contextValue: undefined,847expected: undefined,848actual: undefined,849location: convert.location.from({ uri: test2.uri!, range: test2.range }),850stackTrace: undefined,851}]852]);853854task.end();855});856857test('guards calls after runs are ended', () => {858const task = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false);859task.end();860861task.failed(single.root, new TestMessage('some message'));862task.appendOutput('output');863864assert.strictEqual(proxy.$addTestsToRun.called, false);865assert.strictEqual(proxy.$appendOutputToRun.called, false);866assert.strictEqual(proxy.$appendTestMessagesInRun.called, false);867});868869test('sets state of test with identical local IDs (#131827)', () => {870const testA = single.root.children.get('id-a');871const testB = single.root.children.get('id-b');872const childA = new TestItemImpl('ctrlId', 'id-child', 'child', undefined);873testA!.children.replace([childA]);874const childB = new TestItemImpl('ctrlId', 'id-child', 'child', undefined);875testB!.children.replace([childB]);876877const task1 = c.createTestRun(ext, 'ctrl', single, new TestRunRequestImpl(), 'hello world', false);878const tracker = Iterable.first(c.trackers)!;879880task1.passed(childA);881task1.passed(childB);882assert.deepStrictEqual(proxy.$addTestsToRun.args, [883[884'ctrl',885tracker.id,886[single.root, testA, childA].map(t => convert.TestItem.from(t as TestItemImpl)),887],888[889'ctrl',890tracker.id,891[single.root, testB, childB].map(t => convert.TestItem.from(t as TestItemImpl)),892],893]);894895task1.end();896});897});898899suite('service', () => {900let ctrl: TestExtHostTesting;901902class TestExtHostTesting extends ExtHostTesting {903public getProfileInternalId(ctrl: TestController, profile: TestRunProfile) {904for (const [id, p] of this.controllers.get(ctrl.id)!.profiles) {905if (profile === p) {906return id;907}908}909910throw new Error('profile not found');911}912}913914setup(() => {915const rpcProtocol = AnyCallRPCProtocol();916ctrl = ds.add(new TestExtHostTesting(917rpcProtocol,918new NullLogService(),919new ExtHostCommands(rpcProtocol, new NullLogService(), new class extends mock<IExtHostTelemetry>() {920override onExtensionError(): boolean {921return true;922}923}),924new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()),925));926});927928test('exposes active profiles correctly', async () => {929const extA = { ...nullExtensionDescription, identifier: new ExtensionIdentifier('ext.a'), enabledApiProposals: ['testingActiveProfile'] };930const extB = { ...nullExtensionDescription, identifier: new ExtensionIdentifier('ext.b'), enabledApiProposals: ['testingActiveProfile'] };931932const ctrlA = ds.add(ctrl.createTestController(extA, 'a', 'ctrla'));933const profAA = ds.add(ctrlA.createRunProfile('aa', TestRunProfileKind.Run, () => { }));934const profAB = ds.add(ctrlA.createRunProfile('ab', TestRunProfileKind.Run, () => { }));935936const ctrlB = ds.add(ctrl.createTestController(extB, 'b', 'ctrlb'));937const profBA = ds.add(ctrlB.createRunProfile('ba', TestRunProfileKind.Run, () => { }));938const profBB = ds.add(ctrlB.createRunProfile('bb', TestRunProfileKind.Run, () => { }));939const neverCalled = sinon.stub();940941// empty default state:942assert.deepStrictEqual(profAA.isDefault, false);943assert.deepStrictEqual(profBA.isDefault, false);944assert.deepStrictEqual(profBB.isDefault, false);945946// fires a change event:947const changeA = Event.toPromise(profAA.onDidChangeDefault as Event<boolean>);948const changeBA = Event.toPromise(profBA.onDidChangeDefault as Event<boolean>);949const changeBB = Event.toPromise(profBB.onDidChangeDefault as Event<boolean>);950951ds.add(profAB.onDidChangeDefault(neverCalled));952assert.strictEqual(neverCalled.called, false);953954ctrl.$setDefaultRunProfiles({955a: [ctrl.getProfileInternalId(ctrlA, profAA)],956b: [ctrl.getProfileInternalId(ctrlB, profBA), ctrl.getProfileInternalId(ctrlB, profBB)]957});958959assert.deepStrictEqual(await changeA, true);960assert.deepStrictEqual(await changeBA, true);961assert.deepStrictEqual(await changeBB, true);962963// updates internal state:964assert.deepStrictEqual(profAA.isDefault, true);965assert.deepStrictEqual(profBA.isDefault, true);966assert.deepStrictEqual(profBB.isDefault, true);967assert.deepStrictEqual(profAB.isDefault, false);968969// no-ops if equal970ds.add(profAA.onDidChangeDefault(neverCalled));971ctrl.$setDefaultRunProfiles({972a: [ctrl.getProfileInternalId(ctrlA, profAA)],973});974assert.strictEqual(neverCalled.called, false);975});976});977});978979980