Skip to content

Commit 6131f6f

Browse files
committed
cleanup
1 parent 5d14505 commit 6131f6f

11 files changed

Lines changed: 125 additions & 109 deletions

File tree

src/client/telemetry/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export enum EventName {
4242
// Python testing specific telemetry
4343
UNITTEST_CONFIGURING = 'UNITTEST.CONFIGURING',
4444
UNITTEST_CONFIGURE = 'UNITTEST.CONFIGURE',
45+
UNITTEST_DISCOVERY_TRIGGER = 'UNITTEST.DISCOVERY.TRIGGER',
4546
UNITTEST_DISCOVERING = 'UNITTEST.DISCOVERING',
4647
UNITTEST_DISCOVERING_STOP = 'UNITTEST.DISCOVERY.STOP',
4748
UNITTEST_DISCOVERY_DONE = 'UNITTEST.DISCOVERY.DONE',

src/client/telemetry/index.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2122,6 +2122,27 @@ export interface IEventNamePropertyMapping {
21222122
*/
21232123
interpreterType?: EnvironmentType;
21242124
};
2125+
/**
2126+
* Telemetry event sent indicating the trigger source for discovery.
2127+
*/
2128+
/* __GDPR__
2129+
"unittest.discovery.trigger" : {
2130+
"trigger" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
2131+
}
2132+
*/
2133+
[EventName.UNITTEST_DISCOVERY_TRIGGER]: {
2134+
/**
2135+
* Carries the source which triggered discovering of tests
2136+
*
2137+
* @type {('auto' | 'ui' | 'commandpalette' | 'watching' | 'interpreter')}
2138+
* auto : Triggered by VS Code editor.
2139+
* ui : Triggered by clicking a button.
2140+
* commandpalette : Triggered by running the command from the command palette.
2141+
* watching : Triggered by filesystem or content changes.
2142+
* interpreter : Triggered by interpreter change.
2143+
*/
2144+
trigger: 'auto' | 'ui' | 'commandpalette' | 'watching' | 'interpreter';
2145+
};
21252146
/**
21262147
* Telemetry event sent with details about discovering tests
21272148
*/
@@ -2166,8 +2187,9 @@ export interface IEventNamePropertyMapping {
21662187
*/
21672188
failed: boolean;
21682189
/**
2169-
* Discovery code path: 'project' = Python Environments API / project-based;
2170-
* 'legacy' = single-workspace adapter fallback.
2190+
* Testing architecture used for discovery:
2191+
* 'project' = per-project discovery through the Python Environments API;
2192+
* 'legacy' = workspace-level discovery through the existing WorkspaceTestAdapter.
21712193
*/
21722194
mode?: 'project' | 'legacy';
21732195
/**

src/client/testing/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ export class UnitTestManagementService implements IExtensionActivationService {
222222
}),
223223
interpreterService.onDidChangeInterpreter(async () => {
224224
traceVerbose('Testing: Triggered refresh due to interpreter change.');
225+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_TRIGGER, undefined, { trigger: 'interpreter' });
225226
await this.testController?.refreshTestData(undefined, { forceRefresh: true, trigger: 'interpreter' });
226227
}),
227228
);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { StopWatch } from '../../../common/utils/stopWatch';
5+
6+
/** Source that requested a test discovery refresh. */
7+
export type DiscoveryTriggerKind = 'auto' | 'ui' | 'commandpalette' | 'watching' | 'interpreter';
8+
9+
/** Testing architecture used for discovery. */
10+
export type DiscoveryMode = 'project' | 'legacy';
11+
12+
export interface DiscoveryTelemetryCycle {
13+
mode: DiscoveryMode;
14+
trigger?: DiscoveryTriggerKind;
15+
stopWatch: StopWatch;
16+
}
17+
18+
export class DiscoveryTelemetryState {
19+
private activeCycle?: DiscoveryTelemetryCycle;
20+
21+
constructor(public readonly defaultMode: DiscoveryMode) {}
22+
23+
public start(context: Omit<DiscoveryTelemetryCycle, 'stopWatch'>): void {
24+
this.activeCycle = { ...context, stopWatch: new StopWatch() };
25+
}
26+
27+
public complete(): DiscoveryTelemetryCycle | undefined {
28+
const cycle = this.activeCycle;
29+
this.activeCycle = undefined;
30+
return cycle;
31+
}
32+
}

src/client/testing/testController/common/resultResolver.ts

Lines changed: 8 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,11 @@ import { TestProvider } from '../../types';
77
import { traceInfo } from '../../../logging';
88
import { sendTelemetryEvent } from '../../../telemetry';
99
import { EventName } from '../../../telemetry/constants';
10-
import { StopWatch } from '../../../common/utils/stopWatch';
1110
import { TestItemIndex } from './testItemIndex';
1211
import { TestDiscoveryHandler } from './testDiscoveryHandler';
1312
import { TestExecutionHandler } from './testExecutionHandler';
1413
import { TestCoverageHandler } from './testCoverageHandler';
15-
import { DiscoveredTestNode, DiscoveredTestItem } from './types';
16-
17-
/**
18-
* Trigger source label for the current discovery cycle.
19-
*/
20-
export type DiscoveryTriggerKind = 'auto' | 'ui' | 'commandpalette' | 'watching' | 'interpreter';
21-
22-
/**
23-
* Per-cycle context the controller passes to the resolver so DISCOVERY_DONE can
24-
* include trigger source, mode, and wall-clock duration without having to plumb
25-
* these through every adapter call.
26-
*/
27-
export interface DiscoveryCycleContext {
28-
mode: 'project' | 'legacy';
29-
trigger?: DiscoveryTriggerKind;
30-
stopWatch: StopWatch;
31-
}
14+
import { DiscoveryTelemetryState } from './discoveryTelemetry';
3215

3316
export class PythonResultResolver implements ITestResultResolver {
3417
testController: TestController;
@@ -44,6 +27,8 @@ export class PythonResultResolver implements ITestResultResolver {
4427

4528
public detailedCoverageMap = new Map<string, FileCoverageDetail[]>();
4629

30+
public readonly discoveryTelemetry: DiscoveryTelemetryState;
31+
4732
/**
4833
* Optional project ID for scoping test IDs.
4934
* When set, all test IDs are prefixed with `{projectId}@@vsc@@` for project-based testing.
@@ -57,12 +42,6 @@ export class PythonResultResolver implements ITestResultResolver {
5742
*/
5843
private projectName?: string;
5944

60-
/**
61-
* Per-cycle telemetry context set by the controller before invoking discovery.
62-
* Consumed (and cleared) by resolveDiscovery to emit UNITTEST_DISCOVERY_DONE.
63-
*/
64-
private discoveryCycle?: DiscoveryCycleContext;
65-
6645
constructor(
6746
testController: TestController,
6847
testProvider: TestProvider,
@@ -76,6 +55,7 @@ export class PythonResultResolver implements ITestResultResolver {
7655
this.projectName = projectName;
7756
// Initialize a new TestItemIndex which will be used to track test items in this workspace/project
7857
this.testItemIndex = new TestItemIndex();
58+
this.discoveryTelemetry = new DiscoveryTelemetryState(projectId ? 'project' : 'legacy');
7959
}
8060

8161
// Expose for backward compatibility (WorkspaceTestAdapter accesses these)
@@ -99,41 +79,8 @@ export class PythonResultResolver implements ITestResultResolver {
9979
return this.projectId;
10080
}
10181

102-
/**
103-
* Set per-discovery-cycle telemetry context. Called by the controller right
104-
* before invoking the discovery adapter so resolveDiscovery / failure paths
105-
* can include trigger, mode, and duration in UNITTEST_DISCOVERY_DONE.
106-
*/
107-
public beginDiscoveryCycle(ctx: Omit<DiscoveryCycleContext, 'stopWatch'>): void {
108-
this.discoveryCycle = { ...ctx, stopWatch: new StopWatch() };
109-
}
110-
111-
/**
112-
* Returns and clears the current discovery cycle context, if any.
113-
*/
114-
private takeDiscoveryCycle(): DiscoveryCycleContext | undefined {
115-
const cycle = this.discoveryCycle;
116-
this.discoveryCycle = undefined;
117-
return cycle;
118-
}
119-
120-
/**
121-
* Returns the current discovery cycle context without clearing it.
122-
* Used by error paths that still want to clear via takeDiscoveryCycle.
123-
*/
124-
public peekDiscoveryCycle(): DiscoveryCycleContext | undefined {
125-
return this.discoveryCycle;
126-
}
127-
128-
/**
129-
* Clears the current discovery cycle context.
130-
*/
131-
public clearDiscoveryCycle(): void {
132-
this.discoveryCycle = undefined;
133-
}
134-
13582
public resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void {
136-
PythonResultResolver.discoveryHandler.processDiscovery(
83+
const testCount = PythonResultResolver.discoveryHandler.processDiscovery(
13784
payload,
13885
this.testController,
13986
this.testItemIndex,
@@ -143,15 +90,15 @@ export class PythonResultResolver implements ITestResultResolver {
14390
this.projectId,
14491
this.projectName,
14592
);
146-
const cycle = this.takeDiscoveryCycle();
147-
const mode = cycle?.mode ?? (this.projectId ? 'project' : 'legacy');
93+
const cycle = this.discoveryTelemetry.complete();
94+
const mode = cycle?.mode ?? this.discoveryTelemetry.defaultMode;
14895
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, {
14996
tool: this.testProvider,
15097
failed: false,
15198
mode,
15299
trigger: cycle?.trigger,
153100
totalDurationMs: cycle?.stopWatch.elapsedTime,
154-
testCount: payload?.tests ? countDiscoveredTests(payload.tests) : 0,
101+
testCount,
155102
});
156103
}
157104

@@ -198,19 +145,3 @@ export class PythonResultResolver implements ITestResultResolver {
198145
this.testItemIndex.cleanupStaleReferences(this.testController);
199146
}
200147
}
201-
202-
/**
203-
* Recursively counts leaf test items in a discovered test tree.
204-
* Used to populate UNITTEST_DISCOVERY_DONE.testCount.
205-
*/
206-
function countDiscoveredTests(node: DiscoveredTestNode | DiscoveredTestItem): number {
207-
if ((node as DiscoveredTestNode).children === undefined) {
208-
// No children -> leaf (DiscoveredTestItem).
209-
return 1;
210-
}
211-
let total = 0;
212-
for (const child of (node as DiscoveredTestNode).children) {
213-
total += countDiscoveredTests(child);
214-
}
215-
return total;
216-
}

src/client/testing/testController/common/testDiscoveryHandler.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ export class TestDiscoveryHandler {
3030
token?: CancellationToken,
3131
projectId?: string,
3232
projectName?: string,
33-
): void {
33+
): number {
3434
if (!payload) {
3535
// No test data is available
36-
return;
36+
return 0;
3737
}
3838

3939
const workspacePath = workspaceUri.fsPath;
@@ -57,10 +57,14 @@ export class TestDiscoveryHandler {
5757
// Clear existing mappings before rebuilding test tree
5858
testItemIndex.clear();
5959

60+
if (rawTestData.tests === null) {
61+
return 0;
62+
}
63+
6064
// If the test root for this folder exists: Workspace refresh, update its children.
6165
// Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree.
6266
// Note: populateTestTree will call testItemIndex.registerTestItem() for each discovered test
63-
populateTestTree(
67+
return populateTestTree(
6468
testController,
6569
rawTestData.tests,
6670
undefined,
@@ -74,6 +78,8 @@ export class TestDiscoveryHandler {
7478
projectName,
7579
);
7680
}
81+
82+
return 0;
7783
}
7884

7985
/**

src/client/testing/testController/common/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { ITestDebugLauncher } from '../../common/types';
1717
import { IPythonExecutionFactory } from '../../../common/process/types';
1818
import { PythonEnvironment } from '../../../pythonEnvironments/info';
1919
import { ProjectAdapter } from './projectAdapter';
20+
import { DiscoveryTriggerKind } from './discoveryTelemetry';
2021

2122
export enum TestDataKinds {
2223
Workspace,
@@ -36,7 +37,7 @@ export interface TestData {
3637

3738
export type TestRefreshOptions = {
3839
forceRefresh: boolean;
39-
trigger?: 'auto' | 'ui' | 'commandpalette' | 'watching' | 'interpreter';
40+
trigger?: DiscoveryTriggerKind;
4041
};
4142

4243
export const ITestController = Symbol('ITestController');

src/client/testing/testController/common/utils.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,10 @@ export function populateTestTree(
231231
token?: CancellationToken,
232232
projectId?: string,
233233
projectName?: string,
234-
): void {
234+
): number {
235+
// Count leaf tests while walking the tree so telemetry does not need a second traversal.
236+
let testCount = 0;
237+
235238
// If testRoot is undefined, use the info of the root item of testTreeData to create a test item, and append it to the test controller.
236239
if (!testRoot) {
237240
// Create project-scoped ID if projectId is provided
@@ -275,6 +278,7 @@ export function populateTestTree(
275278
testItemMappings.runIdToTestItem.set(child.runID, testItem);
276279
testItemMappings.runIdToVSid.set(child.runID, vsId);
277280
testItemMappings.vsIdToRunId.set(vsId, child.runID);
281+
testCount += 1;
278282
} else {
279283
// Use project-scoped ID for non-test nodes and look up within the current root
280284
const nodeId = projectId ? `${projectId}${PROJECT_ID_SEPARATOR}${child.id_}` : child.id_;
@@ -302,10 +306,20 @@ export function populateTestTree(
302306

303307
testRoot!.children.add(node);
304308
}
305-
populateTestTree(testController, child, node, testItemMappings, token, projectId, projectName);
309+
testCount += populateTestTree(
310+
testController,
311+
child,
312+
node,
313+
testItemMappings,
314+
token,
315+
projectId,
316+
projectName,
317+
);
306318
}
307319
}
308320
});
321+
322+
return testCount;
309323
}
310324

311325
function isTestItem(test: DiscoveredTestNode | DiscoveredTestItem): test is DiscoveredTestItem {

0 commit comments

Comments
 (0)