From 4a6c761a5186e6a2afac138063ceb20883c40059 Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Wed, 16 Sep 2026 09:00:27 +0200 Subject: [PATCH 1/2] test(project): Add ProjectBuilder integration test for custom-task determineBuildSignature Verify that a custom task can implement and use the determineBuildSignature callback end-to-end via graph.build(): a stable callback return keeps the project cache intact, while a changed return invalidates it and forces a rebuild. The callback derives its signature from an on-disk control file so the test can flip the signature independently of any source-resource change. --- .../application.a/task.build-signature.js | 29 +++++++++ .../ui5-customTask-buildSignature.yaml | 17 +++++ .../ProjectBuilder.customTasks.integration.js | 65 +++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 packages/project/test/fixtures/application.a/task.build-signature.js create mode 100644 packages/project/test/fixtures/application.a/ui5-customTask-buildSignature.yaml diff --git a/packages/project/test/fixtures/application.a/task.build-signature.js b/packages/project/test/fixtures/application.a/task.build-signature.js new file mode 100644 index 00000000000..a1c2f70130c --- /dev/null +++ b/packages/project/test/fixtures/application.a/task.build-signature.js @@ -0,0 +1,29 @@ +const Logger = require("@ui5/logger"); +const log = Logger.getLogger("builder:tasks:buildSignatureTask"); + +const {readFile} = require("node:fs/promises"); +const {join} = require("node:path"); + +// No-op task body: the task itself does nothing. This extension exists solely to exercise the +// determineBuildSignature callback below. +module.exports = async function () { + log.verbose("build-signature-task executed"); +}; + +// determineBuildSignature is invoked by TaskDefinitions#getBuildSignatures() to contribute a value +// to the project's build signature. Here we derive the signature from an on-disk control file +// located at the project root (outside the monitored source/dependency readers), so that a test can +// change the returned signature independently of any source-file change. +module.exports.determineBuildSignature = async function ({taskUtil}) { + const rootPath = taskUtil.getProject().getRootPath(); + const controlFilePath = join(rootPath, "buildSignatureControl.txt"); + try { + const content = await readFile(controlFilePath, {encoding: "utf8"}); + log.verbose(`build-signature-task determineBuildSignature: ${content.trim()}`); + return content.trim(); + } catch (err) { + // Fall back to a constant if the control file is missing + log.verbose(`build-signature-task determineBuildSignature: control file missing (${err.code})`); + return "no-control-file"; + } +}; diff --git a/packages/project/test/fixtures/application.a/ui5-customTask-buildSignature.yaml b/packages/project/test/fixtures/application.a/ui5-customTask-buildSignature.yaml new file mode 100644 index 00000000000..c7747355ba3 --- /dev/null +++ b/packages/project/test/fixtures/application.a/ui5-customTask-buildSignature.yaml @@ -0,0 +1,17 @@ +--- +specVersion: "5.0" +type: application +metadata: + name: application.a +builder: + customTasks: + - name: build-signature-task + afterTask: minify +--- +specVersion: "5.0" +kind: extension +type: task +metadata: + name: build-signature-task +task: + path: task.build-signature.js diff --git a/packages/project/test/lib/build/ProjectBuilder.customTasks.integration.js b/packages/project/test/lib/build/ProjectBuilder.customTasks.integration.js index 1c060c8fa76..ec2855c1293 100644 --- a/packages/project/test/lib/build/ProjectBuilder.customTasks.integration.js +++ b/packages/project/test/lib/build/ProjectBuilder.customTasks.integration.js @@ -325,6 +325,71 @@ test.serial.skip("Build application.a (dependency content changes)", async (t) = t.true(builtFileContent2.includes(`console.log('something new');`), "Build dest contains changed file content"); }); +test.serial("Build application.a (custom task determineBuildSignature callback)", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + await fixtureTester._initialize(); + + // The custom task "build-signature-task" implements a determineBuildSignature callback which + // derives the project's build signature from an on-disk control file at the project root. + // Changing that file's content changes the returned signature (and nothing else), which must + // invalidate application.a's build cache — proving the callback is wired into signature + // computation. A stable content must keep the cache intact. + const controlFilePath = `${fixtureTester.fixturePath}/buildSignatureControl.txt`; + await fs.writeFile(controlFilePath, "v1"); + + // #1 build (no cache): everything builds + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-customTask-buildSignature.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + "library.d": {}, + "library.a": {}, + "library.b": {}, + "library.c": {}, + "application.a": {} + } + } + }); + + // #2 build (with cache, signature unchanged): full cache hit, nothing rebuilt + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-customTask-buildSignature.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); + + // Change only the control file → determineBuildSignature returns a different value. + // No source or dependency resource changes. + await fs.writeFile(controlFilePath, "v2"); + + // #3 build (with cache, changed signature): application.a's build signature changed, so its + // cache is invalidated and it is rebuilt. The dependencies are unaffected. + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-customTask-buildSignature.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: { + // application.a is rebuilt (its build signature changed); none of its tasks can be + // reused from cache, since the changed signature invalidates the whole project cache. + "application.a": {} + } + } + }); + + // #4 build (with cache, signature unchanged again): full cache hit, nothing rebuilt + await fixtureTester.buildProject({ + graphConfig: {rootConfigPath: "ui5-customTask-buildSignature.yaml"}, + config: {destPath, cleanDest: true}, + assertions: { + projects: {} + } + }); +}); + test.serial("Build application.a (cross-project tag change)", async (t) => { const fixtureTester = new FixtureTester(t, "application.a"); const destPath = fixtureTester.destPath; From 9cab5cd7f8c0897c08d4e7d132766ac11ad7168d Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Wed, 16 Sep 2026 09:07:59 +0200 Subject: [PATCH 2/2] test(project): Add failing BuildServer test for determineBuildSignature staleness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capture the status quo: a running BuildServer computes each project's build signature exactly once (BuildContext memoizes the ProjectBuildContext for the server's lifetime), so a custom task's determineBuildSignature callback is never re-evaluated. Changing an input the callback reads (here an on-disk control file outside the watched source paths) while the server runs is therefore silently ignored, and the server keeps serving a stale build result. The new test asserts the desired behavior — the change is reflected without a server restart — and is marked test.failing since the current behavior does not do this. The build-signature custom task now also appends the control value to test.js so the served output observably depends on the callback input. --- .../application.a/task.build-signature.js | 42 ++++++++++------ .../test/lib/build/BuildServer.integration.js | 48 +++++++++++++++++++ 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/packages/project/test/fixtures/application.a/task.build-signature.js b/packages/project/test/fixtures/application.a/task.build-signature.js index a1c2f70130c..9b78d20c3b5 100644 --- a/packages/project/test/fixtures/application.a/task.build-signature.js +++ b/packages/project/test/fixtures/application.a/task.build-signature.js @@ -4,26 +4,40 @@ const log = Logger.getLogger("builder:tasks:buildSignatureTask"); const {readFile} = require("node:fs/promises"); const {join} = require("node:path"); -// No-op task body: the task itself does nothing. This extension exists solely to exercise the -// determineBuildSignature callback below. -module.exports = async function () { - log.verbose("build-signature-task executed"); -}; - -// determineBuildSignature is invoked by TaskDefinitions#getBuildSignatures() to contribute a value -// to the project's build signature. Here we derive the signature from an on-disk control file -// located at the project root (outside the monitored source/dependency readers), so that a test can -// change the returned signature independently of any source-file change. -module.exports.determineBuildSignature = async function ({taskUtil}) { - const rootPath = taskUtil.getProject().getRootPath(); +// Reads the control file located at the project root (outside the monitored source/dependency +// readers). Both the task body and the determineBuildSignature callback derive their value from it, +// so a test can change the returned value / produced output independently of any source-file change. +async function readControlValue(rootPath) { const controlFilePath = join(rootPath, "buildSignatureControl.txt"); try { const content = await readFile(controlFilePath, {encoding: "utf8"}); - log.verbose(`build-signature-task determineBuildSignature: ${content.trim()}`); return content.trim(); } catch (err) { // Fall back to a constant if the control file is missing - log.verbose(`build-signature-task determineBuildSignature: control file missing (${err.code})`); + log.verbose(`build-signature-task: control file missing (${err.code})`); return "no-control-file"; } +} + +// Task body: appends the current control value to the application's test.js. This makes the built +// output observably depend on the control file, so a served resource reflects its value. +module.exports = async function ({taskUtil, workspace, options: {projectNamespace}}) { + log.verbose("build-signature-task executed"); + + const controlValue = await readControlValue(taskUtil.getProject().getRootPath()); + const resource = await workspace.byPath(`/resources/${projectNamespace}/test.js`); + if (resource) { + const content = `${await resource.getString()}\n// build-signature-control: ${controlValue}\n`; + resource.setString(content); + await workspace.write(resource); + } +}; + +// determineBuildSignature is invoked by TaskDefinitions#getBuildSignatures() to contribute a value +// to the project's build signature. We derive it from the same control file so that changing the +// file changes the returned signature (and thus must invalidate the project's build cache). +module.exports.determineBuildSignature = async function ({taskUtil}) { + const controlValue = await readControlValue(taskUtil.getProject().getRootPath()); + log.verbose(`build-signature-task determineBuildSignature: ${controlValue}`); + return controlValue; }; diff --git a/packages/project/test/lib/build/BuildServer.integration.js b/packages/project/test/lib/build/BuildServer.integration.js index 61832f020e8..a8953ecfe11 100644 --- a/packages/project/test/lib/build/BuildServer.integration.js +++ b/packages/project/test/lib/build/BuildServer.integration.js @@ -1307,6 +1307,54 @@ test.serial("Serve application.a (test exclusion of generateVersionInfo)", async }); }); +// A custom task's determineBuildSignature callback derives the project's build signature from an +// input that is NOT a watched source resource (here: an on-disk control file at the project root). +// The desired behavior is that changing such an input while the server runs invalidates the served +// build result — otherwise the dev server keeps serving a stale state and the user has no way of +// knowing. This test asserts that desired behavior: request a resource, change the control file +// (which both the task body and determineBuildSignature read), request again, and expect the served +// content to reflect the new value WITHOUT restarting the server. +// +// It is marked test.failing because it currently fails: BuildServer computes each project's build +// signature exactly once (BuildContext memoizes the ProjectBuildContext for the server's lifetime), +// so determineBuildSignature is never re-evaluated for a running server, and the changed control +// file is ignored until the next `serve()`. AVA reports a failing-marked test as a pass while it +// throws and as a hard error once it starts passing, so committing it keeps CI green and flips to a +// signal the moment the behavior is fixed (at which point drop the `.failing`). +test.serial.failing( + "Serve application.a, changing a determineBuildSignature input invalidates served output", async (t) => { + const fixtureTester = t.context.fixtureTester = await FixtureTester.create(t, "application.a"); + + // The custom task appends the control file's value to test.js and also feeds it into + // determineBuildSignature. + const controlFilePath = `${fixtureTester.fixturePath}/buildSignatureControl.txt`; + await fs.writeFile(controlFilePath, "v1"); + + await fixtureTester.serveProject({ + graphConfig: {rootConfigPath: "ui5-customTask-buildSignature.yaml"}, + }); + + // #1 request: served test.js reflects control value "v1" + const first = await fixtureTester.requestResource({resource: "/test.js"}); + const firstContent = await first.getString(); + t.true(firstContent.includes("// build-signature-control: v1"), + "Initial served resource reflects control value v1"); + + // Change ONLY the control file — no watched source resource changes. The determineBuildSignature + // input is now different. The control file lives at the project root, outside the watched + // source paths, so no watcher event fires for it (mirroring a real determineBuildSignature + // input that is not a project source resource). + await fs.writeFile(controlFilePath, "v2"); + + // #2 request: the served resource must reflect the new control value "v2". + const second = await fixtureTester.requestResource({resource: "/test.js"}); + const secondContent = await second.getString(); + t.true(secondContent.includes("// build-signature-control: v2"), + "Served resource reflects the changed determineBuildSignature input without a server restart"); + t.false(secondContent.includes("// build-signature-control: v1"), + "Served resource no longer reflects the stale control value v1"); + }); + function getFixturePath(fixtureName) { return fileURLToPath(new URL(`../../fixtures/${fixtureName}`, import.meta.url)); }