From 0d526be1c39b2e666075b75f70dff8a57efee9e4 Mon Sep 17 00:00:00 2001 From: arookieofc <2128194521@qq.com> Date: Fri, 6 Nov 2026 19:41:17 +0800 Subject: [PATCH] fix(run): run Gradle module tests from the build root, not the module dir "All Tests" on a Gradle module moved cwd to the module directory while the command stayed './gradlew', so the wrapper could not be found. Keep the run in the build root (where the wrapper resolves) and qualify the task with the module's Gradle path (:services:api:test), using the build model's real project path when loaded. --- src/features/run/producers.test.ts | 9 ++++++++ src/features/run/producers.ts | 37 +++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/features/run/producers.test.ts b/src/features/run/producers.test.ts index 6a9a42b2..f7c45827 100644 --- a/src/features/run/producers.test.ts +++ b/src/features/run/producers.test.ts @@ -813,6 +813,15 @@ describe("project producers", () => { expect( produceDirectory({ path: `${ROOT}/src/test/java/com/acme`, root: ROOT, configs: gradle })!.config!.args, ).toEqual(["test", "--tests", "com.acme.*"]); + // A module's tests run from the build root so `./gradlew` resolves, + // qualified by the module's Gradle path. + const dir = produceDirectory({ path: `${ROOT}/services/api/src/test/java`, root: ROOT, configs: gradle })!; + expect(dir.config).toMatchObject({ cwd: null }); + expect(dir.config!.args).toEqual([":services:api:test"]); + expect( + produceDirectory({ path: `${ROOT}/services/api/src/test/java/com/acme`, root: ROOT, configs: gradle })!.config! + .args, + ).toEqual([":services:api:test", "--tests", "com.acme.*"]); expect(produceDirectory({ path: `${ROOT}/src/main`, root: ROOT, configs })).toBeNull(); expect(produceDirectory({ path: ROOT, root: ROOT, configs })).toBeNull(); }); diff --git a/src/features/run/producers.ts b/src/features/run/producers.ts index a0a90bf6..df973f22 100644 --- a/src/features/run/producers.ts +++ b/src/features/run/producers.ts @@ -14,6 +14,7 @@ import { ipcPath, isSameOrInside } from "@/platform/paths"; import { isTestPath } from "@/platform/languageSettings"; import { currentRoots } from "@/platform/projectRoots"; import { applicationConfig, jvmModuleDir } from "./application"; +import { useBuildModel } from "@/platform/buildModel"; import { formatArgs } from "./configs"; import { goGenerateConfig } from "./goGenerate"; import { goTestParts } from "./goArguments"; @@ -1146,6 +1147,27 @@ function rubyDirectory( return make(minitest, label, [...args, relToCwd(minitest)]); } +/** + * The Gradle build and project path a module directory belongs to, so a module + * test run can stay in the build root (where `./gradlew` resolves) and qualify + * its task (`:services:api:test`). `null` for non-Gradle bases and modules the + * base configuration already runs. Without a loaded model, the Gradle path is + * guessed from the directory's segments — right for the standard layout the + * wrapper itself implies. + */ +function gradleOwnerFor(module: string, base: RunConfig): { dir: string | null; path: string } | null { + if (base.kind !== "gradle" || !module || module === base.cwd) return null; + const dir = module.replace(/\\/g, "/"); + const gradle = useBuildModel + .getState() + .model?.gradle?.find((g) => g.projects.some((p) => p.dir.replace(/\\/g, "/") === dir)); + const project = gradle?.projects.find((p) => p.dir.replace(/\\/g, "/") === dir); + return { + dir: gradle?.dir ?? base.cwd, + path: project?.path ?? `:${module.split("/").join(":")}`, + }; +} + export function produceDirectory(input: ContextInput): Produced | null { input = scopedInput(input); const { root } = input; @@ -1181,11 +1203,20 @@ export function produceDirectory(input: ContextInput): Produced | null { const base = closest(["gradle", "maven"]); if (base) { const module = jvmModuleDir(rel + "/x.java", input.configs); - const moduleBase = { ...base, cwd: module || base.cwd }; - if (!jvmPackage) return make(moduleBase, "All Tests", [...base.args]); + // Gradle in a multi-module project: run from the build root so the + // wrapper (`./gradlew`) resolves, qualifying the task with the module's + // Gradle path (`services/api` → `:services:api:test`). Other builds run + // in the module directory as before. + const owner = gradleOwnerFor(module, base); + const moduleBase = { ...base, cwd: owner ? owner.dir : module || base.cwd }; + const moduleArgs = (base: RunConfig): string[] => { + if (!owner) return [...base.args]; + return base.args.map((a) => (a === "test" ? `${owner.path}:test` : a)); + }; + if (!jvmPackage) return make(moduleBase, "All Tests", moduleArgs(base)); const pkg = jvmPackage[2]!.replace(/\//g, "."); return base.kind === "gradle" - ? make(moduleBase, `Tests in '${pkg}'`, [...base.args, "--tests", `${pkg}.*`]) + ? make(moduleBase, `Tests in '${pkg}'`, [...moduleArgs(base), "--tests", `${pkg}.*`]) : make(moduleBase, `Tests in '${pkg}'`, [ ...base.args, `-Dtest=${pkg}.**.*`,