Skip to content

Commit 6635cc0

Browse files
committed
fix: harden additional test source discovery
1 parent c640768 commit 6635cc0

6 files changed

Lines changed: 102 additions & 10 deletions

File tree

‎package.nls.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"contributes.commands.java.test.goToTest.title": "Go to Test",
88
"contributes.commands.java.test.goToTestSubject.title": "Go to Test Subject",
99
"configuration.java.test.defaultConfig.description": "Specify the name of the default test configuration.",
10-
"configuration.java.test.additionalTestSourcePaths.description": "Additional source paths or glob patterns to scan for Java tests.",
10+
"configuration.java.test.additionalTestSourcePaths.description": "Additional source paths or glob patterns to scan for Java tests. Broad patterns may slow discovery.",
1111
"configuration.java.test.config.description": "Specify the configurations for running the tests.",
1212
"configuration.java.test.config.item.description": "Specify the configuration item for running the tests.",
1313
"configuration.java.test.config.name.description": "Specify the name of the configuration item.",

‎package.nls.zh.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"configuration.java.test.additionalTestSourcePaths.description": "用于搜索 Java 测试的其他源路径或 glob 模式。",
2+
"configuration.java.test.additionalTestSourcePaths.description": "用于搜索 Java 测试的其他源路径或 glob 模式。过于宽泛的模式可能会降低发现速度。",
33
"description": "运行并调试 JUnit 或 TestNG 测试用例",
44
"contributes.commands.java.test.editor.run.title": "运行测试用例",
55
"contributes.commands.java.test.editor.debug.title": "调试测试用例",

‎src/controller/testController.ts‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,24 @@ export function creatTestProfile(name: string, kind: TestRunProfileKind): void {
5151
testController?.createRunProfile(name, kind, runHandler, false, runnableTag);
5252
}
5353

54-
export async function refreshTestSourcePaths(): Promise<void> {
55-
testSourceProvider.clear();
56-
await refreshExplorer();
57-
await startWatchingWorkspace();
54+
let refreshTestSourcePathsPromise: Promise<void> | undefined;
55+
let refreshTestSourcePathsRequested: boolean = false;
56+
57+
export function refreshTestSourcePaths(): Promise<void> {
58+
refreshTestSourcePathsRequested = true;
59+
if (!refreshTestSourcePathsPromise) {
60+
refreshTestSourcePathsPromise = (async () => {
61+
while (refreshTestSourcePathsRequested) {
62+
refreshTestSourcePathsRequested = false;
63+
testSourceProvider.clear();
64+
await refreshExplorer();
65+
await startWatchingWorkspace();
66+
}
67+
})().finally(() => {
68+
refreshTestSourcePathsPromise = undefined;
69+
});
70+
}
71+
return refreshTestSourcePathsPromise;
5872
}
5973

6074
export const loadChildren: (item: TestItem, token?: CancellationToken, force?: boolean) => Promise<void> = instrumentOperation('java.test.explorer.loadChildren', async (_operationId: string, item: TestItem, token?: CancellationToken, force: boolean = false) => {

‎src/provider/testSourceProvider.ts‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ class TestSourcePathProvider {
9191
}
9292

9393
let additionalTestSourceOutputChannel: OutputChannel | undefined;
94+
const LARGE_ADDITIONAL_TEST_SOURCE_COUNT: number = 100;
9495

9596
export function mergeTestSourcePaths(testPaths: ITestSourcePath[], additionalPaths: string[]): ITestSourcePath[] {
9697
const mergedPaths: ITestSourcePath[] = [];
@@ -153,14 +154,22 @@ export async function resolveAdditionalTestSourcePaths(workspacePath: string, co
153154
}
154155
} catch (error) {
155156
const message: string = error instanceof Error ? error.message : String(error);
156-
additionalTestSourceOutputChannel ??= window.createOutputChannel('Test Runner for Java');
157-
additionalTestSourceOutputChannel.appendLine(
158-
`Failed to expand additional test source pattern "${configuredPath}": ${message}`);
157+
logAdditionalTestSourceMessage(`Failed to expand additional test source pattern "${configuredPath}": ${message}`);
159158
}
160159
}
160+
161+
if (paths.length > LARGE_ADDITIONAL_TEST_SOURCE_COUNT) {
162+
logAdditionalTestSourceMessage(
163+
`Additional test source patterns matched ${paths.length} directories. Broad patterns may slow discovery.`);
164+
}
161165
return paths;
162166
}
163167

168+
function logAdditionalTestSourceMessage(message: string): void {
169+
additionalTestSourceOutputChannel ??= window.createOutputChannel('Test Runner for Java');
170+
additionalTestSourceOutputChannel.appendLine(message);
171+
}
172+
164173
function addUniquePath(paths: string[], pathKeys: Set<string>, sourcePath: string): void {
165174
const key: string = getPathKey(sourcePath);
166175
if (!pathKeys.has(key)) {

‎test/suite/testController.loadChildren.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,37 @@ suite('testController - loadChildren', () => {
5656
sinon.assert.callOrder(clearPathsStub, refreshExplorerStub, watcherPatternsStub, additionalPathsStub);
5757
});
5858

59+
test('serializes overlapping source-path refresh requests', async () => {
60+
let releaseFirstRefresh!: () => void;
61+
let signalFirstRefreshStarted!: () => void;
62+
const firstRefreshStarted: Promise<void> = new Promise((resolve) => {
63+
signalFirstRefreshStarted = resolve;
64+
});
65+
const firstRefreshGate: Promise<void> = new Promise((resolve) => {
66+
releaseFirstRefresh = resolve;
67+
});
68+
const clearPathsStub = sinon.stub(testSourceProvider, 'clear');
69+
const refreshExplorerStub = sinon.stub(testExplorerCommands, 'refreshExplorer').callsFake(async () => {
70+
if (refreshExplorerStub.callCount === 1) {
71+
signalFirstRefreshStarted();
72+
await firstRefreshGate;
73+
}
74+
});
75+
const watcherPatternsStub = sinon.stub(testSourceProvider, 'getTestSourcePattern').resolves([]);
76+
const additionalPathsStub = sinon.stub(testSourceProvider, 'getAdditionalTestSourcePaths').resolves([]);
77+
78+
const firstRefresh: Promise<void> = refreshTestSourcePaths();
79+
await firstRefreshStarted;
80+
const overlappingRefresh: Promise<void> = refreshTestSourcePaths();
81+
releaseFirstRefresh();
82+
await Promise.all([firstRefresh, overlappingRefresh]);
83+
84+
assert.strictEqual(clearPathsStub.callCount, 2);
85+
assert.strictEqual(refreshExplorerStub.callCount, 2);
86+
assert.strictEqual(watcherPatternsStub.callCount, 2);
87+
assert.strictEqual(additionalPathsStub.callCount, 2);
88+
});
89+
5990
test('scans Java files found under configured additional source roots', async () => {
6091
const javaFile: Uri = Uri.file('/workspace/additional/src/MainSourceTest.java');
6192
const refreshExplorerStub = sinon.stub(testExplorerCommands, 'refreshExplorer').resolves();

‎test/suite/testSourceProvider.test.ts‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import * as assert from 'assert';
55
import * as fse from 'fs-extra';
66
import * as os from 'os';
77
import * as path from 'path';
8-
import { ITestSourcePath, mergeTestSourcePaths, resolveAdditionalTestSourcePaths } from '../../src/provider/testSourceProvider';
8+
import * as sinon from 'sinon';
9+
import { RelativePattern, Uri, WorkspaceConfiguration, WorkspaceFolder, workspace } from 'vscode';
10+
import * as commandUtils from '../../src/utils/commandUtils';
11+
import { ITestSourcePath, mergeTestSourcePaths, resolveAdditionalTestSourcePaths, testSourceProvider } from '../../src/provider/testSourceProvider';
912

1013
suite('testSourceProvider', () => {
1114
test('returns no extra paths by default', async () => {
@@ -70,6 +73,41 @@ suite('testSourceProvider', () => {
7073
}
7174
});
7275

76+
test('includes additional source roots in file watcher patterns', async () => {
77+
const workspacePath: string = fse.mkdtempSync(path.join(os.tmpdir(), 'java-test-source-paths-'));
78+
const sourceRoots: string[] = [
79+
path.join(workspacePath, 'modules', 'app', 'src', 'main', 'java'),
80+
path.join(workspacePath, 'modules', 'cli', 'src', 'main', 'java'),
81+
];
82+
sourceRoots.forEach((sourceRoot: string) => fse.ensureDirSync(sourceRoot));
83+
const workspaceFolder: WorkspaceFolder = {
84+
uri: Uri.file(workspacePath),
85+
name: 'additional-source-paths-test',
86+
index: 0,
87+
};
88+
const configuration: WorkspaceConfiguration = {
89+
get: () => ['modules/*/src/main/java'],
90+
} as unknown as WorkspaceConfiguration;
91+
92+
testSourceProvider.clear();
93+
sinon.stub(workspace, 'getConfiguration').returns(configuration);
94+
sinon.stub(commandUtils, 'executeJavaLanguageServerCommand').resolves([]);
95+
try {
96+
const patterns: RelativePattern[] = await testSourceProvider.getTestSourcePattern(workspaceFolder);
97+
98+
const normalizePath = (sourcePath: string): string => process.platform === 'win32'
99+
? path.normalize(sourcePath).toLowerCase()
100+
: path.normalize(sourcePath);
101+
assert.deepStrictEqual(
102+
patterns.map((pattern: RelativePattern) => normalizePath(pattern.baseUri.fsPath)).sort(),
103+
sourceRoots.map(normalizePath).sort());
104+
} finally {
105+
testSourceProvider.clear();
106+
sinon.restore();
107+
fse.removeSync(workspacePath);
108+
}
109+
});
110+
73111
test('supports Windows separators in source-root patterns', async () => {
74112
const workspacePath: string = fse.mkdtempSync(path.join(os.tmpdir(), 'java-test-source-paths-'));
75113
const sourceRoot: string = path.join(workspacePath, 'modules', 'app', 'src', 'main', 'java');

0 commit comments

Comments
 (0)