Skip to content

Commit 3adea2f

Browse files
authored
testing: handle default profile changes in watch, nested cancellations (microsoft#203583)
Fixes microsoft#203496 Fixes microsoft#203497
1 parent fe0632c commit 3adea2f

File tree

4 files changed

+144
-56
lines changed

4 files changed

+144
-56
lines changed

src/vs/base/common/prefixTree.ts

Lines changed: 69 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -48,33 +48,57 @@ export class WellDefinedPrefixTree<V> {
4848

4949
/** Deletes a node from the prefix tree, returning the value it contained. */
5050
delete(key: Iterable<string>): V | undefined {
51-
const path = [{ part: '', node: this.root }];
52-
let i = 0;
53-
for (const part of key) {
54-
const node = path[i].node.children?.get(part);
55-
if (!node) {
56-
return undefined; // node not in tree
57-
}
58-
59-
path.push({ part, node });
60-
i++;
51+
const path = this.getPathToKey(key);
52+
if (!path) {
53+
return;
6154
}
6255

56+
let i = path.length - 1;
6357
const value = path[i].node._value;
6458
if (value === unset) {
6559
return; // not actually a real node
6660
}
6761

6862
this._size--;
63+
path[i].node._value = unset;
64+
6965
for (; i > 0; i--) {
66+
const { node, part } = path[i];
67+
if (node.children?.size || node._value !== unset) {
68+
break;
69+
}
70+
71+
path[i - 1].node.children!.delete(part);
72+
}
73+
74+
return value;
75+
}
76+
77+
/** Deletes a subtree from the prefix tree, returning the values they contained. */
78+
*deleteRecursive(key: Iterable<string>): Iterable<V> {
79+
const path = this.getPathToKey(key);
80+
if (!path) {
81+
return;
82+
}
83+
84+
const subtree = path[path.length - 1].node;
85+
86+
// important: run the deletion before we start to yield results, so that
87+
// it still runs even if the caller doesn't consumer the iterator
88+
for (let i = path.length - 1; i > 0; i--) {
7089
const parent = path[i - 1];
7190
parent.node.children!.delete(path[i].part);
7291
if (parent.node.children!.size > 0 || parent.node._value !== unset) {
7392
break;
7493
}
7594
}
7695

77-
return value;
96+
for (const node of bfsIterate(subtree)) {
97+
if (node._value !== unset) {
98+
this._size--;
99+
yield node._value;
100+
}
101+
}
78102
}
79103

80104
/** Gets a value from the tree. */
@@ -140,6 +164,22 @@ export class WellDefinedPrefixTree<V> {
140164
return node._value !== unset;
141165
}
142166

167+
private getPathToKey(key: Iterable<string>) {
168+
const path = [{ part: '', node: this.root }];
169+
let i = 0;
170+
for (const part of key) {
171+
const node = path[i].node.children?.get(part);
172+
if (!node) {
173+
return; // node not in tree
174+
}
175+
176+
path.push({ part, node });
177+
i++;
178+
}
179+
180+
return path;
181+
}
182+
143183
private opNode(key: Iterable<string>, fn: (node: Node<V>) => void, onDescend?: (node: Node<V>) => void): void {
144184
let node = this.root;
145185
for (const part of key) {
@@ -157,26 +197,31 @@ export class WellDefinedPrefixTree<V> {
157197
onDescend?.(node);
158198
}
159199

160-
if (node._value === unset) {
161-
this._size++;
162-
}
163-
200+
const sizeBefore = node._value === unset ? 0 : 1;
164201
fn(node);
202+
const sizeAfter = node._value === unset ? 0 : 1;
203+
this._size += sizeAfter - sizeBefore;
165204
}
166205

167206
/** Returns an iterable of the tree values in no defined order. */
168207
*values() {
169-
const stack = [this.root];
170-
while (stack.length > 0) {
171-
const node = stack.pop()!;
172-
if (node._value !== unset) {
173-
yield node._value;
208+
for (const { _value } of bfsIterate(this.root)) {
209+
if (_value !== unset) {
210+
yield _value;
174211
}
212+
}
213+
}
214+
}
215+
216+
function* bfsIterate<T>(root: Node<T>): Iterable<Node<T>> {
217+
const stack = [root];
218+
while (stack.length > 0) {
219+
const node = stack.pop()!;
220+
yield node;
175221

176-
if (node.children) {
177-
for (const child of node.children.values()) {
178-
stack.push(child);
179-
}
222+
if (node.children) {
223+
for (const child of node.children.values()) {
224+
stack.push(child);
180225
}
181226
}
182227
}

src/vs/base/test/common/prefixTree.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,26 @@ suite('WellDefinedPrefixTree', () => {
137137

138138
assert.deepStrictEqual([...tree.values()], [43, 42]);
139139
});
140+
141+
142+
test('delete recursive', () => {
143+
const key1 = ['foo', 'bar'];
144+
const key2 = ['foo', 'bar', 'baz'];
145+
const key3 = ['foo', 'bar', 'baz2', 'baz3'];
146+
const key4 = ['foo', 'bar2'];
147+
tree.insert(key1, 42);
148+
tree.insert(key2, 43);
149+
tree.insert(key3, 44);
150+
tree.insert(key4, 45);
151+
assert.strictEqual(tree.size, 4);
152+
153+
assert.deepStrictEqual([...tree.deleteRecursive(key1)], [42, 44, 43]);
154+
assert.strictEqual(tree.size, 1);
155+
156+
assert.deepStrictEqual([...tree.deleteRecursive(key1)], []);
157+
assert.strictEqual(tree.size, 1);
158+
159+
assert.deepStrictEqual([...tree.deleteRecursive(key4)], [45]);
160+
assert.strictEqual(tree.size, 0);
161+
});
140162
});

src/vs/workbench/contrib/testing/browser/testExplorerActions.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -290,21 +290,14 @@ export class ContinuousRunTestAction extends Action2 {
290290

291291
public override async run(accessor: ServicesAccessor, ...elements: TestItemTreeElement[]): Promise<any> {
292292
const crService = accessor.get(ITestingContinuousRunService);
293-
const profileService = accessor.get(ITestProfileService);
294293
for (const element of elements) {
295294
const id = element.test.item.extId;
296295
if (crService.isSpecificallyEnabledFor(id)) {
297296
crService.stop(id);
298297
continue;
299298
}
300299

301-
const profiles = profileService.getGroupDefaultProfiles(TestRunProfileBitset.Run)
302-
.filter(p => p.supportsContinuousRun && p.controllerId === element.test.controllerId);
303-
if (!profiles.length) {
304-
continue;
305-
}
306-
307-
crService.start(profiles, id);
300+
crService.start(TestRunProfileBitset.Run, id);
308301
}
309302
}
310303
}

src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { CancellationTokenSource } from 'vs/base/common/cancellation';
7-
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
7+
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
88
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
99
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
1010
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
1111
import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue';
1212
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
1313
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
1414
import { TestService } from 'vs/workbench/contrib/testing/common/testServiceImpl';
15-
import { ITestRunProfile } from 'vs/workbench/contrib/testing/common/testTypes';
15+
import { ITestRunProfile, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes';
1616
import { Emitter, Event } from 'vs/base/common/event';
1717
import { TestId } from 'vs/workbench/contrib/testing/common/testId';
1818
import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree';
19+
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
20+
import * as arrays from 'vs/base/common/arrays';
1921

2022
export const ITestingContinuousRunService = createDecorator<ITestingContinuousRunService>('testingContinuousRunService');
2123

@@ -56,10 +58,11 @@ export interface ITestingContinuousRunService {
5658
isEnabled(): boolean;
5759

5860
/**
59-
* Starts a continuous auto run with a specific profile or set of profiles.
60-
* Globally if no test is given, for a specific test otherwise.
61+
* Starts a continuous auto run with a specific set of profiles, or all
62+
* default profiles in a group. Globally if no test is given,
63+
* for a specific test otherwise.
6164
*/
62-
start(profile: ITestRunProfile[], testId?: string): void;
65+
start(profile: ITestRunProfile[] | TestRunProfileBitset, testId?: string): void;
6366

6467
/**
6568
* Stops any continuous run
@@ -72,8 +75,8 @@ export class TestingContinuousRunService extends Disposable implements ITestingC
7275
declare readonly _serviceBrand: undefined;
7376

7477
private readonly changeEmitter = new Emitter<string | undefined>();
75-
private globallyRunning?: CancellationTokenSource;
76-
private readonly running = new WellDefinedPrefixTree<CancellationTokenSource>();
78+
private globallyRunning?: IDisposable;
79+
private readonly running = new WellDefinedPrefixTree<IDisposable>();
7780
private readonly lastRun: StoredValue<Set<number>>;
7881
private readonly isGloballyOn: IContextKey<boolean>;
7982

@@ -87,6 +90,7 @@ export class TestingContinuousRunService extends Disposable implements ITestingC
8790
@ITestService private readonly testService: TestService,
8891
@IStorageService storageService: IStorageService,
8992
@IContextKeyService contextKeyService: IContextKeyService,
93+
@ITestProfileService private readonly testProfileService: ITestProfileService,
9094
) {
9195
super();
9296
this.isGloballyOn = TestingContextKeys.isContinuousModeOn.bindTo(contextKeyService);
@@ -133,45 +137,69 @@ export class TestingContinuousRunService extends Disposable implements ITestingC
133137
}
134138

135139
/** @inheritdoc */
136-
public start(profile: ITestRunProfile[], testId?: string): void {
140+
public start(profiles: ITestRunProfile[] | TestRunProfileBitset, testId?: string): void {
141+
const store = new DisposableStore();
137142
const cts = new CancellationTokenSource();
143+
store.add(toDisposable(() => cts.dispose(true)));
138144

139145
if (testId === undefined) {
140146
this.isGloballyOn.set(true);
141147
}
142148

143149
if (!testId) {
144-
this.globallyRunning?.dispose(true);
145-
this.globallyRunning = cts;
150+
this.globallyRunning?.dispose();
151+
this.globallyRunning = store;
146152
} else {
147153
this.running.mutate(TestId.fromString(testId).path, c => {
148-
c?.dispose(true);
149-
return cts;
154+
c?.dispose();
155+
return store;
150156
});
151157
}
152158

153-
this.lastRun.store(new Set(profile.map(p => p.profileId)));
159+
let actualProfiles: ITestRunProfile[];
160+
if (profiles instanceof Array) {
161+
actualProfiles = profiles;
162+
} else {
163+
// restart the continuous run when default profiles change, if we were
164+
// asked to run for a group
165+
const getRelevant = () => this.testProfileService.getGroupDefaultProfiles(profiles)
166+
.filter(p => p.supportsContinuousRun && (!testId || TestId.root(testId) === p.controllerId));
167+
actualProfiles = getRelevant();
168+
store.add(this.testProfileService.onDidChange(() => {
169+
if (!arrays.equals(getRelevant(), actualProfiles)) {
170+
this.start(profiles, testId);
171+
}
172+
}));
173+
}
154174

155-
this.testService.startContinuousRun({
156-
continuous: true,
157-
targets: profile.map(p => ({
158-
testIds: [testId ?? p.controllerId],
159-
controllerId: p.controllerId,
160-
profileGroup: p.group,
161-
profileId: p.profileId
162-
})),
163-
}, cts.token);
175+
this.lastRun.store(new Set(actualProfiles.map(p => p.profileId)));
176+
177+
if (actualProfiles.length) {
178+
this.testService.startContinuousRun({
179+
continuous: true,
180+
targets: actualProfiles.map(p => ({
181+
testIds: [testId ?? p.controllerId],
182+
controllerId: p.controllerId,
183+
profileGroup: p.group,
184+
profileId: p.profileId
185+
})),
186+
}, cts.token);
187+
}
164188

165189
this.changeEmitter.fire(testId);
166190
}
167191

168192
/** @inheritdoc */
169193
public stop(testId?: string): void {
170194
if (!testId) {
171-
this.globallyRunning?.dispose(true);
195+
this.globallyRunning?.dispose();
172196
this.globallyRunning = undefined;
173197
} else {
174-
this.running.delete(TestId.fromString(testId).path)?.dispose(true);
198+
const cancellations = [...this.running.deleteRecursive(TestId.fromString(testId).path)];
199+
// deleteRecursive returns a BFS order, reverse it so children are cancelled before parents
200+
for (let i = cancellations.length - 1; i >= 0; i--) {
201+
cancellations[i].dispose();
202+
}
175203
}
176204

177205
if (testId === undefined) {

0 commit comments

Comments
 (0)