Skip to content

Commit f27ea1c

Browse files
committed
Refactor rebuild policy into lifecycle plugin
Introduces a new `JarLifecycleListener` service hook in bndlib and wires `Project` to call listeners before/after JAR writes, replacing the in-core rebuild trigger policy logic. The old `Workspace` rebuild policy state and related `Constants` entries are removed, and the service package version is bumped to 4.11.0. Bndtools now owns rebuild-trigger behavior via `RebuildTriggerPolicy` and `RebuildTriggerPolicyPlugin`, which are registered from `Central.applyRebuildTriggerPolicy()` based on preferences. Preference/UI/explorer code is updated to use the new policy constants, and tests that depended on `Workspace#setRebuildTriggerPolicy` are temporarily commented with TODO markers. Add rebuild trigger policy plugin tests Move content-hash/API-digest rebuild behavior tests out of `ProjectTest` into a new `bndtools.core` test suite that exercises `RebuildTriggerPolicyPlugin` with dedicated workspace testdata. Add `ProjectTest` coverage for `JarLifecycleListener` integration, verifying `beforeWrite`/`afterWrite` invocation and shared context data flow. Also wire `biz.aQute.bnd.test` into `bndtools.core` testpath and remove stale commented cleanup lines. Signed-off-by: Christoph Rueger <chrisrueger@gmail.com>
1 parent 4767395 commit f27ea1c

20 files changed

Lines changed: 881 additions & 493 deletions

File tree

biz.aQute.bndlib.tests/test/test/ProjectTest.java

Lines changed: 52 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
import java.util.HashSet;
1919
import java.util.Iterator;
2020
import java.util.List;
21+
import java.util.Map;
2122
import java.util.Set;
23+
import java.util.concurrent.atomic.AtomicBoolean;
24+
import java.util.concurrent.atomic.AtomicReference;
2225
import java.util.jar.Manifest;
2326
import java.util.regex.Pattern;
2427

@@ -39,6 +42,7 @@
3942
import aQute.bnd.osgi.Processor;
4043
import aQute.bnd.osgi.Resource;
4144
import aQute.bnd.osgi.eclipse.EclipseClasspath;
45+
import aQute.bnd.service.JarLifecycleListener;
4246
import aQute.bnd.service.RepositoryPlugin;
4347
import aQute.bnd.service.Strategy;
4448
import aQute.bnd.test.jupiter.InjectTemporaryDirectory;
@@ -547,181 +551,73 @@ private void stale(Project project, boolean b) throws Exception {
547551
}
548552

549553
/**
550-
* Check that the content-hash optimization prevents unnecessary JAR
551-
* rewrites when the JAR content is unchanged between builds. This avoids
552-
* cascading rebuilds of dependent projects.
554+
* Verify that JarArtifactLifecycleListener plugins are called at the right
555+
* times.
553556
*/
554557
@Test
555-
public void testContentHashSkipsBuildWhenUnchanged() throws Exception {
558+
public void testJarArtifactLifecycleListenerIntegration() throws Exception {
556559
Workspace ws = getWorkspace(IO.getFile("testresources/ws"));
557-
ws.setRebuildTriggerPolicy("api");
558560
Project project = ws.getProject("p-stale");
559561
assertNotNull(project);
560562

561-
// First build - should create JAR and digest files
562-
File[] firstBuild = project.build();
563-
assertNotNull(firstBuild);
564-
assertTrue(firstBuild.length > 0);
565-
566-
File jarFile = firstBuild[0];
567-
assertTrue(jarFile.isFile());
568-
long firstTimestamp = jarFile.lastModified();
569-
570-
// Verify digest file was created
571-
File digestFile = getDigestFile(jarFile);
572-
assertTrue(digestFile.isFile(), "Digest file should be created after build");
573-
String firstDigest = IO.collect(digestFile)
574-
.trim();
575-
assertFalse(firstDigest.isEmpty(), "Digest should not be empty");
576-
577-
// Simulate time passing by adjusting the JAR timestamp backward,
578-
// then mark the project as changed. If the optimization works
579-
// correctly, the JAR's timestamp will be restored to this value
580-
// since the content hasn't changed.
581-
long adjustedTimestamp = firstTimestamp - 10000;
582-
jarFile.setLastModified(adjustedTimestamp);
583-
584-
// Mark project as changed so it rebuilds
585-
project.setChanged();
586-
project.refresh();
587-
588-
// Second build - content is unchanged, JAR timestamp should be
589-
// preserved
590-
File[] secondBuild = project.build();
591-
assertNotNull(secondBuild);
592-
assertTrue(secondBuild.length > 0);
593-
594-
File jarFile2 = secondBuild[0];
595-
assertTrue(jarFile2.isFile());
596-
597-
// The JAR timestamp should be preserved since content didn't change
598-
assertEquals(adjustedTimestamp, jarFile2.lastModified(),
599-
"JAR timestamp should be preserved when content is unchanged");
600-
601-
// Digest file should still exist with the same content
602-
assertTrue(digestFile.isFile());
603-
String secondDigest = IO.collect(digestFile)
604-
.trim();
605-
assertEquals(firstDigest, secondDigest, "Digest should be unchanged");
606-
}
563+
// Register a mock listener to verify it's called
564+
AtomicBoolean beforeCalled = new AtomicBoolean();
565+
AtomicBoolean afterCalled = new AtomicBoolean();
607566

608-
/**
609-
* Check that when JAR content actually changes, the JAR is rewritten and
610-
* the digest is updated.
611-
*/
612-
@Test
613-
public void testContentHashRewritesWhenChanged() throws Exception {
614-
Workspace ws = getWorkspace(IO.getFile("testresources/ws"));
615-
ws.setRebuildTriggerPolicy("api");
616-
Project project = ws.getProject("p-stale");
617-
assertNotNull(project);
567+
JarLifecycleListener mockListener = new JarLifecycleListener() {
568+
@Override
569+
public void beforeWrite(Project p, Jar jar, File outputFile, Map<String, Object> context) {
570+
beforeCalled.set(true);
571+
}
618572

619-
// First build
620-
File[] firstBuild = project.build();
621-
assertNotNull(firstBuild);
622-
File jarFile = firstBuild[0];
623-
File digestFile = getDigestFile(jarFile);
624-
assertTrue(digestFile.isFile());
625-
String firstDigest = IO.collect(digestFile)
626-
.trim();
627-
628-
// Change the project content so the JAR will be different
629-
project.setChanged();
630-
project.refresh();
631-
project.setProperty("Include-Resource", "p;literal=\"changed content\"");
632-
633-
// Second build - content changed, JAR should be rewritten
634-
File[] secondBuild = project.build();
635-
assertNotNull(secondBuild);
636-
File jarFile2 = secondBuild[0];
637-
638-
// Digest should have changed
639-
assertTrue(digestFile.isFile());
640-
String secondDigest = IO.collect(digestFile)
641-
.trim();
642-
assertThat(secondDigest).as("Digest should change when content changes")
643-
.isNotEqualTo(firstDigest);
644-
}
573+
@Override
574+
public void afterWrite(Project p, File outputFile, Jar jar, Map<String, Object> context) {
575+
afterCalled.set(true);
576+
}
577+
};
645578

646-
private static File getDigestFile(File jarFile) {
647-
return new File(jarFile.getParentFile(), jarFile.getName() + ".digest");
579+
ws.addBasicPlugin(mockListener);
580+
581+
// Build
582+
File[] built = project.build();
583+
assertNotNull(built);
584+
assertThat(built.length).isGreaterThan(0);
585+
586+
// Verify listeners were called
587+
assertThat(beforeCalled).isTrue();
588+
assertThat(afterCalled).isTrue();
648589
}
649590

650591
/**
651-
* Check that the JAR timestamp is preserved when the content changes
652-
* Verify that the JAR's timestamp is preserved when the JAR content
653-
* changes but the exported API surface remains identical. The
654-
* optimization works by comparing a stored API-digest sidecar file
655-
* with the newly computed API digest during build. When they match,
656-
* the old file timestamp is kept so that downstream timestamp-based
657-
* staleness checks don't trigger unnecessary cascade rebuilds.
658-
* <p>
659-
* Since the p-stale test project is {@code -resourceonly} (no
660-
* exported packages), this test simulates the mechanism directly:
661-
* we write a known API digest, change the project content so the
662-
* full content digest changes, and verify that timestamp
663-
* preservation still engages via the API digest fallback.
592+
* Verify that WriteContext is passed correctly and allows data exchange.
664593
*/
665594
@Test
666-
public void testApiDigestPreservesTimestampWhenApiUnchanged() throws Exception {
595+
public void testJarArtifactWriteContextDataExchange() throws Exception {
667596
Workspace ws = getWorkspace(IO.getFile("testresources/ws"));
668-
ws.setRebuildTriggerPolicy("api");
669597
Project project = ws.getProject("p-stale");
670-
assertNotNull(project);
671598

672-
// First build — establishes the baseline JAR and digest files
673-
File[] firstBuild = project.build();
674-
assertNotNull(firstBuild);
675-
assertTrue(firstBuild.length > 0);
676-
677-
File jarFile = firstBuild[0];
678-
assertTrue(jarFile.isFile());
679-
680-
// Record the old timestamp and adjust it to simulate time
681-
long adjustedTimestamp = jarFile.lastModified() - 10000;
682-
jarFile.setLastModified(adjustedTimestamp);
683-
684-
// The content digest from the first build
685-
File digestFile = getDigestFile(jarFile);
686-
assertTrue(digestFile.isFile(), "Content digest file should exist after build");
687-
688-
// Write a fake API digest sidecar. The next build will produce
689-
// the same resource-only bundle (no Export-Package), so
690-
// calcApiDigest() returns null. However, when we change the
691-
// project content the content digest will differ. To simulate
692-
// the API-digest-unchanged path, we need a non-null API digest.
693-
// We'll change the content, then verify the mechanism by
694-
// checking the timestamp. Since calcApiDigest returns null for
695-
// resource-only bundles, the API digest path won't engage here,
696-
// but we can verify the infrastructure is correctly wired:
697-
// the API digest file should remain from the first build if no
698-
// new one is computed.
699-
File apiDigestFile = new File(jarFile.getParentFile(), jarFile.getName() + ".api-digest");
700-
701-
// Change the project content so the JAR will differ
702-
project.setChanged();
703-
project.refresh();
704-
project.setProperty("Include-Resource", "p;literal=\"changed content\"");
705-
706-
// Rebuild — content changed so content digest differs, and
707-
// calcApiDigest returns null for resource-only bundles. The
708-
// timestamp should NOT be preserved (no API digest fallback).
709-
File[] secondBuild = project.build();
710-
assertNotNull(secondBuild);
711-
712-
File jarFile2 = secondBuild[0];
713-
// Content changed so the digest should differ
714-
String newDigest = IO.collect(digestFile).trim();
715-
// The JAR should have a new timestamp since both content and
716-
// API digests indicate a change (or API digest is absent)
717-
assertThat(jarFile2.lastModified())
718-
.as("Timestamp should NOT be preserved when content changes and no API digest exists")
719-
.isNotEqualTo(adjustedTimestamp);
720-
721-
// The content digest file should still exist
722-
assertTrue(digestFile.isFile(), "Content digest file should exist after rebuild");
599+
AtomicReference<String> beforeData = new AtomicReference<>();
600+
AtomicReference<String> afterData = new AtomicReference<>();
601+
602+
JarLifecycleListener testListener = new JarLifecycleListener() {
603+
@Override
604+
public void beforeWrite(Project p, Jar jar, File outputFile, Map<String, Object> context) {
605+
context.put("testKey", "testValue");
606+
}
607+
608+
@Override
609+
public void afterWrite(Project p, File outputFile, Jar jar, Map<String, Object> context) {
610+
afterData.set((String) context.get("testKey"));
611+
}
612+
};
613+
614+
ws.addBasicPlugin(testListener);
615+
project.build();
616+
617+
assertThat(afterData.get()).isEqualTo("testValue");
723618
}
724619

620+
725621
/**
726622
* Check multiple repos
727623
*

0 commit comments

Comments
 (0)