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
3296 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
const ext: IExtensionDescription = {} as any;
641
642
teardown(() => {
643
for (const { id } of c.trackers) {
644
c.disposeTestRun(id);
645
}
646
});
647
648
setup(async () => {
649
proxy = mockObject<MainThreadTestingShape>()();
650
cts = new CancellationTokenSource();
651
c = new TestRunCoordinator(proxy, new NullLogService());
652
653
configuration = new TestRunProfileImpl(mockObject<MainThreadTestingShape>()(), new Map(), new Set(), Event.None, 'ctrlId', 42, 'Do Run', TestRunProfileKind.Run, () => { }, false);
654
655
await single.expand(single.root.id, Infinity);
656
single.collectDiff();
657
658
req = {
659
include: undefined,
660
exclude: [single.root.children.get('id-b')!],
661
profile: configuration,
662
preserveFocus: false,
663
};
664
665
dto = TestRunDto.fromInternal({
666
controllerId: 'ctrl',
667
profileId: configuration.profileId,
668
excludeExtIds: ['id-b'],
669
runId: 'run-id',
670
testIds: [single.root.id],
671
}, single);
672
});
673
674
test('tracks a run started from a main thread request', () => {
675
const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token));
676
assert.strictEqual(tracker.hasRunningTasks, false);
677
678
const task1 = c.createTestRun(ext, 'ctrl', single, req, 'run1', true);
679
const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true);
680
assert.strictEqual(proxy.$startedExtensionTestRun.called, false);
681
assert.strictEqual(tracker.hasRunningTasks, true);
682
683
task1.appendOutput('hello');
684
const taskId = proxy.$appendOutputToRun.args[0]?.[1];
685
assert.deepStrictEqual([['run-id', taskId, VSBuffer.fromString('hello'), undefined, undefined]], proxy.$appendOutputToRun.args);
686
task1.end();
687
688
assert.strictEqual(proxy.$finishedExtensionTestRun.called, false);
689
assert.strictEqual(tracker.hasRunningTasks, true);
690
691
task2.end();
692
693
assert.strictEqual(proxy.$finishedExtensionTestRun.called, false);
694
assert.strictEqual(tracker.hasRunningTasks, false);
695
});
696
697
test('run cancel force ends after a timeout', () => {
698
const clock = sinon.useFakeTimers();
699
try {
700
const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token));
701
const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true);
702
const onEnded = sinon.stub();
703
ds.add(tracker.onEnd(onEnded));
704
705
assert.strictEqual(task.token.isCancellationRequested, false);
706
assert.strictEqual(tracker.hasRunningTasks, true);
707
tracker.cancel();
708
709
assert.strictEqual(task.token.isCancellationRequested, true);
710
assert.strictEqual(tracker.hasRunningTasks, true);
711
712
clock.tick(9999);
713
assert.strictEqual(tracker.hasRunningTasks, true);
714
assert.strictEqual(onEnded.called, false);
715
716
clock.tick(1);
717
assert.strictEqual(onEnded.called, true);
718
assert.strictEqual(tracker.hasRunningTasks, false);
719
} finally {
720
clock.restore();
721
}
722
});
723
724
test('run cancel force ends on second cancellation request', () => {
725
const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token));
726
const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true);
727
const onEnded = sinon.stub();
728
ds.add(tracker.onEnd(onEnded));
729
730
assert.strictEqual(task.token.isCancellationRequested, false);
731
assert.strictEqual(tracker.hasRunningTasks, true);
732
tracker.cancel();
733
734
assert.strictEqual(task.token.isCancellationRequested, true);
735
assert.strictEqual(tracker.hasRunningTasks, true);
736
assert.strictEqual(onEnded.called, false);
737
tracker.cancel();
738
739
assert.strictEqual(tracker.hasRunningTasks, false);
740
assert.strictEqual(onEnded.called, true);
741
});
742
743
test('tracks a run started from an extension request', () => {
744
const task1 = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false);
745
746
const tracker = Iterable.first(c.trackers)!;
747
assert.strictEqual(tracker.hasRunningTasks, true);
748
assert.deepStrictEqual(proxy.$startedExtensionTestRun.args, [
749
[{
750
profile: { group: 2, id: 42 },
751
controllerId: 'ctrl',
752
id: tracker.id,
753
include: [single.root.id],
754
exclude: [new TestId(['ctrlId', 'id-b']).toString()],
755
persist: false,
756
continuous: false,
757
preserveFocus: false,
758
}]
759
]);
760
761
const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true);
762
const task3Detached = c.createTestRun(ext, 'ctrl', single, { ...req }, 'task3Detached', true);
763
764
task1.end();
765
assert.strictEqual(proxy.$finishedExtensionTestRun.called, false);
766
assert.strictEqual(tracker.hasRunningTasks, true);
767
768
task2.end();
769
assert.deepStrictEqual(proxy.$finishedExtensionTestRun.args, [[tracker.id]]);
770
assert.strictEqual(tracker.hasRunningTasks, false);
771
772
task3Detached.end();
773
});
774
775
test('adds tests to run smartly', () => {
776
const task1 = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false);
777
const tracker = Iterable.first(c.trackers)!;
778
const expectedArgs: unknown[][] = [];
779
assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs);
780
781
task1.passed(single.root.children.get('id-a')!.children.get('id-aa')!);
782
expectedArgs.push([
783
'ctrlId',
784
tracker.id,
785
[
786
convert.TestItem.from(single.root),
787
convert.TestItem.from(single.root.children.get('id-a') as TestItemImpl),
788
convert.TestItem.from(single.root.children.get('id-a')!.children.get('id-aa') as TestItemImpl),
789
]
790
]);
791
assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs);
792
793
task1.enqueued(single.root.children.get('id-a')!.children.get('id-ab')!);
794
expectedArgs.push([
795
'ctrlId',
796
tracker.id,
797
[
798
convert.TestItem.from(single.root.children.get('id-a') as TestItemImpl),
799
convert.TestItem.from(single.root.children.get('id-a')!.children.get('id-ab') as TestItemImpl),
800
],
801
]);
802
assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs);
803
804
task1.passed(single.root.children.get('id-a')!.children.get('id-ab')!);
805
assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs);
806
807
task1.end();
808
});
809
810
test('adds test messages to run', () => {
811
const test1 = new TestItemImpl('ctrlId', 'id-c', 'test c', URI.file('/testc.txt'));
812
const test2 = new TestItemImpl('ctrlId', 'id-d', 'test d', URI.file('/testd.txt'));
813
test1.range = test2.range = new Range(new Position(0, 0), new Position(1, 0));
814
single.root.children.replace([test1, test2]);
815
const task = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false);
816
817
const message1 = new TestMessage('some message');
818
message1.location = new Location(URI.file('/a.txt'), new Position(0, 0));
819
task.failed(test1, message1);
820
821
const args = proxy.$appendTestMessagesInRun.args[0];
822
assert.deepStrictEqual(proxy.$appendTestMessagesInRun.args[0], [
823
args[0],
824
args[1],
825
new TestId(['ctrlId', 'id-c']).toString(),
826
[{
827
message: 'some message',
828
type: TestMessageType.Error,
829
expected: undefined,
830
contextValue: undefined,
831
actual: undefined,
832
location: convert.location.from(message1.location),
833
stackTrace: undefined,
834
}]
835
]);
836
837
// should use test location as default
838
task.failed(test2, new TestMessage('some message'));
839
assert.deepStrictEqual(proxy.$appendTestMessagesInRun.args[1], [
840
args[0],
841
args[1],
842
new TestId(['ctrlId', 'id-d']).toString(),
843
[{
844
message: 'some message',
845
type: TestMessageType.Error,
846
contextValue: undefined,
847
expected: undefined,
848
actual: undefined,
849
location: convert.location.from({ uri: test2.uri!, range: test2.range }),
850
stackTrace: undefined,
851
}]
852
]);
853
854
task.end();
855
});
856
857
test('guards calls after runs are ended', () => {
858
const task = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false);
859
task.end();
860
861
task.failed(single.root, new TestMessage('some message'));
862
task.appendOutput('output');
863
864
assert.strictEqual(proxy.$addTestsToRun.called, false);
865
assert.strictEqual(proxy.$appendOutputToRun.called, false);
866
assert.strictEqual(proxy.$appendTestMessagesInRun.called, false);
867
});
868
869
test('sets state of test with identical local IDs (#131827)', () => {
870
const testA = single.root.children.get('id-a');
871
const testB = single.root.children.get('id-b');
872
const childA = new TestItemImpl('ctrlId', 'id-child', 'child', undefined);
873
testA!.children.replace([childA]);
874
const childB = new TestItemImpl('ctrlId', 'id-child', 'child', undefined);
875
testB!.children.replace([childB]);
876
877
const task1 = c.createTestRun(ext, 'ctrl', single, new TestRunRequestImpl(), 'hello world', false);
878
const tracker = Iterable.first(c.trackers)!;
879
880
task1.passed(childA);
881
task1.passed(childB);
882
assert.deepStrictEqual(proxy.$addTestsToRun.args, [
883
[
884
'ctrl',
885
tracker.id,
886
[single.root, testA, childA].map(t => convert.TestItem.from(t as TestItemImpl)),
887
],
888
[
889
'ctrl',
890
tracker.id,
891
[single.root, testB, childB].map(t => convert.TestItem.from(t as TestItemImpl)),
892
],
893
]);
894
895
task1.end();
896
});
897
});
898
899
suite('service', () => {
900
let ctrl: TestExtHostTesting;
901
902
class TestExtHostTesting extends ExtHostTesting {
903
public getProfileInternalId(ctrl: TestController, profile: TestRunProfile) {
904
for (const [id, p] of this.controllers.get(ctrl.id)!.profiles) {
905
if (profile === p) {
906
return id;
907
}
908
}
909
910
throw new Error('profile not found');
911
}
912
}
913
914
setup(() => {
915
const rpcProtocol = AnyCallRPCProtocol();
916
ctrl = ds.add(new TestExtHostTesting(
917
rpcProtocol,
918
new NullLogService(),
919
new ExtHostCommands(rpcProtocol, new NullLogService(), new class extends mock<IExtHostTelemetry>() {
920
override onExtensionError(): boolean {
921
return true;
922
}
923
}),
924
new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()),
925
));
926
});
927
928
test('exposes active profiles correctly', async () => {
929
const extA = { ...nullExtensionDescription, identifier: new ExtensionIdentifier('ext.a'), enabledApiProposals: ['testingActiveProfile'] };
930
const extB = { ...nullExtensionDescription, identifier: new ExtensionIdentifier('ext.b'), enabledApiProposals: ['testingActiveProfile'] };
931
932
const ctrlA = ds.add(ctrl.createTestController(extA, 'a', 'ctrla'));
933
const profAA = ds.add(ctrlA.createRunProfile('aa', TestRunProfileKind.Run, () => { }));
934
const profAB = ds.add(ctrlA.createRunProfile('ab', TestRunProfileKind.Run, () => { }));
935
936
const ctrlB = ds.add(ctrl.createTestController(extB, 'b', 'ctrlb'));
937
const profBA = ds.add(ctrlB.createRunProfile('ba', TestRunProfileKind.Run, () => { }));
938
const profBB = ds.add(ctrlB.createRunProfile('bb', TestRunProfileKind.Run, () => { }));
939
const neverCalled = sinon.stub();
940
941
// empty default state:
942
assert.deepStrictEqual(profAA.isDefault, false);
943
assert.deepStrictEqual(profBA.isDefault, false);
944
assert.deepStrictEqual(profBB.isDefault, false);
945
946
// fires a change event:
947
const changeA = Event.toPromise(profAA.onDidChangeDefault as Event<boolean>);
948
const changeBA = Event.toPromise(profBA.onDidChangeDefault as Event<boolean>);
949
const changeBB = Event.toPromise(profBB.onDidChangeDefault as Event<boolean>);
950
951
ds.add(profAB.onDidChangeDefault(neverCalled));
952
assert.strictEqual(neverCalled.called, false);
953
954
ctrl.$setDefaultRunProfiles({
955
a: [ctrl.getProfileInternalId(ctrlA, profAA)],
956
b: [ctrl.getProfileInternalId(ctrlB, profBA), ctrl.getProfileInternalId(ctrlB, profBB)]
957
});
958
959
assert.deepStrictEqual(await changeA, true);
960
assert.deepStrictEqual(await changeBA, true);
961
assert.deepStrictEqual(await changeBB, true);
962
963
// updates internal state:
964
assert.deepStrictEqual(profAA.isDefault, true);
965
assert.deepStrictEqual(profBA.isDefault, true);
966
assert.deepStrictEqual(profBB.isDefault, true);
967
assert.deepStrictEqual(profAB.isDefault, false);
968
969
// no-ops if equal
970
ds.add(profAA.onDidChangeDefault(neverCalled));
971
ctrl.$setDefaultRunProfiles({
972
a: [ctrl.getProfileInternalId(ctrlA, profAA)],
973
});
974
assert.strictEqual(neverCalled.called, false);
975
});
976
});
977
});
978
979