Skip to content

Commit 174fa34

Browse files
committed
gradle multi-module unit tests and discovery
1 parent 416c5af commit 174fa34

4 files changed

Lines changed: 415 additions & 23 deletions

File tree

src/liberty/projectDiscovery.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,9 @@ async function discoverProjects(
177177
const gradleChildBuildPaths = new Set<string>();
178178
const gradleParentPaths = new Set<string>();
179179
for (const entry of gradleEntries) {
180-
if ((!entry.parsedBuild && !entry.regexBuildFile) || !entry.parsedSettings) { continue; }
180+
const hasParsedBuild = entry.parsedBuild || entry.regexBuildFile;
181+
const isAggregatorBySettings = entry.parsedSettings && gradleUtil.hasGradleSubprojects(entry.parsedSettings);
182+
if (!hasParsedBuild && !isAggregatorBySettings) { continue; }
181183
const gradleBuildFile = gradleUtil.findChildGradleProjects(
182184
entry.parsedBuild ?? {}, entry.parsedSettings, entry.path
183185
);
@@ -230,7 +232,7 @@ async function discoverProjects(
230232
}),
231233

232234
...gradleEntries.map(async (entry) => {
233-
if (!entry.parsedBuild && !entry.regexBuildFile) { return; }
235+
if (!entry.parsedBuild && !entry.regexBuildFile && !gradleParentPaths.has(entry.path)) { return; }
234236
let buildFile: BuildFileImpl;
235237

236238
if (gradleParentPaths.has(entry.path)) {
@@ -304,7 +306,7 @@ async function stampProjects(
304306
project.isAggregator = metadata.isAggregator;
305307
project.isLibertyEnabled = metadata.isLibertyEnabled;
306308
mavenMetadataMap.set(entry.path, metadata);
307-
} else if (entry.type === "gradle" && (entry.parsedBuild || entry.regexBuildFile)) {
309+
} else if (entry.type === "gradle" && (entry.parsedBuild || entry.regexBuildFile || entry.parsedSettings)) {
308310
const metadata = await gradleUtil.extractGradleMetadata(entry.path, entry.parsedBuild ?? null, entry.parsedSettings);
309311
console.log(`[stamp] gradle ${entry.path}: projectName=${metadata.projectName}, parentProjectName=${metadata.parentProjectName}, isAggregator=${metadata.isAggregator}, isLibertyEnabled=${metadata.isLibertyEnabled}`);
310312
project.artifactId = metadata.projectName;
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/*
2+
* Unit tests for Gradle multi-module discovery.
3+
* Plain mocha + chai, no VS Code instance required.
4+
*
5+
* Gradle multi-module relationship is one-directional:
6+
* - Root declares subprojects via settings.gradle include()
7+
* - Children do NOT declare a parent reference
8+
* - No two-pass strategy needed — settings.gradle is single source of truth
9+
*
10+
* Variables:
11+
* RL = Root has Liberty plugin in build.gradle
12+
* CL = Child has Liberty plugin in build.gradle
13+
* SI = settings.gradle includes the child
14+
*
15+
* Group | RL | CL | SI | Expected
16+
* -------|----|----|----|-----------------------------------------
17+
* 1 | N | Y | Y | Root display-only aggregator, child actionable
18+
* 2 | Y | Y | Y | Root aggregator, child actionable
19+
* 3 | N | N | Y | Both excluded (no Liberty anywhere)
20+
* 4 | N | Y | N | Child standalone, root excluded
21+
*
22+
* Reference test project: ~/test/multi-module-test-projects/bob-nested-gradle-ears
23+
* - Root build.gradle has no Liberty plugin
24+
* - app-one-ear/build.gradle and app-two-ear/build.gradle declare Liberty directly
25+
* - settings.gradle includes all subprojects via colon-separated paths
26+
*/
27+
import { strict as assert } from "assert";
28+
import { detectLibertyPluginFromText, findChildGradleProjects, hasGradleSubprojects } from "../../util/gradleUtil";
29+
import { LIBERTY_PROJECT_GRADLE } from "../../definitions/constants";
30+
31+
// ── Fixture builders ──────────────────────────────────────────────────────────
32+
33+
function makeRootBuildGradle(hasLiberty: boolean): string {
34+
return hasLiberty
35+
? `plugins {
36+
id("io.openliberty.tools.gradle.Liberty") version "3.0"
37+
}`
38+
: `plugins {
39+
id 'base'
40+
}`;
41+
}
42+
43+
function makeChildBuildGradle(hasLiberty: boolean): string {
44+
return hasLiberty
45+
? `plugins {
46+
id 'java'
47+
id("io.openliberty.tools.gradle.Liberty") version "3.0"
48+
}`
49+
: `plugins {
50+
id 'java'
51+
}`;
52+
}
53+
54+
function makeSettingsGradle(includeChild: boolean): any {
55+
// Simulate parsed settings.gradle as g2js would return
56+
return includeChild
57+
? { "rootProject.name": "my-root", include: ["child-module"] }
58+
: { "rootProject.name": "my-root" };
59+
}
60+
61+
// ── Group 1: Root no Liberty, child has Liberty, settings includes child ───────
62+
// Root display-only aggregator, child actionable
63+
64+
describe("gradle group 1 — root no Liberty, child has Liberty, included in settings", () => {
65+
const rootContent = makeRootBuildGradle(false);
66+
const childContent = makeChildBuildGradle(true);
67+
const parsedSettings = makeSettingsGradle(true);
68+
69+
it("root is not detected as Liberty project via text", () => {
70+
const result = detectLibertyPluginFromText(rootContent);
71+
assert.equal(result, null);
72+
});
73+
74+
it("root is detected as aggregator via settings.gradle include", () => {
75+
const result = findChildGradleProjects({}, parsedSettings, "/workspace/build.gradle");
76+
assert.ok(result.getChildren().length > 0);
77+
});
78+
79+
it("child is detected as Liberty project via text", () => {
80+
const result = detectLibertyPluginFromText(childContent);
81+
assert.notEqual(result, null);
82+
assert.equal(result!.getProjectType(), LIBERTY_PROJECT_GRADLE);
83+
});
84+
});
85+
86+
// ── Group 2: Both root and child have Liberty, settings includes child ─────────
87+
// Root aggregator, child actionable
88+
89+
describe("gradle group 2 — both have Liberty, included in settings", () => {
90+
const rootContent = makeRootBuildGradle(true);
91+
const childContent = makeChildBuildGradle(true);
92+
const parsedSettings = makeSettingsGradle(true);
93+
94+
it("root is detected as Liberty project via text", () => {
95+
const result = detectLibertyPluginFromText(rootContent);
96+
assert.notEqual(result, null);
97+
assert.equal(result!.getProjectType(), LIBERTY_PROJECT_GRADLE);
98+
});
99+
100+
it("root is also detected as aggregator via settings.gradle include", () => {
101+
const result = findChildGradleProjects({}, parsedSettings, "/workspace/build.gradle");
102+
assert.ok(result.getChildren().length > 0);
103+
});
104+
105+
it("child is detected as Liberty project via text", () => {
106+
const result = detectLibertyPluginFromText(childContent);
107+
assert.notEqual(result, null);
108+
assert.equal(result!.getProjectType(), LIBERTY_PROJECT_GRADLE);
109+
});
110+
});
111+
112+
// ── Group 3: Neither has Liberty, settings includes child ─────────────────────
113+
// Both excluded — no Liberty anywhere
114+
115+
describe("gradle group 3 — no Liberty anywhere, included in settings", () => {
116+
const rootContent = makeRootBuildGradle(false);
117+
const childContent = makeChildBuildGradle(false);
118+
const parsedSettings = makeSettingsGradle(true);
119+
120+
it("root not detected as Liberty project", () => {
121+
assert.equal(detectLibertyPluginFromText(rootContent), null);
122+
});
123+
124+
it("child not detected as Liberty project", () => {
125+
assert.equal(detectLibertyPluginFromText(childContent), null);
126+
});
127+
128+
it("root still detected as aggregator structurally — excluded later by hasLibertyDescendants", () => {
129+
const result = findChildGradleProjects({}, parsedSettings, "/workspace/build.gradle");
130+
assert.ok(result.getChildren().length > 0);
131+
});
132+
});
133+
134+
// ── Group 4: Child has Liberty, not included in settings ──────────────────────
135+
// Child standalone, root excluded
136+
137+
describe("gradle group 4 — child has Liberty, not in settings", () => {
138+
const rootContent = makeRootBuildGradle(false);
139+
const childContent = makeChildBuildGradle(true);
140+
const parsedSettings = makeSettingsGradle(false);
141+
142+
it("root not detected as Liberty project", () => {
143+
assert.equal(detectLibertyPluginFromText(rootContent), null);
144+
});
145+
146+
it("root has no children in settings — not an aggregator", () => {
147+
const result = findChildGradleProjects({}, parsedSettings, "/workspace/build.gradle");
148+
assert.equal(result.getChildren().length, 0);
149+
});
150+
151+
it("child is detected as standalone Liberty project", () => {
152+
const result = detectLibertyPluginFromText(childContent);
153+
assert.notEqual(result, null);
154+
assert.equal(result!.getProjectType(), LIBERTY_PROJECT_GRADLE);
155+
});
156+
});
157+
158+
// ── Colon-path resolution ─────────────────────────────────────────────────────
159+
// settings.gradle include 'application-one:app-one-ear' style paths
160+
161+
describe("gradle colon-path subproject inclusion", () => {
162+
const parsedSettings = {
163+
"rootProject.name": "bob-nested-gradle-ears",
164+
include: [
165+
"application-one:app-one-ear",
166+
"application-one:app-one-ejb",
167+
"application-two:app-two-ear",
168+
],
169+
};
170+
171+
it("findChildGradleProjects returns all colon-path children", () => {
172+
const result = findChildGradleProjects({}, parsedSettings, "/workspace/build.gradle");
173+
const children = result.getChildren();
174+
assert.ok(children.includes("application-one:app-one-ear"));
175+
assert.ok(children.includes("application-one:app-one-ejb"));
176+
assert.ok(children.includes("application-two:app-two-ear"));
177+
});
178+
179+
it("colon paths cover all three ear subprojects from bob-nested-gradle-ears", () => {
180+
const result = findChildGradleProjects({}, parsedSettings, "/workspace/build.gradle");
181+
assert.equal(result.getChildren().length, 3);
182+
});
183+
});
184+
185+
// ── hasGradleSubprojects — phase 2 guard fix ──────────────────────────────────
186+
// The bug: phase 2 skips no-Liberty roots because parsedBuild/regexBuildFile
187+
// are null. hasGradleSubprojects() lets discovery check settings.gradle
188+
// independently of whether the root has Liberty, so findChildGradleProjects
189+
// is called for no-Liberty aggregators like bob-nested-gradle-ears.
190+
191+
describe("hasGradleSubprojects — detects aggregator from settings alone", () => {
192+
it("returns true when parsedSettings has include entries", () => {
193+
const parsedSettings = { "rootProject.name": "my-root", include: ["app-one-ear", "app-two-ear"] };
194+
assert.equal(hasGradleSubprojects(parsedSettings), true);
195+
});
196+
197+
it("returns false when parsedSettings has no include entries", () => {
198+
const parsedSettings = { "rootProject.name": "my-root" };
199+
assert.equal(hasGradleSubprojects(parsedSettings), false);
200+
});
201+
202+
it("returns false when parsedSettings is null", () => {
203+
assert.equal(hasGradleSubprojects(null), false);
204+
});
205+
206+
it("returns false when parsedSettings is undefined", () => {
207+
assert.equal(hasGradleSubprojects(undefined), false);
208+
});
209+
210+
it("returns true for colon-path includes (bob-nested-gradle-ears pattern)", () => {
211+
const parsedSettings = {
212+
"rootProject.name": "bob-nested-gradle-ears",
213+
include: [
214+
"application-one:app-one-ear",
215+
"application-one:app-one-ejb",
216+
"application-two:app-two-ear",
217+
],
218+
};
219+
assert.equal(hasGradleSubprojects(parsedSettings), true);
220+
});
221+
222+
it("no-Liberty root + subprojects → findChildGradleProjects returns valid aggregator", () => {
223+
const parsedSettings = {
224+
"rootProject.name": "bob-nested-gradle-ears",
225+
include: ["application-one:app-one-ear", "application-two:app-two-ear"],
226+
};
227+
// Simulate what phase 2 should do: check hasGradleSubprojects first,
228+
// then call findChildGradleProjects with empty parsedBuild for no-Liberty root
229+
const isAggregator = hasGradleSubprojects(parsedSettings);
230+
assert.equal(isAggregator, true);
231+
232+
const result = findChildGradleProjects({}, parsedSettings, "/workspace/build.gradle");
233+
assert.equal(result.isValidBuildFile(), true);
234+
assert.ok(result.getChildren().includes("application-one:app-one-ear"));
235+
assert.ok(result.getChildren().includes("application-two:app-two-ear"));
236+
});
237+
});
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Unit tests for extractGradleMetadata — the metadata stamping layer.
3+
* Tests cover the fields that downstream pipeline stages depend on:
4+
* isAggregator, isLibertyEnabled, contextValue, projectName, parentProjectName
5+
*
6+
* All tests use parsed content directly (no filesystem reads beyond test fixtures).
7+
*/
8+
import { strict as assert } from "assert";
9+
import { extractGradleMetadata } from "../../util/gradleUtil";
10+
import { LIBERTY_PROJECT_GRADLE } from "../../definitions/constants";
11+
12+
// ── Fixture parsed content ────────────────────────────────────────────────────
13+
14+
const NO_LIBERTY_BUILD: any = {};
15+
const LIBERTY_BUILD: any = {
16+
plugins: [{ id: "io.openliberty.tools.gradle.Liberty", version: "3.0" }]
17+
};
18+
const SETTINGS_WITH_SUBPROJECTS: any = {
19+
"rootProject.name": "my-root",
20+
include: ["app-one-ear", "app-two-ear"]
21+
};
22+
const SETTINGS_NO_SUBPROJECTS: any = {
23+
"rootProject.name": "my-root"
24+
};
25+
26+
// Real project paths for parentProjectName resolution (requires filesystem)
27+
const REAL_ROOT = "/Users/dshi/test/multi-module-test-projects/bob-nested-gradle-ears";
28+
const REAL_CHILD = `${REAL_ROOT}/application-one/app-one-ear/build.gradle`;
29+
30+
// ── Aggregator with no Liberty (bob-nested-gradle-ears root pattern) ──────────
31+
32+
describe("extractGradleMetadata — no-Liberty aggregator root", () => {
33+
it("isAggregator is true when settings has subprojects", async () => {
34+
const metadata = await extractGradleMetadata("/workspace/build.gradle", NO_LIBERTY_BUILD, SETTINGS_WITH_SUBPROJECTS);
35+
assert.equal(metadata.isAggregator, true);
36+
});
37+
38+
it("contextValue is LIBERTY_PROJECT_GRADLE (not empty) for aggregator with no Liberty", async () => {
39+
const metadata = await extractGradleMetadata("/workspace/build.gradle", NO_LIBERTY_BUILD, SETTINGS_WITH_SUBPROJECTS);
40+
assert.equal(metadata.contextValue, LIBERTY_PROJECT_GRADLE);
41+
});
42+
43+
it("isLibertyEnabled is false — aggregator has no Liberty plugin itself", async () => {
44+
const metadata = await extractGradleMetadata("/workspace/build.gradle", NO_LIBERTY_BUILD, SETTINGS_WITH_SUBPROJECTS);
45+
assert.equal(metadata.isLibertyEnabled, false);
46+
});
47+
48+
it("projectName is extracted from settings rootProject.name", async () => {
49+
const metadata = await extractGradleMetadata("/workspace/build.gradle", NO_LIBERTY_BUILD, SETTINGS_WITH_SUBPROJECTS);
50+
assert.equal(metadata.projectName, "my-root");
51+
});
52+
53+
it("subprojects are populated from settings include", async () => {
54+
const metadata = await extractGradleMetadata("/workspace/build.gradle", NO_LIBERTY_BUILD, SETTINGS_WITH_SUBPROJECTS);
55+
assert.ok(metadata.subprojects.includes("app-one-ear"));
56+
assert.ok(metadata.subprojects.includes("app-two-ear"));
57+
});
58+
});
59+
60+
// ── Liberty-enabled aggregator root ───────────────────────────────────────────
61+
62+
describe("extractGradleMetadata — Liberty aggregator root", () => {
63+
it("isAggregator is true", async () => {
64+
const metadata = await extractGradleMetadata("/workspace/build.gradle", LIBERTY_BUILD, SETTINGS_WITH_SUBPROJECTS);
65+
assert.equal(metadata.isAggregator, true);
66+
});
67+
68+
it("isLibertyEnabled is true", async () => {
69+
const metadata = await extractGradleMetadata("/workspace/build.gradle", LIBERTY_BUILD, SETTINGS_WITH_SUBPROJECTS);
70+
assert.equal(metadata.isLibertyEnabled, true);
71+
});
72+
73+
it("contextValue is LIBERTY_PROJECT_GRADLE", async () => {
74+
const metadata = await extractGradleMetadata("/workspace/build.gradle", LIBERTY_BUILD, SETTINGS_WITH_SUBPROJECTS);
75+
assert.equal(metadata.contextValue, LIBERTY_PROJECT_GRADLE);
76+
});
77+
});
78+
79+
// ── Standalone Liberty child (no subprojects) ─────────────────────────────────
80+
81+
describe("extractGradleMetadata — Liberty child, no subprojects", () => {
82+
it("isAggregator is false", async () => {
83+
const metadata = await extractGradleMetadata("/workspace/app-one-ear/build.gradle", LIBERTY_BUILD, SETTINGS_NO_SUBPROJECTS);
84+
assert.equal(metadata.isAggregator, false);
85+
});
86+
87+
it("isLibertyEnabled is true", async () => {
88+
const metadata = await extractGradleMetadata("/workspace/app-one-ear/build.gradle", LIBERTY_BUILD, SETTINGS_NO_SUBPROJECTS);
89+
assert.equal(metadata.isLibertyEnabled, true);
90+
});
91+
92+
it("contextValue is LIBERTY_PROJECT_GRADLE", async () => {
93+
const metadata = await extractGradleMetadata("/workspace/app-one-ear/build.gradle", LIBERTY_BUILD, SETTINGS_NO_SUBPROJECTS);
94+
assert.equal(metadata.contextValue, LIBERTY_PROJECT_GRADLE);
95+
});
96+
});
97+
98+
// ── Non-Liberty, non-aggregator (shared-ejb pattern) ─────────────────────────
99+
100+
describe("extractGradleMetadata — no Liberty, no subprojects", () => {
101+
it("isAggregator is false", async () => {
102+
const metadata = await extractGradleMetadata("/workspace/shared-ejb/build.gradle", NO_LIBERTY_BUILD, SETTINGS_NO_SUBPROJECTS);
103+
assert.equal(metadata.isAggregator, false);
104+
});
105+
106+
it("isLibertyEnabled is false", async () => {
107+
const metadata = await extractGradleMetadata("/workspace/shared-ejb/build.gradle", NO_LIBERTY_BUILD, SETTINGS_NO_SUBPROJECTS);
108+
assert.equal(metadata.isLibertyEnabled, false);
109+
});
110+
111+
it("contextValue is empty — not a Liberty project", async () => {
112+
const metadata = await extractGradleMetadata("/workspace/shared-ejb/build.gradle", NO_LIBERTY_BUILD, SETTINGS_NO_SUBPROJECTS);
113+
assert.equal(metadata.contextValue, "");
114+
});
115+
});
116+
117+
// ── parentProjectName resolution (real filesystem) ────────────────────────────
118+
// Verifies that a child in bob-nested-gradle-ears resolves its parent name
119+
// by reading the parent directory's settings.gradle from disk.
120+
121+
describe("extractGradleMetadata — parentProjectName resolution", () => {
122+
it("child resolves parentProjectName from parent settings.gradle", async () => {
123+
const metadata = await extractGradleMetadata(REAL_CHILD, LIBERTY_BUILD, SETTINGS_NO_SUBPROJECTS);
124+
console.log(" parentProjectName:", metadata.parentProjectName);
125+
assert.equal(metadata.parentProjectName, "bob-nested-gradle-ears");
126+
});
127+
128+
it("root has no parentProjectName", async () => {
129+
const metadata = await extractGradleMetadata(`${REAL_ROOT}/build.gradle`, NO_LIBERTY_BUILD, SETTINGS_WITH_SUBPROJECTS);
130+
assert.equal(metadata.parentProjectName, undefined);
131+
});
132+
});

0 commit comments

Comments
 (0)