Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/test/browser/extHostTesting.test.ts
5250 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import assert from 'assert';
7
import * as sinon from 'sinon';
8
import { timeout } from '../../../../base/common/async.js';
9
import { VSBuffer } from '../../../../base/common/buffer.js';
10
import { CancellationTokenSource } from '../../../../base/common/cancellation.js';
11
import { Event } from '../../../../base/common/event.js';
12
import { Iterable } from '../../../../base/common/iterator.js';
13
import { URI } from '../../../../base/common/uri.js';
14
import { mock, mockObject, MockObject } from '../../../../base/test/common/mock.js';
15
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
16
import * as editorRange from '../../../../editor/common/core/range.js';
17
import { ExtensionIdentifier, IExtensionDescription } from '../../../../platform/extensions/common/extensions.js';
18
import { NullLogService } from '../../../../platform/log/common/log.js';
19
import { MainThreadTestingShape } from '../../common/extHost.protocol.js';
20
import { ExtHostCommands } from '../../common/extHostCommands.js';
21
import { ExtHostDocumentsAndEditors } from '../../common/extHostDocumentsAndEditors.js';
22
import { IExtHostTelemetry } from '../../common/extHostTelemetry.js';
23
import { ExtHostTesting, TestRunCoordinator, TestRunDto, TestRunProfileImpl } from '../../common/extHostTesting.js';
24
import { ExtHostTestItemCollection, TestItemImpl } from '../../common/extHostTestItem.js';
25
import * as convert from '../../common/extHostTypeConverters.js';
26
import { Location, Position, Range, TestMessage, TestRunProfileKind, TestRunRequest as TestRunRequestImpl, TestTag } from '../../common/extHostTypes.js';
27
import { AnyCallRPCProtocol } from '../common/testRPCProtocol.js';
28
import { TestId } from '../../../contrib/testing/common/testId.js';
29
import { TestDiffOpType, TestItemExpandState, TestMessageType, TestsDiff } from '../../../contrib/testing/common/testTypes.js';
30
import { nullExtensionDescription } from '../../../services/extensions/common/extensions.js';
31
import type { TestController, TestItem, TestRunProfile, TestRunRequest } from 'vscode';
32
33
const simplify = (item: TestItem) => ({
34
id: item.id,
35
label: item.label,
36
uri: item.uri,
37
range: item.range,
38
});
39
40
const assertTreesEqual = (a: TestItemImpl | undefined, b: TestItemImpl | undefined) => {
41
if (!a) {
42
throw new assert.AssertionError({ message: 'Expected a to be defined', actual: a });
43
}
44
45
if (!b) {
46
throw new assert.AssertionError({ message: 'Expected b to be defined', actual: b });
47
}
48
49
assert.deepStrictEqual(simplify(a), simplify(b));
50
51
const aChildren = [...a.children].map(([_, c]) => c.id).sort();
52
const bChildren = [...b.children].map(([_, c]) => c.id).sort();
53
assert.strictEqual(aChildren.length, bChildren.length, `expected ${a.label}.children.length == ${b.label}.children.length`);
54
aChildren.forEach(key => assertTreesEqual(a.children.get(key) as TestItemImpl, b.children.get(key) as TestItemImpl));
55
};
56
57
// const assertTreeListEqual = (a: ReadonlyArray<TestItem>, b: ReadonlyArray<TestItem>) => {
58
// assert.strictEqual(a.length, b.length, `expected a.length == n.length`);
59
// a.forEach((_, i) => assertTreesEqual(a[i], b[i]));
60
// };
61
62
// class TestMirroredCollection extends MirroredTestCollection {
63
// public changeEvent!: TestChangeEvent;
64
65
// constructor() {
66
// super();
67
// this.onDidChangeTests(evt => this.changeEvent = evt);
68
// }
69
70
// public get length() {
71
// return this.items.size;
72
// }
73
// }
74
75
suite('ExtHost Testing', () => {
76
class TestExtHostTestItemCollection extends ExtHostTestItemCollection {
77
public setDiff(diff: TestsDiff) {
78
this.diff = diff;
79
}
80
}
81
82
teardown(() => {
83
sinon.restore();
84
});
85
86
const ds = ensureNoDisposablesAreLeakedInTestSuite();
87
88
let single: TestExtHostTestItemCollection;
89
let resolveCalls: (string | undefined)[] = [];
90
setup(() => {
91
resolveCalls = [];
92
single = ds.add(new TestExtHostTestItemCollection('ctrlId', 'root', {
93
getDocument: () => undefined,
94
} as Partial<ExtHostDocumentsAndEditors> as ExtHostDocumentsAndEditors));
95
single.resolveHandler = item => {
96
resolveCalls.push(item?.id);
97
if (item === undefined) {
98
const a = new TestItemImpl('ctrlId', 'id-a', 'a', URI.file('/'));
99
a.canResolveChildren = true;
100
const b = new TestItemImpl('ctrlId', 'id-b', 'b', URI.file('/'));
101
single.root.children.add(a);
102
single.root.children.add(b);
103
} else if (item.id === 'id-a') {
104
item.children.add(new TestItemImpl('ctrlId', 'id-aa', 'aa', URI.file('/')));
105
item.children.add(new TestItemImpl('ctrlId', 'id-ab', 'ab', URI.file('/')));
106
}
107
};
108
109
ds.add(single.onDidGenerateDiff(d => single.setDiff(d /* don't clear during testing */)));
110
});
111
112
suite('OwnedTestCollection', () => {
113
test('adds a root recursively', async () => {
114
await single.expand(single.root.id, Infinity);
115
const a = single.root.children.get('id-a') as TestItemImpl;
116
const b = single.root.children.get('id-b') as TestItemImpl;
117
assert.deepStrictEqual(single.collectDiff(), [
118
{
119
op: TestDiffOpType.Add,
120
item: { controllerId: 'ctrlId', expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(single.root) } }
121
},
122
{
123
op: TestDiffOpType.Add,
124
item: { controllerId: 'ctrlId', expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(a) } }
125
},
126
{
127
op: TestDiffOpType.Add,
128
item: { controllerId: 'ctrlId', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(a.children.get('id-aa') as TestItemImpl) }
129
},
130
{
131
op: TestDiffOpType.Add,
132
item: { controllerId: 'ctrlId', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(a.children.get('id-ab') as TestItemImpl) }
133
},
134
{
135
op: TestDiffOpType.Update,
136
item: { extId: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.Expanded }
137
},
138
{
139
op: TestDiffOpType.Add,
140
item: { controllerId: 'ctrlId', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(b) }
141
},
142
{
143
op: TestDiffOpType.Update,
144
item: { extId: single.root.id, expand: TestItemExpandState.Expanded }
145
},
146
]);
147
});
148
149
test('parents are set correctly', () => {
150
single.expand(single.root.id, Infinity);
151
single.collectDiff();
152
153
const a = single.root.children.get('id-a')!;
154
const ab = a.children.get('id-ab')!;
155
assert.strictEqual(a.parent, undefined);
156
assert.strictEqual(ab.parent, a);
157
});
158
159
test('can add an item with same ID as root', () => {
160
single.collectDiff();
161
162
const child = new TestItemImpl('ctrlId', 'ctrlId', 'c', undefined);
163
single.root.children.add(child);
164
assert.deepStrictEqual(single.collectDiff(), [
165
{
166
op: TestDiffOpType.Add,
167
item: { controllerId: 'ctrlId', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(child) },
168
}
169
]);
170
});
171
172
test('no-ops if items not changed', () => {
173
single.collectDiff();
174
assert.deepStrictEqual(single.collectDiff(), []);
175
});
176
177
test('watches property mutations', () => {
178
single.expand(single.root.id, Infinity);
179
single.collectDiff();
180
single.root.children.get('id-a')!.description = 'Hello world'; /* item a */
181
182
assert.deepStrictEqual(single.collectDiff(), [
183
{
184
op: TestDiffOpType.Update,
185
item: { extId: new TestId(['ctrlId', 'id-a']).toString(), item: { description: 'Hello world' } },
186
}
187
]);
188
});
189
190
test('removes children', () => {
191
single.expand(single.root.id, Infinity);
192
single.collectDiff();
193
single.root.children.delete('id-a');
194
195
assert.deepStrictEqual(single.collectDiff(), [
196
{ op: TestDiffOpType.Remove, itemId: new TestId(['ctrlId', 'id-a']).toString() },
197
]);
198
assert.deepStrictEqual(
199
[...single.tree.keys()].sort(),
200
[single.root.id, new TestId(['ctrlId', 'id-b']).toString()],
201
);
202
assert.strictEqual(single.tree.size, 2);
203
});
204
205
test('adds new children', () => {
206
single.expand(single.root.id, Infinity);
207
single.collectDiff();
208
const child = new TestItemImpl('ctrlId', 'id-ac', 'c', undefined);
209
single.root.children.get('id-a')!.children.add(child);
210
211
assert.deepStrictEqual(single.collectDiff(), [
212
{
213
op: TestDiffOpType.Add, item: {
214
controllerId: 'ctrlId',
215
expand: TestItemExpandState.NotExpandable,
216
item: convert.TestItem.from(child),
217
}
218
},
219
]);
220
assert.deepStrictEqual(
221
[...single.tree.values()].map(n => n.actual.id).sort(),
222
[single.root.id, 'id-a', 'id-aa', 'id-ab', 'id-ac', 'id-b'],
223
);
224
assert.strictEqual(single.tree.size, 6);
225
});
226
227
test('manages tags correctly', () => {
228
single.expand(single.root.id, Infinity);
229
single.collectDiff();
230
const tag1 = new TestTag('tag1');
231
const tag2 = new TestTag('tag2');
232
const tag3 = new TestTag('tag3');
233
const child = new TestItemImpl('ctrlId', 'id-ac', 'c', undefined);
234
child.tags = [tag1, tag2];
235
single.root.children.get('id-a')!.children.add(child);
236
237
assert.deepStrictEqual(single.collectDiff(), [
238
{ op: TestDiffOpType.AddTag, tag: { id: 'ctrlId\0tag1' } },
239
{ op: TestDiffOpType.AddTag, tag: { id: 'ctrlId\0tag2' } },
240
{
241
op: TestDiffOpType.Add, item: {
242
controllerId: 'ctrlId',
243
expand: TestItemExpandState.NotExpandable,
244
item: convert.TestItem.from(child),
245
}
246
},
247
]);
248
249
child.tags = [tag2, tag3];
250
assert.deepStrictEqual(single.collectDiff(), [
251
{ op: TestDiffOpType.AddTag, tag: { id: 'ctrlId\0tag3' } },
252
{
253
op: TestDiffOpType.Update, item: {
254
extId: new TestId(['ctrlId', 'id-a', 'id-ac']).toString(),
255
item: { tags: ['ctrlId\0tag2', 'ctrlId\0tag3'] }
256
}
257
},
258
{ op: TestDiffOpType.RemoveTag, id: 'ctrlId\0tag1' },
259
]);
260
261
const a = single.root.children.get('id-a')!;
262
a.tags = [tag2];
263
a.children.replace([]);
264
assert.deepStrictEqual(single.collectDiff().filter(t => t.op === TestDiffOpType.RemoveTag), [
265
{ op: TestDiffOpType.RemoveTag, id: 'ctrlId\0tag3' },
266
]);
267
});
268
269
test('replaces on uri change', () => {
270
single.expand(single.root.id, Infinity);
271
single.collectDiff();
272
273
const oldA = single.root.children.get('id-a') as TestItemImpl;
274
const uri = single.root.children.get('id-a')!.uri?.with({ path: '/different' });
275
const newA = new TestItemImpl('ctrlId', 'id-a', 'Hello world', uri);
276
newA.children.replace([...oldA.children].map(([_, item]) => item));
277
single.root.children.replace([...single.root.children].map(([id, i]) => id === 'id-a' ? newA : i));
278
279
assert.deepStrictEqual(single.collectDiff(), [
280
{ op: TestDiffOpType.Remove, itemId: new TestId(['ctrlId', 'id-a']).toString() },
281
{
282
op: TestDiffOpType.Add,
283
item: { controllerId: 'ctrlId', expand: TestItemExpandState.NotExpandable, item: { ...convert.TestItem.from(newA) } }
284
},
285
{
286
op: TestDiffOpType.Add,
287
item: { controllerId: 'ctrlId', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(newA.children.get('id-aa') as TestItemImpl) }
288
},
289
{
290
op: TestDiffOpType.Add,
291
item: { controllerId: 'ctrlId', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(newA.children.get('id-ab') as TestItemImpl) }
292
},
293
]);
294
});
295
296
test('treats in-place replacement as mutation', () => {
297
single.expand(single.root.id, Infinity);
298
single.collectDiff();
299
300
const oldA = single.root.children.get('id-a') as TestItemImpl;
301
const uri = single.root.children.get('id-a')!.uri;
302
const newA = new TestItemImpl('ctrlId', 'id-a', 'Hello world', uri);
303
newA.children.replace([...oldA.children].map(([_, item]) => item));
304
single.root.children.replace([
305
newA,
306
new TestItemImpl('ctrlId', 'id-b', single.root.children.get('id-b')!.label, uri),
307
]);
308
309
assert.deepStrictEqual(single.collectDiff(), [
310
{
311
op: TestDiffOpType.Update,
312
item: { extId: new TestId(['ctrlId', 'id-a']).toString(), item: { label: 'Hello world' } },
313
},
314
{
315
op: TestDiffOpType.DocumentSynced,
316
docv: undefined,
317
uri: uri
318
}
319
]);
320
321
newA.label = 'still connected';
322
assert.deepStrictEqual(single.collectDiff(), [
323
{
324
op: TestDiffOpType.Update,
325
item: { extId: new TestId(['ctrlId', 'id-a']).toString(), item: { label: 'still connected' } }
326
},
327
]);
328
329
oldA.label = 'no longer connected';
330
assert.deepStrictEqual(single.collectDiff(), []);
331
});
332
333
suite('expandibility restoration', () => {
334
const doReplace = async (canResolveChildren = true) => {
335
const uri = single.root.children.get('id-a')!.uri;
336
const newA = new TestItemImpl('ctrlId', 'id-a', 'Hello world', uri);
337
newA.canResolveChildren = canResolveChildren;
338
single.root.children.replace([
339
newA,
340
new TestItemImpl('ctrlId', 'id-b', single.root.children.get('id-b')!.label, uri),
341
]);
342
await timeout(0); // drain microtasks
343
};
344
345
test('does not restore an unexpanded state', async () => {
346
await single.expand(single.root.id, 0);
347
assert.deepStrictEqual(resolveCalls, [undefined]);
348
await doReplace();
349
assert.deepStrictEqual(resolveCalls, [undefined]);
350
});
351
352
test('restores resolve state on replacement', async () => {
353
await single.expand(single.root.id, Infinity);
354
assert.deepStrictEqual(resolveCalls, [undefined, 'id-a']);
355
await doReplace();
356
assert.deepStrictEqual(resolveCalls, [undefined, 'id-a', 'id-a']);
357
});
358
359
test('does not expand if new child is not expandable', async () => {
360
await single.expand(single.root.id, Infinity);
361
assert.deepStrictEqual(resolveCalls, [undefined, 'id-a']);
362
await doReplace(false);
363
assert.deepStrictEqual(resolveCalls, [undefined, 'id-a']);
364
});
365
});
366
367
test('treats in-place replacement as mutation deeply', () => {
368
single.expand(single.root.id, Infinity);
369
single.collectDiff();
370
371
const oldA = single.root.children.get('id-a')!;
372
const uri = oldA.uri;
373
const newA = new TestItemImpl('ctrlId', 'id-a', single.root.children.get('id-a')!.label, uri);
374
const oldAA = oldA.children.get('id-aa')!;
375
const oldAB = oldA.children.get('id-ab')!;
376
const newAB = new TestItemImpl('ctrlId', 'id-ab', 'Hello world', uri);
377
newA.children.replace([oldAA, newAB]);
378
single.root.children.replace([newA, single.root.children.get('id-b')!]);
379
380
assert.deepStrictEqual(single.collectDiff(), [
381
{
382
op: TestDiffOpType.Update,
383
item: { extId: TestId.fromExtHostTestItem(oldAB, 'ctrlId').toString(), item: { label: 'Hello world' } },
384
},
385
{
386
op: TestDiffOpType.DocumentSynced,
387
docv: undefined,
388
uri: uri
389
}
390
]);
391
392
oldAA.label = 'still connected1';
393
newAB.label = 'still connected2';
394
oldAB.label = 'not connected3';
395
assert.deepStrictEqual(single.collectDiff(), [
396
{
397
op: TestDiffOpType.Update,
398
item: { extId: new TestId(['ctrlId', 'id-a', 'id-aa']).toString(), item: { label: 'still connected1' } }
399
},
400
{
401
op: TestDiffOpType.Update,
402
item: { extId: new TestId(['ctrlId', 'id-a', 'id-ab']).toString(), item: { label: 'still connected2' } }
403
},
404
]);
405
406
assert.strictEqual(newAB.parent, newA);
407
assert.strictEqual(oldAA.parent, newA);
408
assert.deepStrictEqual(newA.parent, undefined);
409
});
410
411
test('moves an item to be a new child', async () => {
412
await single.expand(single.root.id, 0);
413
single.collectDiff();
414
const b = single.root.children.get('id-b') as TestItemImpl;
415
const a = single.root.children.get('id-a') as TestItemImpl;
416
a.children.add(b);
417
assert.deepStrictEqual(single.collectDiff(), [
418
{
419
op: TestDiffOpType.Remove,
420
itemId: new TestId(['ctrlId', 'id-b']).toString(),
421
},
422
{
423
op: TestDiffOpType.Add,
424
item: { controllerId: 'ctrlId', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(b) }
425
},
426
]);
427
428
b.label = 'still connected';
429
assert.deepStrictEqual(single.collectDiff(), [
430
{
431
op: TestDiffOpType.Update,
432
item: { extId: new TestId(['ctrlId', 'id-a', 'id-b']).toString(), item: { label: 'still connected' } }
433
},
434
]);
435
436
assert.deepStrictEqual([...single.root.children].map(([_, item]) => item), [single.root.children.get('id-a')]);
437
assert.deepStrictEqual(b.parent, a);
438
});
439
440
test('sends document sync events', async () => {
441
await single.expand(single.root.id, 0);
442
single.collectDiff();
443
444
const a = single.root.children.get('id-a') as TestItemImpl;
445
a.range = new Range(new Position(0, 0), new Position(1, 0));
446
447
assert.deepStrictEqual(single.collectDiff(), [
448
{
449
op: TestDiffOpType.DocumentSynced,
450
docv: undefined,
451
uri: URI.file('/')
452
},
453
{
454
op: TestDiffOpType.Update,
455
item: {
456
extId: new TestId(['ctrlId', 'id-a']).toString(),
457
item: {
458
range: editorRange.Range.lift({
459
endColumn: 1,
460
endLineNumber: 2,
461
startColumn: 1,
462
startLineNumber: 1
463
})
464
}
465
},
466
},
467
]);
468
469
// sends on replace even if it's a no-op
470
a.range = a.range;
471
assert.deepStrictEqual(single.collectDiff(), [
472
{
473
op: TestDiffOpType.DocumentSynced,
474
docv: undefined,
475
uri: URI.file('/')
476
},
477
]);
478
479
// sends on a child replacement
480
const uri = URI.file('/');
481
const a2 = new TestItemImpl('ctrlId', 'id-a', 'a', uri);
482
a2.range = a.range;
483
single.root.children.replace([a2, single.root.children.get('id-b')!]);
484
assert.deepStrictEqual(single.collectDiff(), [
485
{
486
op: TestDiffOpType.DocumentSynced,
487
docv: undefined,
488
uri
489
},
490
]);
491
});
492
});
493
494
495
suite('MirroredTestCollection', () => {
496
// todo@connor4312: re-renable when we figure out what observing looks like we async children
497
// let m: TestMirroredCollection;
498
// setup(() => m = new TestMirroredCollection());
499
500
// test('mirrors creation of the root', () => {
501
// const tests = testStubs.nested();
502
// single.addRoot(tests, 'pid');
503
// single.expand(single.root.id, Infinity);
504
// m.apply(single.collectDiff());
505
// assertTreesEqual(m.rootTestItems[0], owned.getTestById(single.root.id)![1].actual);
506
// assert.strictEqual(m.length, single.itemToInternal.size);
507
// });
508
509
// test('mirrors node deletion', () => {
510
// const tests = testStubs.nested();
511
// single.addRoot(tests, 'pid');
512
// m.apply(single.collectDiff());
513
// single.expand(single.root.id, Infinity);
514
// tests.children!.splice(0, 1);
515
// single.onItemChange(tests, 'pid');
516
// single.expand(single.root.id, Infinity);
517
// m.apply(single.collectDiff());
518
519
// assertTreesEqual(m.rootTestItems[0], owned.getTestById(single.root.id)![1].actual);
520
// assert.strictEqual(m.length, single.itemToInternal.size);
521
// });
522
523
// test('mirrors node addition', () => {
524
// const tests = testStubs.nested();
525
// single.addRoot(tests, 'pid');
526
// m.apply(single.collectDiff());
527
// tests.children![0].children!.push(stubTest('ac'));
528
// single.onItemChange(tests, 'pid');
529
// m.apply(single.collectDiff());
530
531
// assertTreesEqual(m.rootTestItems[0], owned.getTestById(single.root.id)![1].actual);
532
// assert.strictEqual(m.length, single.itemToInternal.size);
533
// });
534
535
// test('mirrors node update', () => {
536
// const tests = testStubs.nested();
537
// single.addRoot(tests, 'pid');
538
// m.apply(single.collectDiff());
539
// tests.children![0].description = 'Hello world'; /* item a */
540
// single.onItemChange(tests, 'pid');
541
// m.apply(single.collectDiff());
542
543
// assertTreesEqual(m.rootTestItems[0], owned.getTestById(single.root.id)![1].actual);
544
// });
545
546
// suite('MirroredChangeCollector', () => {
547
// let tests = testStubs.nested();
548
// setup(() => {
549
// tests = testStubs.nested();
550
// single.addRoot(tests, 'pid');
551
// m.apply(single.collectDiff());
552
// });
553
554
// test('creates change for root', () => {
555
// assertTreeListEqual(m.changeEvent.added, [
556
// tests,
557
// tests.children[0],
558
// tests.children![0].children![0],
559
// tests.children![0].children![1],
560
// tests.children[1],
561
// ]);
562
// assertTreeListEqual(m.changeEvent.removed, []);
563
// assertTreeListEqual(m.changeEvent.updated, []);
564
// });
565
566
// test('creates change for delete', () => {
567
// const rm = tests.children.shift()!;
568
// single.onItemChange(tests, 'pid');
569
// m.apply(single.collectDiff());
570
571
// assertTreeListEqual(m.changeEvent.added, []);
572
// assertTreeListEqual(m.changeEvent.removed, [
573
// { ...rm },
574
// { ...rm.children![0] },
575
// { ...rm.children![1] },
576
// ]);
577
// assertTreeListEqual(m.changeEvent.updated, []);
578
// });
579
580
// test('creates change for update', () => {
581
// tests.children[0].label = 'updated!';
582
// single.onItemChange(tests, 'pid');
583
// m.apply(single.collectDiff());
584
585
// assertTreeListEqual(m.changeEvent.added, []);
586
// assertTreeListEqual(m.changeEvent.removed, []);
587
// assertTreeListEqual(m.changeEvent.updated, [tests.children[0]]);
588
// });
589
590
// test('is a no-op if a node is added and removed', () => {
591
// const nested = testStubs.nested('id2-');
592
// tests.children.push(nested);
593
// single.onItemChange(tests, 'pid');
594
// tests.children.pop();
595
// single.onItemChange(tests, 'pid');
596
// const previousEvent = m.changeEvent;
597
// m.apply(single.collectDiff());
598
// assert.strictEqual(m.changeEvent, previousEvent);
599
// });
600
601
// test('is a single-op if a node is added and changed', () => {
602
// const child = stubTest('c');
603
// tests.children.push(child);
604
// single.onItemChange(tests, 'pid');
605
// child.label = 'd';
606
// single.onItemChange(tests, 'pid');
607
// m.apply(single.collectDiff());
608
609
// assertTreeListEqual(m.changeEvent.added, [child]);
610
// assertTreeListEqual(m.changeEvent.removed, []);
611
// assertTreeListEqual(m.changeEvent.updated, []);
612
// });
613
614
// test('gets the common ancestor (1)', () => {
615
// tests.children![0].children![0].label = 'za';
616
// tests.children![0].children![1].label = 'zb';
617
// single.onItemChange(tests, 'pid');
618
// m.apply(single.collectDiff());
619
620
// });
621
622
// test('gets the common ancestor (2)', () => {
623
// tests.children![0].children![0].label = 'za';
624
// tests.children![1].label = 'ab';
625
// single.onItemChange(tests, 'pid');
626
// m.apply(single.collectDiff());
627
// });
628
// });
629
});
630
631
suite('TestRunTracker', () => {
632
let proxy: MockObject<MainThreadTestingShape>;
633
let c: TestRunCoordinator;
634
let cts: CancellationTokenSource;
635
let configuration: TestRunProfileImpl;
636
637
let req: TestRunRequest;
638
639
let dto: TestRunDto;
640
// eslint-disable-next-line local/code-no-any-casts
641
const ext: IExtensionDescription = {} as any;
642
643
teardown(() => {
644
for (const { id } of c.trackers) {
645
c.disposeTestRun(id);
646
}
647
});
648
649
setup(async () => {
650
proxy = mockObject<MainThreadTestingShape>()();
651
cts = new CancellationTokenSource();
652
c = new TestRunCoordinator(proxy, new NullLogService());
653
654
configuration = new TestRunProfileImpl(mockObject<MainThreadTestingShape>()(), new Map(), new Set(), Event.None, 'ctrlId', 42, 'Do Run', TestRunProfileKind.Run, () => { }, false);
655
656
await single.expand(single.root.id, Infinity);
657
single.collectDiff();
658
659
req = {
660
include: undefined,
661
exclude: [single.root.children.get('id-b')!],
662
profile: configuration,
663
preserveFocus: false,
664
};
665
666
dto = TestRunDto.fromInternal({
667
controllerId: 'ctrl',
668
profileId: configuration.profileId,
669
excludeExtIds: ['id-b'],
670
runId: 'run-id',
671
testIds: [single.root.id],
672
}, single);
673
});
674
675
test('tracks a run started from a main thread request', () => {
676
const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token));
677
assert.strictEqual(tracker.hasRunningTasks, false);
678
679
const task1 = c.createTestRun(ext, 'ctrl', single, req, 'run1', true);
680
const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true);
681
assert.strictEqual(proxy.$startedExtensionTestRun.called, false);
682
assert.strictEqual(tracker.hasRunningTasks, true);
683
684
task1.appendOutput('hello');
685
const taskId = proxy.$appendOutputToRun.args[0]?.[1];
686
assert.deepStrictEqual([['run-id', taskId, VSBuffer.fromString('hello'), undefined, undefined]], proxy.$appendOutputToRun.args);
687
task1.end();
688
689
assert.strictEqual(proxy.$finishedExtensionTestRun.called, false);
690
assert.strictEqual(tracker.hasRunningTasks, true);
691
692
task2.end();
693
694
assert.strictEqual(proxy.$finishedExtensionTestRun.called, false);
695
assert.strictEqual(tracker.hasRunningTasks, false);
696
});
697
698
test('run cancel force ends after a timeout', () => {
699
const clock = sinon.useFakeTimers();
700
try {
701
const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token));
702
const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true);
703
const onEnded = sinon.stub();
704
ds.add(tracker.onEnd(onEnded));
705
706
assert.strictEqual(task.token.isCancellationRequested, false);
707
assert.strictEqual(tracker.hasRunningTasks, true);
708
tracker.cancel();
709
710
assert.strictEqual(task.token.isCancellationRequested, true);
711
assert.strictEqual(tracker.hasRunningTasks, true);
712
713
clock.tick(9999);
714
assert.strictEqual(tracker.hasRunningTasks, true);
715
assert.strictEqual(onEnded.called, false);
716
717
clock.tick(1);
718
assert.strictEqual(onEnded.called, true);
719
assert.strictEqual(tracker.hasRunningTasks, false);
720
} finally {
721
clock.restore();
722
}
723
});
724
725
test('run cancel force ends on second cancellation request', () => {
726
const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token));
727
const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true);
728
const onEnded = sinon.stub();
729
ds.add(tracker.onEnd(onEnded));
730
731
assert.strictEqual(task.token.isCancellationRequested, false);
732
assert.strictEqual(tracker.hasRunningTasks, true);
733
tracker.cancel();
734
735
assert.strictEqual(task.token.isCancellationRequested, true);
736
assert.strictEqual(tracker.hasRunningTasks, true);
737
assert.strictEqual(onEnded.called, false);
738
tracker.cancel();
739
740
assert.strictEqual(tracker.hasRunningTasks, false);
741
assert.strictEqual(onEnded.called, true);
742
});
743
744
test('tracks a run started from an extension request', () => {
745
const task1 = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false);
746
747
const tracker = Iterable.first(c.trackers)!;
748
assert.strictEqual(tracker.hasRunningTasks, true);
749
assert.deepStrictEqual(proxy.$startedExtensionTestRun.args, [
750
[{
751
profile: { group: 2, id: 42 },
752
controllerId: 'ctrl',
753
id: tracker.id,
754
include: [single.root.id],
755
exclude: [new TestId(['ctrlId', 'id-b']).toString()],
756
persist: false,
757
continuous: false,
758
preserveFocus: false,
759
}]
760
]);
761
762
const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true);
763
const task3Detached = c.createTestRun(ext, 'ctrl', single, { ...req }, 'task3Detached', true);
764
765
task1.end();
766
assert.strictEqual(proxy.$finishedExtensionTestRun.called, false);
767
assert.strictEqual(tracker.hasRunningTasks, true);
768
769
task2.end();
770
assert.deepStrictEqual(proxy.$finishedExtensionTestRun.args, [[tracker.id]]);
771
assert.strictEqual(tracker.hasRunningTasks, false);
772
773
task3Detached.end();
774
});
775
776
test('adds tests to run smartly', () => {
777
const task1 = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false);
778
const tracker = Iterable.first(c.trackers)!;
779
const expectedArgs: unknown[][] = [];
780
assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs);
781
782
task1.passed(single.root.children.get('id-a')!.children.get('id-aa')!);
783
expectedArgs.push([
784
'ctrlId',
785
tracker.id,
786
[
787
convert.TestItem.from(single.root),
788
convert.TestItem.from(single.root.children.get('id-a') as TestItemImpl),
789
convert.TestItem.from(single.root.children.get('id-a')!.children.get('id-aa') as TestItemImpl),
790
]
791
]);
792
assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs);
793
794
task1.enqueued(single.root.children.get('id-a')!.children.get('id-ab')!);
795
expectedArgs.push([
796
'ctrlId',
797
tracker.id,
798
[
799
convert.TestItem.from(single.root.children.get('id-a') as TestItemImpl),
800
convert.TestItem.from(single.root.children.get('id-a')!.children.get('id-ab') as TestItemImpl),
801
],
802
]);
803
assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs);
804
805
task1.passed(single.root.children.get('id-a')!.children.get('id-ab')!);
806
assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs);
807
808
task1.end();
809
});
810
811
test('adds test messages to run', () => {
812
const test1 = new TestItemImpl('ctrlId', 'id-c', 'test c', URI.file('/testc.txt'));
813
const test2 = new TestItemImpl('ctrlId', 'id-d', 'test d', URI.file('/testd.txt'));
814
test1.range = test2.range = new Range(new Position(0, 0), new Position(1, 0));
815
single.root.children.replace([test1, test2]);
816
const task = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false);
817
818
const message1 = new TestMessage('some message');
819
message1.location = new Location(URI.file('/a.txt'), new Position(0, 0));
820
task.failed(test1, message1);
821
822
const args = proxy.$appendTestMessagesInRun.args[0];
823
assert.deepStrictEqual(proxy.$appendTestMessagesInRun.args[0], [
824
args[0],
825
args[1],
826
new TestId(['ctrlId', 'id-c']).toString(),
827
[{
828
message: 'some message',
829
type: TestMessageType.Error,
830
expected: undefined,
831
contextValue: undefined,
832
actual: undefined,
833
location: convert.location.from(message1.location),
834
stackTrace: undefined,
835
}]
836
]);
837
838
// should use test location as default
839
task.failed(test2, new TestMessage('some message'));
840
assert.deepStrictEqual(proxy.$appendTestMessagesInRun.args[1], [
841
args[0],
842
args[1],
843
new TestId(['ctrlId', 'id-d']).toString(),
844
[{
845
message: 'some message',
846
type: TestMessageType.Error,
847
contextValue: undefined,
848
expected: undefined,
849
actual: undefined,
850
location: convert.location.from({ uri: test2.uri!, range: test2.range }),
851
stackTrace: undefined,
852
}]
853
]);
854
855
task.end();
856
});
857
858
test('guards calls after runs are ended', () => {
859
const task = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false);
860
task.end();
861
862
task.failed(single.root, new TestMessage('some message'));
863
task.appendOutput('output');
864
865
assert.strictEqual(proxy.$addTestsToRun.called, false);
866
assert.strictEqual(proxy.$appendOutputToRun.called, false);
867
assert.strictEqual(proxy.$appendTestMessagesInRun.called, false);
868
});
869
870
test('sets state of test with identical local IDs (#131827)', () => {
871
const testA = single.root.children.get('id-a');
872
const testB = single.root.children.get('id-b');
873
const childA = new TestItemImpl('ctrlId', 'id-child', 'child', undefined);
874
testA!.children.replace([childA]);
875
const childB = new TestItemImpl('ctrlId', 'id-child', 'child', undefined);
876
testB!.children.replace([childB]);
877
878
const task1 = c.createTestRun(ext, 'ctrl', single, new TestRunRequestImpl(), 'hello world', false);
879
const tracker = Iterable.first(c.trackers)!;
880
881
task1.passed(childA);
882
task1.passed(childB);
883
assert.deepStrictEqual(proxy.$addTestsToRun.args, [
884
[
885
'ctrl',
886
tracker.id,
887
[single.root, testA, childA].map(t => convert.TestItem.from(t as TestItemImpl)),
888
],
889
[
890
'ctrl',
891
tracker.id,
892
[single.root, testB, childB].map(t => convert.TestItem.from(t as TestItemImpl)),
893
],
894
]);
895
896
task1.end();
897
});
898
});
899
900
suite('service', () => {
901
let ctrl: TestExtHostTesting;
902
903
class TestExtHostTesting extends ExtHostTesting {
904
public getProfileInternalId(ctrl: TestController, profile: TestRunProfile) {
905
for (const [id, p] of this.controllers.get(ctrl.id)!.profiles) {
906
if (profile === p) {
907
return id;
908
}
909
}
910
911
throw new Error('profile not found');
912
}
913
}
914
915
setup(() => {
916
const rpcProtocol = AnyCallRPCProtocol();
917
ctrl = ds.add(new TestExtHostTesting(
918
rpcProtocol,
919
new NullLogService(),
920
new ExtHostCommands(rpcProtocol, new NullLogService(), new class extends mock<IExtHostTelemetry>() {
921
override onExtensionError(): boolean {
922
return true;
923
}
924
}),
925
new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()),
926
));
927
});
928
929
test('exposes active profiles correctly', async () => {
930
const extA = { ...nullExtensionDescription, identifier: new ExtensionIdentifier('ext.a'), enabledApiProposals: ['testingActiveProfile'] };
931
const extB = { ...nullExtensionDescription, identifier: new ExtensionIdentifier('ext.b'), enabledApiProposals: ['testingActiveProfile'] };
932
933
const ctrlA = ds.add(ctrl.createTestController(extA, 'a', 'ctrla'));
934
const profAA = ds.add(ctrlA.createRunProfile('aa', TestRunProfileKind.Run, () => { }));
935
const profAB = ds.add(ctrlA.createRunProfile('ab', TestRunProfileKind.Run, () => { }));
936
937
const ctrlB = ds.add(ctrl.createTestController(extB, 'b', 'ctrlb'));
938
const profBA = ds.add(ctrlB.createRunProfile('ba', TestRunProfileKind.Run, () => { }));
939
const profBB = ds.add(ctrlB.createRunProfile('bb', TestRunProfileKind.Run, () => { }));
940
const neverCalled = sinon.stub();
941
942
// empty default state:
943
assert.deepStrictEqual(profAA.isDefault, false);
944
assert.deepStrictEqual(profBA.isDefault, false);
945
assert.deepStrictEqual(profBB.isDefault, false);
946
947
// fires a change event:
948
const changeA = Event.toPromise(profAA.onDidChangeDefault as Event<boolean>);
949
const changeBA = Event.toPromise(profBA.onDidChangeDefault as Event<boolean>);
950
const changeBB = Event.toPromise(profBB.onDidChangeDefault as Event<boolean>);
951
952
ds.add(profAB.onDidChangeDefault(neverCalled));
953
assert.strictEqual(neverCalled.called, false);
954
955
ctrl.$setDefaultRunProfiles({
956
a: [ctrl.getProfileInternalId(ctrlA, profAA)],
957
b: [ctrl.getProfileInternalId(ctrlB, profBA), ctrl.getProfileInternalId(ctrlB, profBB)]
958
});
959
960
assert.deepStrictEqual(await changeA, true);
961
assert.deepStrictEqual(await changeBA, true);
962
assert.deepStrictEqual(await changeBB, true);
963
964
// updates internal state:
965
assert.deepStrictEqual(profAA.isDefault, true);
966
assert.deepStrictEqual(profBA.isDefault, true);
967
assert.deepStrictEqual(profBB.isDefault, true);
968
assert.deepStrictEqual(profAB.isDefault, false);
969
970
// no-ops if equal
971
ds.add(profAA.onDidChangeDefault(neverCalled));
972
ctrl.$setDefaultRunProfiles({
973
a: [ctrl.getProfileInternalId(ctrlA, profAA)],
974
});
975
assert.strictEqual(neverCalled.called, false);
976
});
977
});
978
});
979
980