Skip to content

Commit 9197670

Browse files
committed
test(project): Add tests for cross project source file modifications
1 parent c9fdd9b commit 9197670

6 files changed

Lines changed: 168 additions & 0 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
const {readFile, writeFile} = require("fs/promises");
2+
const path = require("path");
3+
4+
/**
5+
* Custom task that verifies the frozen CAS reader protects against
6+
* cross-project dependency source race conditions.
7+
*
8+
* Uses data.json — an untransformed source file (no placeholders, not processed by
9+
* minify/replaceCopyright/replaceVersion). This is critical because transformed files
10+
* are written to stage writers which have higher priority than both the frozen reader
11+
* and the filesystem reader, making disk modifications invisible regardless.
12+
*
13+
* Flow:
14+
* 1. Read library.d's data.json via the dependency reader (CAS-backed)
15+
* 2. Overwrite the file on disk with different content
16+
* 3. Re-read via the dependency reader
17+
* 4. Assert the content is still the original (CAS-served)
18+
* 5. Restore the original file on disk
19+
*/
20+
module.exports = async function ({taskUtil}) {
21+
const libDProject = taskUtil.getProject("library.d");
22+
const libDReader = libDProject.getReader();
23+
24+
// Step 1: Read the original content via the dependency reader (CAS-backed)
25+
// data.json is untransformed (no placeholders, not a .js file subject to minification)
26+
// so it is only served by the frozen CAS reader or the filesystem reader
27+
const resourcePath = "/resources/library/d/data.json";
28+
const originalResource = await libDReader.byPath(resourcePath);
29+
if (!originalResource) {
30+
throw new Error(`Resource ${resourcePath} not found via dependency reader`);
31+
}
32+
const originalContent = await originalResource.getString();
33+
34+
// Step 2: Overwrite the file on disk
35+
const sourcePath = libDProject.getSourcePath();
36+
const diskFilePath = path.join(sourcePath, "library", "d", "data.json");
37+
const diskOriginalContent = await readFile(diskFilePath, {encoding: "utf8"});
38+
await writeFile(diskFilePath, JSON.stringify({key: "modified-by-race-condition"}));
39+
40+
try {
41+
// Step 3: Re-read via the dependency reader — should still return CAS-frozen content
42+
// Without the frozen reader, this would read the modified disk content
43+
const reReadResource = await libDReader.byPath(resourcePath);
44+
if (!reReadResource) {
45+
throw new Error(`Resource ${resourcePath} not found on re-read via dependency reader`);
46+
}
47+
const reReadContent = await reReadResource.getString();
48+
49+
// Step 4: Assert the content is still the original (not modified disk content)
50+
if (reReadContent !== originalContent) {
51+
throw new Error(
52+
"Frozen source reader protection failed: dependency reader returned modified disk content " +
53+
"instead of the original CAS-frozen content. " +
54+
`Expected: ${JSON.stringify(originalContent)}, Got: ${JSON.stringify(reReadContent)}`
55+
);
56+
}
57+
} finally {
58+
// Step 5: Always restore the original file on disk
59+
await writeFile(diskFilePath, diskOriginalContent);
60+
}
61+
};

packages/project/test/fixtures/application.a/node_modules/library.d/main/src/library/d/data.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
specVersion: "5.0"
3+
type: application
4+
metadata:
5+
name: application.a
6+
builder:
7+
customTasks:
8+
- name: dependency-race-condition-task
9+
afterTask: escapeNonAsciiCharacters
10+
---
11+
specVersion: "5.0"
12+
kind: extension
13+
type: task
14+
metadata:
15+
name: dependency-race-condition-task
16+
task:
17+
path: dependency-race-condition-task.js
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"key": "original-value"}

packages/project/test/lib/build/ProjectBuilder.integration.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2349,6 +2349,39 @@ test.serial("Build race condition: file modified during active build", async (t)
23492349
);
23502350
});
23512351

2352+
test.serial("Build dependency race condition: frozen source reader protects against filesystem changes",
2353+
async (t) => {
2354+
const fixtureTester = new FixtureTester(t, "application.a");
2355+
const destPath = fixtureTester.destPath;
2356+
2357+
// Build with dependency-race-condition custom task and all dependencies included.
2358+
// library.d is built first → its sources are frozen in CAS.
2359+
// Then application.a builds, running the custom task that:
2360+
// 1. Reads library.d's some.js via the dependency reader (CAS-backed)
2361+
// 2. Modifies some.js on disk
2362+
// 3. Re-reads via the dependency reader
2363+
// 4. Asserts the content is still the original CAS-frozen content (not modified disk)
2364+
// 5. Restores the file on disk
2365+
// If the frozen reader is not working, the custom task throws and the build fails.
2366+
await fixtureTester.buildProject({
2367+
graphConfig: {rootConfigPath: "ui5-dependency-race-condition.yaml"},
2368+
config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
2369+
assertions: {
2370+
projects: {
2371+
"library.d": {},
2372+
"library.a": {},
2373+
"library.b": {},
2374+
"library.c": {},
2375+
"application.a": {}
2376+
}
2377+
}
2378+
});
2379+
2380+
// Sanity check: verify library.d's some.js exists in build output
2381+
const builtContent = await fs.readFile(`${destPath}/resources/library/d/some.js`, {encoding: "utf8"});
2382+
t.truthy(builtContent, "library.d some.js exists in build output");
2383+
});
2384+
23522385
test.serial("Build with dependencies: Verify sap-ui-version.json generation and regeneration", async (t) => {
23532386
const fixtureTester = new FixtureTester(t, "application.a");
23542387
const destPath = fixtureTester.destPath;

packages/project/test/lib/resources/ProjectResources.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import test from "ava";
22
import sinon from "sinon";
3+
import {createProxy, createResource} from "@ui5/fs/resourceFactory";
34
import ProjectResources from "../../../lib/resources/ProjectResources.js";
45

56
function createProjectResources({frozenSourceReader} = {}) {
@@ -108,3 +109,57 @@ test("initStages clears frozen source reader", (t) => {
108109
t.truthy(reader2, "Reader returned after initStages");
109110
t.not(reader1, reader2, "Reader was recreated after initStages");
110111
});
112+
113+
test("Frozen source reader takes priority over filesystem source reader", async (t) => {
114+
const resourcePath = "/resources/test/some.js";
115+
const filesystemContent = "filesystem content";
116+
const frozenCASContent = "frozen CAS content";
117+
118+
// Create a source reader that simulates the filesystem
119+
const filesystemResource = createResource({path: resourcePath, string: filesystemContent});
120+
const sourceReader = createProxy({
121+
name: "Filesystem source reader",
122+
listResourcePaths: () => [resourcePath],
123+
getResource: async (virPath) => {
124+
if (virPath === resourcePath) {
125+
return filesystemResource;
126+
}
127+
return null;
128+
}
129+
});
130+
131+
// Create a frozen CAS reader with different content
132+
const frozenResource = createResource({path: resourcePath, string: frozenCASContent});
133+
const frozenReader = createProxy({
134+
name: "Frozen CAS reader",
135+
listResourcePaths: () => [resourcePath],
136+
getResource: async (virPath) => {
137+
if (virPath === resourcePath) {
138+
return frozenResource;
139+
}
140+
return null;
141+
}
142+
});
143+
144+
const writer = {
145+
byGlob: sinon.stub().resolves([]),
146+
write: sinon.stub().resolves(),
147+
};
148+
149+
const pr = new ProjectResources({
150+
getName: () => "test.project",
151+
getStyledReader: sinon.stub().returns(sourceReader),
152+
createWriter: sinon.stub().returns(writer),
153+
addReadersForWriter: sinon.stub(),
154+
buildManifest: null,
155+
});
156+
157+
pr.setFrozenSourceReader(frozenReader);
158+
159+
const reader = pr.getReader();
160+
const result = await reader.byPath(resourcePath);
161+
t.truthy(result, "Resource found via reader");
162+
const content = await result.getString();
163+
t.is(content, frozenCASContent,
164+
"Frozen CAS reader takes priority over filesystem source reader");
165+
});

0 commit comments

Comments
 (0)