Ignore @SpringBootApplication classes in test source folders - #421
Ignore @SpringBootApplication classes in test source folders#421Martin Lippert (martinlippert) wants to merge 2 commits into
Conversation
`vscode.java.resolveMainClass` searches every source folder of a project, so a `@SpringBootApplication` class copied into `src/test/java` — a common pattern for integration tests — was offered as an additional launch candidate. Running an app then popped up a quick pick asking the user to choose between the real main class and one that is irrelevant to launch. Filter those out by matching each resolved main class against the test source folders of the project's classpath. The Spring Tools classpath listener already reports `isTest` for each source entry (set from `IClasspathEntry.isTest()`), so no extra language server round trip is needed; the flag was just missing from the `CPE` type declaration. The unfiltered list stays available via `getMainClasses()` so that live processes launched from a test main class are still associated with their app. When every main class sits in a test source folder, the filter falls back to the full list to keep such a project launchable. Closes microsoft#420
|
@microsoft-github-policy-service agree company="Broadcom" |
Changyong Gong (chagong)
left a comment
There was a problem hiding this comment.
The ordinary Maven/Gradle case is covered, but linked test source folders can still surface as launch candidates because the two path producers use logical and physical resource locations. I added concrete suggestions that preserve the current fast path and use JDT LS classification as a linked-resource-safe fallback. The remaining implementation looks focused; GitHub checks, local compile, lint, and patch hygiene passed.
| export function excludeTestMainClasses(mainClasses: MainClassData[], classpath: ClassPathData): MainClassData[] { | ||
| const testSourceFolders = (classpath?.entries ?? []) | ||
| .filter(cpe => cpe.kind === "source" && cpe.isTest) | ||
| .map(cpe => cpe.path); | ||
| if (testSourceFolders.length === 0) { | ||
| return mainClasses; | ||
| } | ||
| // Keep entries without a file path: they cannot be located, so they cannot be ruled out. | ||
| return mainClasses.filter(mc => !mc.filePath || !testSourceFolders.some(folder => isInFolder(mc.filePath, folder))); | ||
| } |
There was a problem hiding this comment.
Linked test source folders are not filtered here because CPE.path is built from the logical project path while MainClassData.filePath comes from IFile.getLocation(), which resolves to the physical target. Keep the current containment check as the fast path, then let JDT LS resolve unmatched physical URIs back to their logical compilation units; a classification failure should retain the candidate so Run/Debug remains available.
| export function excludeTestMainClasses(mainClasses: MainClassData[], classpath: ClassPathData): MainClassData[] { | |
| const testSourceFolders = (classpath?.entries ?? []) | |
| .filter(cpe => cpe.kind === "source" && cpe.isTest) | |
| .map(cpe => cpe.path); | |
| if (testSourceFolders.length === 0) { | |
| return mainClasses; | |
| } | |
| // Keep entries without a file path: they cannot be located, so they cannot be ruled out. | |
| return mainClasses.filter(mc => !mc.filePath || !testSourceFolders.some(folder => isInFolder(mc.filePath, folder))); | |
| } | |
| export async function excludeTestMainClasses(mainClasses: MainClassData[], classpath: ClassPathData): Promise<MainClassData[]> { | |
| const testSourceFolders = (classpath?.entries ?? []) | |
| .filter(cpe => cpe.kind === "source" && cpe.isTest) | |
| .map(cpe => cpe.path); | |
| if (testSourceFolders.length === 0) { | |
| return mainClasses; | |
| } | |
| const testMainClassFlags = await Promise.all(mainClasses.map(async mainClass => { | |
| if (!mainClass.filePath) { | |
| return false; | |
| } | |
| if (testSourceFolders.some(folder => isInFolder(mainClass.filePath, folder))) { | |
| return true; | |
| } | |
| try { | |
| return await vscode.commands.executeCommand<boolean>( | |
| "java.execute.workspaceCommand", | |
| "java.project.isTestFile", | |
| vscode.Uri.file(mainClass.filePath).toString() | |
| ) === true; | |
| } catch { | |
| return false; | |
| } | |
| })); | |
| return mainClasses.filter((_, index) => !testMainClassFlags[index]); | |
| } |
Please also cover a linked test source whose physical path is outside CPE.path, plus the command-rejection fallback.
| * that a project whose only main class lives there stays launchable. | ||
| */ | ||
| public async getLaunchableMainClasses(): Promise<MainClassData[]> { | ||
| const mainClasses = await this.getMainClasses(); |
There was a problem hiding this comment.
With the classifier returning a promise, await it before applying the existing all-test fallback.
| const mainClasses = await this.getMainClasses(); | |
| const launchable = await excludeTestMainClasses(mainClasses, this.classpath); |
vscode.java.resolveMainClasssearches every source folder of a project, so a@SpringBootApplicationclass copied intosrc/test/java— a common pattern for integration tests — was offered as an additional launch candidate. Running an app then popped up a quick pick asking the user to choose between the real main class and one that is irrelevant to launch.Filter those out by matching each resolved main class against the test source folders of the project's classpath. The Spring Tools classpath listener already reports
isTestfor each source entry (set fromIClasspathEntry.isTest()), so no extra language server round trip is needed; the flag was just missing from theCPEtype declaration.The unfiltered list stays available via
getMainClasses()so that live processes launched from a test main class are still associated with their app. When every main class sits in a test source folder, the filter falls back to the full list to keep such a project launchable.Closes #420