Skip to content

Commit 7736a13

Browse files
committed
test(project): Improve code coverage to meet configured thresholds
Add targeted tests to bring @ui5/project coverage above the configured thresholds (95% statements, 90% branches, 95% functions, 95% lines). Also fix AVA timeout for parallel test execution and remove dead code module utils/sanitizeFileName.js. New test files: - build/cache/utils.js, index/TreeNode.js, index/ResourceIndex.js - build/BuildReader.js, helpers/WatchHandler.js, helpers/ProjectBuilderOutputStyle.js - ui5framework/maven/CacheMode.js Extended existing tests: - HashTree.js, SharedHashTree.js, CacheManager.js - specifications/Project.js, types/Component.js, types/Library.js - types/Module.js, types/ThemeLibrary.js, extensions/Task.js
1 parent faa479c commit 7736a13

19 files changed

Lines changed: 1494 additions & 45 deletions

File tree

packages/project/ava.config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
import avaCommonConfig from "../../ava.common.config.js";
22

3-
export default avaCommonConfig;
3+
export default {
4+
...avaCommonConfig,
5+
timeout: "5m",
6+
};

packages/project/lib/utils/sanitizeFileName.js

Lines changed: 0 additions & 44 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// test resource
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import test from "ava";
2+
import sinon from "sinon";
3+
import BuildReader from "../../../lib/build/BuildReader.js";
4+
5+
function createMockProject(name, namespace, type = "library") {
6+
return {
7+
getName: () => name,
8+
getNamespace: () => namespace,
9+
getType: () => type,
10+
};
11+
}
12+
13+
test.afterEach.always(() => {
14+
sinon.restore();
15+
});
16+
17+
test("constructor: throws when multiple projects share a namespace", (t) => {
18+
const projects = [
19+
createMockProject("proj-a", "my/namespace"),
20+
createMockProject("proj-b", "my/namespace"),
21+
];
22+
t.throws(() => new BuildReader("test", projects, {}), {
23+
message: /Multiple projects with namespace 'my\/namespace' found/
24+
});
25+
});
26+
27+
test("byGlob: delegates to combined reader", async (t) => {
28+
const projects = [createMockProject("proj-a", "my/ns")];
29+
const mockReader = {byGlob: sinon.stub().resolves([{path: "/a.js"}])};
30+
const buildServerInterface = {
31+
getReaderForProjects: sinon.stub().resolves(mockReader),
32+
};
33+
const reader = new BuildReader("test", projects, buildServerInterface);
34+
const result = await reader.byGlob("**/*.js");
35+
t.deepEqual(result, [{path: "/a.js"}]);
36+
t.is(buildServerInterface.getReaderForProjects.firstCall.args[0][0], "proj-a");
37+
});
38+
39+
test("byPath: returns resource from primary reader", async (t) => {
40+
const projects = [createMockProject("proj-a", "my/ns")];
41+
const resource = {getPath: () => "/resources/my/ns/a.js"};
42+
const mockReader = {byPath: sinon.stub().resolves(resource)};
43+
const buildServerInterface = {
44+
getReaderForProject: sinon.stub().resolves(mockReader),
45+
getReaderForProjects: sinon.stub().resolves(mockReader),
46+
};
47+
const reader = new BuildReader("test", projects, buildServerInterface);
48+
const result = await reader.byPath("/resources/my/ns/a.js");
49+
t.is(result, resource);
50+
});
51+
52+
test("byPath: falls back to all projects when primary reader returns null", async (t) => {
53+
const projects = [createMockProject("proj-a", "my/ns")];
54+
const resource = {getPath: () => "/resources/my/ns/a.js"};
55+
const primaryReader = {byPath: sinon.stub().resolves(null)};
56+
const fallbackReader = {byPath: sinon.stub().resolves(resource)};
57+
const buildServerInterface = {
58+
getReaderForProject: sinon.stub().resolves(primaryReader),
59+
getReaderForProjects: sinon.stub().resolves(fallbackReader),
60+
};
61+
const reader = new BuildReader("test", projects, buildServerInterface);
62+
const result = await reader.byPath("/resources/my/ns/a.js");
63+
t.is(result, resource);
64+
});
65+
66+
test("_getReaderForResource: final fallback when path doesn't match any namespace", async (t) => {
67+
const projects = [
68+
createMockProject("proj-a", "ns/a"),
69+
createMockProject("proj-b", "ns/b"),
70+
];
71+
const mockReader = {byPath: sinon.stub().resolves(null)};
72+
const buildServerInterface = {
73+
getReaderForProjects: sinon.stub().resolves(mockReader),
74+
getCachedReadersForProjects: sinon.stub().returns(null),
75+
};
76+
const reader = new BuildReader("test", projects, buildServerInterface);
77+
const result = await reader.byPath("/resources/unknown/path.js");
78+
t.is(result, null);
79+
t.true(buildServerInterface.getReaderForProjects.called);
80+
});
81+
82+
test("_getReaderForResource: uses cached reader to identify project", async (t) => {
83+
const projects = [
84+
createMockProject("proj-a", "ns/a"),
85+
createMockProject("proj-b", "ns/b"),
86+
];
87+
const foundResource = {getProject: () => ({getName: () => "proj-a"})};
88+
const cachedReader = {byPath: sinon.stub().resolves(foundResource)};
89+
const projectReader = {byPath: sinon.stub().resolves(foundResource)};
90+
const buildServerInterface = {
91+
getReaderForProject: sinon.stub().resolves(projectReader),
92+
getReaderForProjects: sinon.stub().resolves(projectReader),
93+
getCachedReadersForProjects: sinon.stub().returns(cachedReader),
94+
};
95+
const reader = new BuildReader("test", projects, buildServerInterface);
96+
const result = await reader.byPath("/resources/other/path.js");
97+
t.is(result, foundResource);
98+
});
99+
100+
test("_getReaderForResource: application fallback for non-resource paths", async (t) => {
101+
const projects = [
102+
createMockProject("my-app", "my/app", "application"),
103+
createMockProject("my-lib", "my/lib"),
104+
];
105+
const foundResource = {getPath: () => "/index.html"};
106+
const appReader = {byPath: sinon.stub().resolves(foundResource)};
107+
const cachedReader = {byPath: sinon.stub().resolves(null)};
108+
const buildServerInterface = {
109+
getReaderForProject: sinon.stub().resolves(appReader),
110+
getReaderForProjects: sinon.stub().resolves(appReader),
111+
getCachedReadersForProjects: sinon.stub().returns(cachedReader),
112+
};
113+
const reader = new BuildReader("test", projects, buildServerInterface);
114+
const result = await reader.byPath("/index.html");
115+
t.is(result, foundResource);
116+
});

packages/project/test/lib/build/cache/CacheManager.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,100 @@ test.serial("create() returns singleton per cache directory", async (t) => {
136136
const cm2 = await CacheManager.create(testDir);
137137
t.is(cm1, cm2, "Same cache directory returns same instance");
138138
});
139+
140+
test.serial("readContentRaw returns stored buffer", async (t) => {
141+
const testDir = getUniqueTestDir();
142+
const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default;
143+
const cm = new CacheManager(path.join(testDir, "buildCache"));
144+
145+
const content = Buffer.from("test content for raw read");
146+
cm.putContent("sha256-raw", content);
147+
const raw = cm.readContentRaw("sha256-raw");
148+
t.truthy(raw, "Returns a value");
149+
cm.close();
150+
});
151+
152+
test.serial("putCompressedContent stores pre-compressed data that readContent decompresses", async (t) => {
153+
const testDir = getUniqueTestDir();
154+
const {gzipSync} = await import("node:zlib");
155+
const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default;
156+
const cm = new CacheManager(path.join(testDir, "buildCache"));
157+
158+
const content = Buffer.from("compressed content");
159+
const compressed = gzipSync(content);
160+
cm.putCompressedContent("sha256-comp", compressed);
161+
const decompressed = cm.readContent("sha256-comp");
162+
t.deepEqual(decompressed, content, "readContent returns decompressed content");
163+
cm.close();
164+
});
165+
166+
test.serial("writeStageResource writes resource by integrity", async (t) => {
167+
const testDir = getUniqueTestDir();
168+
const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default;
169+
const cm = new CacheManager(path.join(testDir, "buildCache"));
170+
171+
const resource = {
172+
getIntegrity: async () => "sha256-stage",
173+
getBuffer: async () => Buffer.from("stage resource content")
174+
};
175+
await cm.writeStageResource(resource);
176+
t.true(cm.hasContent("sha256-stage"));
177+
t.deepEqual(cm.readContent("sha256-stage"), Buffer.from("stage resource content"));
178+
cm.close();
179+
});
180+
181+
test.serial("Batch operations: content batch begin/end", async (t) => {
182+
const testDir = getUniqueTestDir();
183+
const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default;
184+
const cm = new CacheManager(path.join(testDir, "buildCache"));
185+
186+
cm.beginContentBatch();
187+
cm.putContent("sha256-batch1", Buffer.from("batch1"));
188+
cm.putContent("sha256-batch2", Buffer.from("batch2"));
189+
cm.endContentBatch();
190+
191+
t.true(cm.hasContent("sha256-batch1"));
192+
t.true(cm.hasContent("sha256-batch2"));
193+
cm.close();
194+
});
195+
196+
test.serial("Batch operations: content batch rollback", async (t) => {
197+
const testDir = getUniqueTestDir();
198+
const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default;
199+
const cm = new CacheManager(path.join(testDir, "buildCache"));
200+
201+
cm.beginContentBatch();
202+
cm.putContent("sha256-rollback", Buffer.from("rollback"));
203+
cm.rollbackContentBatch();
204+
205+
t.false(cm.hasContent("sha256-rollback"), "Content should not exist after rollback");
206+
cm.close();
207+
});
208+
209+
test.serial("Batch operations: metadata batch begin/end", async (t) => {
210+
const testDir = getUniqueTestDir();
211+
const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default;
212+
const cm = new CacheManager(path.join(testDir, "buildCache"));
213+
214+
cm.beginMetadataBatch();
215+
await cm.writeIndexCache("proj-batch", "sig", "source", {data: true});
216+
cm.endMetadataBatch();
217+
218+
const result = await cm.readIndexCache("proj-batch", "sig", "source");
219+
t.deepEqual(result, {data: true});
220+
cm.close();
221+
});
222+
223+
test.serial("Batch operations: metadata batch rollback", async (t) => {
224+
const testDir = getUniqueTestDir();
225+
const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default;
226+
const cm = new CacheManager(path.join(testDir, "buildCache"));
227+
228+
cm.beginMetadataBatch();
229+
await cm.writeIndexCache("proj-rollback", "sig", "source", {data: true});
230+
cm.rollbackMetadataBatch();
231+
232+
const result = await cm.readIndexCache("proj-rollback", "sig", "source");
233+
t.is(result, null, "Metadata should not exist after rollback");
234+
cm.close();
235+
});

packages/project/test/lib/build/cache/index/HashTree.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,3 +756,65 @@ test("Intermediate directory hashes are correct after deep leaf modification", a
756756
t.is(tree.getRootHash(), referenceTree.getRootHash(),
757757
"Root hash must match reference");
758758
});
759+
760+
test("hasDirectoryChanged: returns true when directory hash differs", (t) => {
761+
const resources = [
762+
{path: "dir/file1.js", integrity: "hash1"},
763+
{path: "dir/file2.js", integrity: "hash2"}
764+
];
765+
const tree = new HashTree(resources);
766+
const previousHash = "some-old-hash";
767+
t.true(tree.hasDirectoryChanged("dir", previousHash));
768+
});
769+
770+
test("hasDirectoryChanged: returns false when directory hash matches", (t) => {
771+
const resources = [
772+
{path: "dir/file1.js", integrity: "hash1"}
773+
];
774+
const tree = new HashTree(resources);
775+
const currentHash = tree.getDirectoryHash("dir");
776+
t.false(tree.hasDirectoryChanged("dir", currentHash));
777+
});
778+
779+
test("getStats: returns resource/directory counts and depth", (t) => {
780+
const resources = [
781+
{path: "a.js", integrity: "hash-a"},
782+
{path: "dir/b.js", integrity: "hash-b"},
783+
{path: "dir/sub/c.js", integrity: "hash-c"}
784+
];
785+
const tree = new HashTree(resources);
786+
const stats = tree.getStats();
787+
t.is(stats.resources, 3);
788+
t.is(stats.directories, 3); // root, dir, dir/sub
789+
t.is(stats.maxDepth, 3); // c.js is at depth 3 (root=0, dir=1, sub=2, c.js=3)
790+
t.is(stats.rootHash, tree.getRootHash());
791+
});
792+
793+
test("validate: returns true for valid tree", (t) => {
794+
const resources = [
795+
{path: "file.js", integrity: "hash1"},
796+
{path: "dir/file2.js", integrity: "hash2"}
797+
];
798+
const tree = new HashTree(resources);
799+
t.true(tree.validate());
800+
});
801+
802+
test("clone: creates independent copy", async (t) => {
803+
const resources = [
804+
{path: "file1.js", integrity: "hash1", lastModified: 1000, size: 100},
805+
{path: "dir/file2.js", integrity: "hash2", lastModified: 2000, size: 200}
806+
];
807+
const tree = new HashTree(resources);
808+
const cloned = tree.clone();
809+
810+
t.is(cloned.getRootHash(), tree.getRootHash(), "Clone has same root hash");
811+
812+
// Mutating clone should not affect original
813+
const indexTimestamp = cloned.getIndexTimestamp();
814+
await cloned.upsertResources([createMockResource("file1.js", "new-hash", indexTimestamp + 1, 101, 1)]);
815+
t.not(cloned.getRootHash(), tree.getRootHash(), "Original unchanged after clone mutation");
816+
});
817+
818+
test("fromCache: throws on unsupported version", (t) => {
819+
t.throws(() => HashTree.fromCache({version: 99, root: {}}), {message: /Unsupported version/});
820+
});

0 commit comments

Comments
 (0)