/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Iterable } from 'vs/base/common/iterator'; import { URI } from 'vs/base/common/uri'; import { mockObject, MockObject } from 'vs/base/test/common/mock'; import { MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; import { TestRunCoordinator, TestRunDto, TestRunProfileImpl } from 'vs/workbench/api/common/extHostTesting'; import * as convert from 'vs/workbench/api/common/extHostTypeConverters'; import { Location, Position, Range, TestMessage, TestResultState, TestRunProfileKind, TestRunRequest as TestRunRequestImpl, TestTag } from 'vs/workbench/api/common/extHostTypes'; import { TestDiffOpType, TestItemExpandState, TestMessageType } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestItemImpl, testStubs } from 'vs/workbench/contrib/testing/test/common/testStubs'; import { TestSingleUseCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; import type { TestItem, TestRunRequest } from 'vscode'; const simplify = (item: TestItem) => ({ id: item.id, label: item.label, uri: item.uri, range: item.range, }); const assertTreesEqual = (a: TestItemImpl | undefined, b: TestItemImpl | undefined) => { if (!a) { throw new assert.AssertionError({ message: 'Expected a to be defined', actual: a }); } if (!b) { throw new assert.AssertionError({ message: 'Expected b to be defined', actual: b }); } assert.deepStrictEqual(simplify(a), simplify(b)); const aChildren = [...a.children].map(c => c.id).sort(); const bChildren = [...b.children].map(c => c.id).sort(); assert.strictEqual(aChildren.length, bChildren.length, `expected ${a.label}.children.length == ${b.label}.children.length`); aChildren.forEach(key => assertTreesEqual(a.children.get(key) as TestItemImpl, b.children.get(key) as TestItemImpl)); }; // const assertTreeListEqual = (a: ReadonlyArray, b: ReadonlyArray) => { // assert.strictEqual(a.length, b.length, `expected a.length == n.length`); // a.forEach((_, i) => assertTreesEqual(a[i], b[i])); // }; // class TestMirroredCollection extends MirroredTestCollection { // public changeEvent!: TestChangeEvent; // constructor() { // super(); // this.onDidChangeTests(evt => this.changeEvent = evt); // } // public get length() { // return this.items.size; // } // } suite('ExtHost Testing', () => { let single: TestSingleUseCollection; setup(() => { single = testStubs.nested(); single.onDidGenerateDiff(d => single.setDiff(d /* don't clear during testing */)); }); teardown(() => { single.dispose(); }); suite('OwnedTestCollection', () => { test('adds a root recursively', async () => { await single.expand(single.root.id, Infinity); const a = single.root.children.get('id-a') as TestItemImpl; const b = single.root.children.get('id-b') as TestItemImpl; assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.Add, item: { controllerId: 'ctrlId', parent: null, expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(single.root) } } }, { op: TestDiffOpType.Add, item: { controllerId: 'ctrlId', parent: single.root.id, expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(a) } } }, { op: TestDiffOpType.Add, item: { controllerId: 'ctrlId', parent: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(a.children.get('id-aa') as TestItemImpl) } }, { op: TestDiffOpType.Add, item: { controllerId: 'ctrlId', parent: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(a.children.get('id-ab') as TestItemImpl) } }, { op: TestDiffOpType.Update, item: { extId: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.Expanded } }, { op: TestDiffOpType.Add, item: { controllerId: 'ctrlId', parent: single.root.id, expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(b) } }, { op: TestDiffOpType.Update, item: { extId: single.root.id, expand: TestItemExpandState.Expanded } }, ]); }); test('parents are set correctly', () => { single.expand(single.root.id, Infinity); single.collectDiff(); const a = single.root.children.get('id-a')!; const ab = a.children.get('id-ab')!; assert.strictEqual(a.parent, undefined); assert.strictEqual(ab.parent, a); }); test('no-ops if items not changed', () => { single.collectDiff(); assert.deepStrictEqual(single.collectDiff(), []); }); test('watches property mutations', () => { single.expand(single.root.id, Infinity); single.collectDiff(); single.root.children.get('id-a')!.description = 'Hello world'; /* item a */ assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.Update, item: { extId: new TestId(['ctrlId', 'id-a']).toString(), item: { description: 'Hello world' } }, } ]); }); test('removes children', () => { single.expand(single.root.id, Infinity); single.collectDiff(); single.root.children.delete('id-a'); assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.Remove, itemId: new TestId(['ctrlId', 'id-a']).toString() }, ]); assert.deepStrictEqual( [...single.tree.keys()].sort(), [single.root.id, new TestId(['ctrlId', 'id-b']).toString()], ); assert.strictEqual(single.tree.size, 2); }); test('adds new children', () => { single.expand(single.root.id, Infinity); single.collectDiff(); const child = new TestItemImpl('ctrlId', 'id-ac', 'c', undefined); single.root.children.get('id-a')!.children.add(child); assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.Add, item: { controllerId: 'ctrlId', parent: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(child), } }, ]); assert.deepStrictEqual( [...single.tree.values()].map(n => n.actual.id).sort(), [single.root.id, 'id-a', 'id-aa', 'id-ab', 'id-ac', 'id-b'], ); assert.strictEqual(single.tree.size, 6); }); test('manages tags correctly', () => { single.expand(single.root.id, Infinity); single.collectDiff(); const tag1 = new TestTag('tag1'); const tag2 = new TestTag('tag2'); const tag3 = new TestTag('tag3'); const child = new TestItemImpl('ctrlId', 'id-ac', 'c', undefined); child.tags = [tag1, tag2]; single.root.children.get('id-a')!.children.add(child); assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.AddTag, tag: { ctrlLabel: 'root', id: 'ctrlId\0tag1' } }, { op: TestDiffOpType.AddTag, tag: { ctrlLabel: 'root', id: 'ctrlId\0tag2' } }, { op: TestDiffOpType.Add, item: { controllerId: 'ctrlId', parent: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(child), } }, ]); child.tags = [tag2, tag3]; assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.AddTag, tag: { ctrlLabel: 'root', id: 'ctrlId\0tag3' } }, { op: TestDiffOpType.Update, item: { extId: new TestId(['ctrlId', 'id-a', 'id-ac']).toString(), item: { tags: ['ctrlId\0tag2', 'ctrlId\0tag3'] } } }, { op: TestDiffOpType.RemoveTag, id: 'ctrlId\0tag1' }, ]); const a = single.root.children.get('id-a')!; a.tags = [tag2]; a.children.replace([]); assert.deepStrictEqual(single.collectDiff().filter(t => t.op === TestDiffOpType.RemoveTag), [ { op: TestDiffOpType.RemoveTag, id: 'ctrlId\0tag3' }, ]); }); test('treats in-place replacement as mutation', () => { single.expand(single.root.id, Infinity); single.collectDiff(); const oldA = single.root.children.get('id-a') as TestItemImpl; const newA = new TestItemImpl('ctrlId', 'id-a', 'Hello world', undefined); newA.children.replace([...oldA.children]); single.root.children.replace([ newA, new TestItemImpl('ctrlId', 'id-b', single.root.children.get('id-b')!.label, undefined), ]); assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.Update, item: { extId: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.Expanded, item: { label: 'Hello world' } }, }, ]); newA.label = 'still connected'; assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.Update, item: { extId: new TestId(['ctrlId', 'id-a']).toString(), item: { label: 'still connected' } } }, ]); oldA.label = 'no longer connected'; assert.deepStrictEqual(single.collectDiff(), []); }); test('treats in-place replacement as mutation deeply', () => { single.expand(single.root.id, Infinity); single.collectDiff(); const oldA = single.root.children.get('id-a')!; const newA = new TestItemImpl('ctrlId', 'id-a', single.root.children.get('id-a')!.label, undefined); const oldAA = oldA.children.get('id-aa')!; const oldAB = oldA.children.get('id-ab')!; const newAB = new TestItemImpl('ctrlId', 'id-ab', 'Hello world', undefined); newA.children.replace([oldAA, newAB]); single.root.children.replace([newA, single.root.children.get('id-b')!]); assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.Update, item: { extId: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.Expanded }, }, { op: TestDiffOpType.Update, item: { extId: TestId.fromExtHostTestItem(oldAB, 'ctrlId').toString(), item: { label: 'Hello world' } }, }, ]); oldAA.label = 'still connected1'; newAB.label = 'still connected2'; oldAB.label = 'not connected3'; assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.Update, item: { extId: new TestId(['ctrlId', 'id-a', 'id-aa']).toString(), item: { label: 'still connected1' } } }, { op: TestDiffOpType.Update, item: { extId: new TestId(['ctrlId', 'id-a', 'id-ab']).toString(), item: { label: 'still connected2' } } }, ]); assert.strictEqual(newAB.parent, newA); assert.strictEqual(oldAA.parent, newA); assert.deepStrictEqual(newA.parent, undefined); }); test('moves an item to be a new child', async () => { await single.expand(single.root.id, 0); single.collectDiff(); const b = single.root.children.get('id-b') as TestItemImpl; const a = single.root.children.get('id-a') as TestItemImpl; a.children.add(b); assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.Remove, itemId: new TestId(['ctrlId', 'id-b']).toString(), }, { op: TestDiffOpType.Add, item: { controllerId: 'ctrlId', parent: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(b) } }, ]); b.label = 'still connected'; assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.Update, item: { extId: new TestId(['ctrlId', 'id-a', 'id-b']).toString(), item: { label: 'still connected' } } }, ]); assert.deepStrictEqual([...single.root.children], [single.root.children.get('id-a')]); assert.deepStrictEqual(b.parent, a); }); }); suite('MirroredTestCollection', () => { // todo@connor4312: re-renable when we figure out what observing looks like we async children // let m: TestMirroredCollection; // setup(() => m = new TestMirroredCollection()); // test('mirrors creation of the root', () => { // const tests = testStubs.nested(); // single.addRoot(tests, 'pid'); // single.expand(single.root.id, Infinity); // m.apply(single.collectDiff()); // assertTreesEqual(m.rootTestItems[0], owned.getTestById(single.root.id)![1].actual); // assert.strictEqual(m.length, single.itemToInternal.size); // }); // test('mirrors node deletion', () => { // const tests = testStubs.nested(); // single.addRoot(tests, 'pid'); // m.apply(single.collectDiff()); // single.expand(single.root.id, Infinity); // tests.children!.splice(0, 1); // single.onItemChange(tests, 'pid'); // single.expand(single.root.id, Infinity); // m.apply(single.collectDiff()); // assertTreesEqual(m.rootTestItems[0], owned.getTestById(single.root.id)![1].actual); // assert.strictEqual(m.length, single.itemToInternal.size); // }); // test('mirrors node addition', () => { // const tests = testStubs.nested(); // single.addRoot(tests, 'pid'); // m.apply(single.collectDiff()); // tests.children![0].children!.push(stubTest('ac')); // single.onItemChange(tests, 'pid'); // m.apply(single.collectDiff()); // assertTreesEqual(m.rootTestItems[0], owned.getTestById(single.root.id)![1].actual); // assert.strictEqual(m.length, single.itemToInternal.size); // }); // test('mirrors node update', () => { // const tests = testStubs.nested(); // single.addRoot(tests, 'pid'); // m.apply(single.collectDiff()); // tests.children![0].description = 'Hello world'; /* item a */ // single.onItemChange(tests, 'pid'); // m.apply(single.collectDiff()); // assertTreesEqual(m.rootTestItems[0], owned.getTestById(single.root.id)![1].actual); // }); // suite('MirroredChangeCollector', () => { // let tests = testStubs.nested(); // setup(() => { // tests = testStubs.nested(); // single.addRoot(tests, 'pid'); // m.apply(single.collectDiff()); // }); // test('creates change for root', () => { // assertTreeListEqual(m.changeEvent.added, [ // tests, // tests.children[0], // tests.children![0].children![0], // tests.children![0].children![1], // tests.children[1], // ]); // assertTreeListEqual(m.changeEvent.removed, []); // assertTreeListEqual(m.changeEvent.updated, []); // }); // test('creates change for delete', () => { // const rm = tests.children.shift()!; // single.onItemChange(tests, 'pid'); // m.apply(single.collectDiff()); // assertTreeListEqual(m.changeEvent.added, []); // assertTreeListEqual(m.changeEvent.removed, [ // { ...rm }, // { ...rm.children![0] }, // { ...rm.children![1] }, // ]); // assertTreeListEqual(m.changeEvent.updated, []); // }); // test('creates change for update', () => { // tests.children[0].label = 'updated!'; // single.onItemChange(tests, 'pid'); // m.apply(single.collectDiff()); // assertTreeListEqual(m.changeEvent.added, []); // assertTreeListEqual(m.changeEvent.removed, []); // assertTreeListEqual(m.changeEvent.updated, [tests.children[0]]); // }); // test('is a no-op if a node is added and removed', () => { // const nested = testStubs.nested('id2-'); // tests.children.push(nested); // single.onItemChange(tests, 'pid'); // tests.children.pop(); // single.onItemChange(tests, 'pid'); // const previousEvent = m.changeEvent; // m.apply(single.collectDiff()); // assert.strictEqual(m.changeEvent, previousEvent); // }); // test('is a single-op if a node is added and changed', () => { // const child = stubTest('c'); // tests.children.push(child); // single.onItemChange(tests, 'pid'); // child.label = 'd'; // single.onItemChange(tests, 'pid'); // m.apply(single.collectDiff()); // assertTreeListEqual(m.changeEvent.added, [child]); // assertTreeListEqual(m.changeEvent.removed, []); // assertTreeListEqual(m.changeEvent.updated, []); // }); // test('gets the common ancestor (1)', () => { // tests.children![0].children![0].label = 'za'; // tests.children![0].children![1].label = 'zb'; // single.onItemChange(tests, 'pid'); // m.apply(single.collectDiff()); // }); // test('gets the common ancestor (2)', () => { // tests.children![0].children![0].label = 'za'; // tests.children![1].label = 'ab'; // single.onItemChange(tests, 'pid'); // m.apply(single.collectDiff()); // }); // }); }); suite('TestRunTracker', () => { let proxy: MockObject; let c: TestRunCoordinator; let cts: CancellationTokenSource; let configuration: TestRunProfileImpl; let req: TestRunRequest; let dto: TestRunDto; setup(async () => { proxy = mockObject()(); cts = new CancellationTokenSource(); c = new TestRunCoordinator(proxy); configuration = new TestRunProfileImpl(mockObject()(), 'ctrlId', 42, 'Do Run', TestRunProfileKind.Run, () => { }, false); await single.expand(single.root.id, Infinity); single.collectDiff(); req = { include: undefined, exclude: [single.root.children.get('id-b')!], profile: configuration, }; dto = TestRunDto.fromInternal({ controllerId: 'ctrl', profileId: configuration.profileId, excludeExtIds: ['id-b'], runId: 'run-id', testIds: [single.root.id], }, single); }); test('tracks a run started from a main thread request', () => { const tracker = c.prepareForMainThreadTestRun(req, dto, cts.token); assert.strictEqual(tracker.isRunning, false); const task1 = c.createTestRun('ctrl', single, req, 'run1', true); const task2 = c.createTestRun('ctrl', single, req, 'run2', true); assert.strictEqual(proxy.$startedExtensionTestRun.called, false); assert.strictEqual(tracker.isRunning, true); task1.appendOutput('hello'); const taskId = proxy.$appendOutputToRun.args[0]?.[1]; assert.deepStrictEqual([['run-id', taskId, VSBuffer.fromString('hello'), undefined, undefined]], proxy.$appendOutputToRun.args); task1.end(); assert.strictEqual(proxy.$finishedExtensionTestRun.called, false); assert.strictEqual(tracker.isRunning, true); task2.end(); assert.strictEqual(proxy.$finishedExtensionTestRun.called, false); assert.strictEqual(tracker.isRunning, false); }); test('tracks a run started from an extension request', () => { const task1 = c.createTestRun('ctrl', single, req, 'hello world', false); const tracker = Iterable.first(c.trackers)!; assert.strictEqual(tracker.isRunning, true); assert.deepStrictEqual(proxy.$startedExtensionTestRun.args, [ [{ profile: { group: 2, id: 42 }, controllerId: 'ctrl', id: tracker.id, include: [single.root.id], exclude: ['id-b'], persist: false, }] ]); const task2 = c.createTestRun('ctrl', single, req, 'run2', true); const task3Detached = c.createTestRun('ctrl', single, { ...req }, 'task3Detached', true); task1.end(); assert.strictEqual(proxy.$finishedExtensionTestRun.called, false); assert.strictEqual(tracker.isRunning, true); task2.end(); assert.deepStrictEqual(proxy.$finishedExtensionTestRun.args, [[tracker.id]]); assert.strictEqual(tracker.isRunning, false); task3Detached.end(); }); test('adds tests to run smartly', () => { const task1 = c.createTestRun('ctrlId', single, req, 'hello world', false); const tracker = Iterable.first(c.trackers)!; const expectedArgs: unknown[][] = []; assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs); task1.passed(single.root.children.get('id-a')!.children.get('id-aa')!); expectedArgs.push([ 'ctrlId', tracker.id, [ convert.TestItem.from(single.root), convert.TestItem.from(single.root.children.get('id-a') as TestItemImpl), convert.TestItem.from(single.root.children.get('id-a')!.children.get('id-aa') as TestItemImpl), ] ]); assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs); task1.enqueued(single.root.children.get('id-a')!.children.get('id-ab')!); expectedArgs.push([ 'ctrlId', tracker.id, [ convert.TestItem.from(single.root.children.get('id-a') as TestItemImpl), convert.TestItem.from(single.root.children.get('id-a')!.children.get('id-ab') as TestItemImpl), ], ]); assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs); task1.passed(single.root.children.get('id-a')!.children.get('id-ab')!); assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs); }); test('adds test messages to run', () => { const test1 = new TestItemImpl('ctrlId', 'id-c', 'test c', URI.file('/testc.txt')); const test2 = new TestItemImpl('ctrlId', 'id-d', 'test d', URI.file('/testd.txt')); test1.range = test2.range = new Range(new Position(0, 0), new Position(1, 0)); single.root.children.replace([test1, test2]); const task = c.createTestRun('ctrlId', single, req, 'hello world', false); const message1 = new TestMessage('some message'); message1.location = new Location(URI.file('/a.txt'), new Position(0, 0)); task.failed(test1, message1); const args = proxy.$appendTestMessagesInRun.args[0]; assert.deepStrictEqual(proxy.$appendTestMessagesInRun.args[0], [ args[0], args[1], new TestId(['ctrlId', 'id-c']).toString(), [{ message: 'some message', type: TestMessageType.Error, expected: undefined, actual: undefined, location: convert.location.from(message1.location) }] ]); // should use test location as default task.failed(test2, new TestMessage('some message')); assert.deepStrictEqual(proxy.$appendTestMessagesInRun.args[1], [ args[0], args[1], new TestId(['ctrlId', 'id-d']).toString(), [{ message: 'some message', type: TestMessageType.Error, expected: undefined, actual: undefined, location: convert.location.from({ uri: test2.uri!, range: test2.range! }), }] ]); }); test('guards calls after runs are ended', () => { const task = c.createTestRun('ctrl', single, req, 'hello world', false); task.end(); task.failed(single.root, new TestMessage('some message')); task.appendOutput('output'); assert.strictEqual(proxy.$addTestsToRun.called, false); assert.strictEqual(proxy.$appendOutputToRun.called, false); assert.strictEqual(proxy.$appendTestMessagesInRun.called, false); }); test('excludes tests outside tree or explicitly excluded', () => { const task = c.createTestRun('ctrlId', single, { profile: configuration, include: [single.root.children.get('id-a')!], exclude: [single.root.children.get('id-a')!.children.get('id-aa')!], }, 'hello world', false); task.passed(single.root.children.get('id-a')!.children.get('id-aa')!); task.passed(single.root.children.get('id-a')!.children.get('id-ab')!); assert.deepStrictEqual(proxy.$updateTestStateInRun.args.length, 1); const args = proxy.$updateTestStateInRun.args[0]; assert.deepStrictEqual(proxy.$updateTestStateInRun.args, [[ args[0], args[1], new TestId(['ctrlId', 'id-a', 'id-ab']).toString(), TestResultState.Passed, undefined, ]]); }); test('sets state of test with identical local IDs (#131827)', () => { const testA = single.root.children.get('id-a'); const testB = single.root.children.get('id-b'); const childA = new TestItemImpl('ctrlId', 'id-child', 'child', undefined); testA!.children.replace([childA]); const childB = new TestItemImpl('ctrlId', 'id-child', 'child', undefined); testB!.children.replace([childB]); const task1 = c.createTestRun('ctrl', single, new TestRunRequestImpl(), 'hello world', false); const tracker = Iterable.first(c.trackers)!; task1.passed(childA); task1.passed(childB); assert.deepStrictEqual(proxy.$addTestsToRun.args, [ [ 'ctrl', tracker.id, [single.root, testA, childA].map(t => convert.TestItem.from(t as TestItemImpl)), ], [ 'ctrl', tracker.id, [single.root, testB, childB].map(t => convert.TestItem.from(t as TestItemImpl)), ], ]); }); }); });