From 05a9001dff2608cc2cd44b926af6bd60595f9fc3 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Mon, 20 Apr 2026 19:56:50 +0200 Subject: [PATCH] Add per-builder build events to IResourceChangeEvent Introduce PRE_PROJECT_BUILD and POST_PROJECT_BUILD event types on IResourceChangeEvent, fired once per builder execution from BuildManager.basicBuild around the SafeRunner.run that invokes the builder. Listeners receive the IProject as source, the builder id (from the project's build spec) via the new getBuilderName() default method, and the build kind via getBuildKind(). No resource delta is attached; the surrounding workspace-level PRE_BUILD/POST_BUILD pair continues to carry the aggregated delta. Events fire above the needsBuild short-circuit so every builder that is considered reports a matched PRE/POST pair; builders whose project has no delta produce near-zero durations but still appear to listeners, giving tools complete visibility of the build sequence. The POST_PROJECT_BUILD broadcast runs from a finally around the lock and rule handoff, so a matched pair is delivered even if that handoff throws. Dispatch goes through a new NotificationManager.broadcastProjectBuildEvent path that skips delta computation and does not update lastPostBuildTree, so per-builder events remain pure timing/metadata signals nested inside the workspace build. ResourceChangeListenerList.hasListenerFor was a hard-coded switch over bitmasks 1..32 and silently dropped anything else; extend its counter bookkeeping to include 64 and 128 so listeners registered for the new event types actually receive them. Bumps org.eclipse.core.resources to 3.25.100. Adds ProjectBuildEventTest covering FULL, CLEAN, INCREMENTAL-with-no-delta, nesting order against the enclosing workspace events, and the getBuilderName() contract. Fixes https://github.com/eclipse-platform/eclipse.platform/issues/2801 --- .../META-INF/MANIFEST.MF | 2 +- .../core/internal/events/BuildManager.java | 70 ++++--- .../internal/events/NotificationManager.java | 14 ++ .../internal/events/ResourceChangeEvent.java | 29 +++ .../events/ResourceChangeListenerList.java | 20 ++ .../core/internal/resources/Workspace.java | 9 + .../core/resources/IResourceChangeEvent.java | 50 ++++- .../builders/ProjectBuildEventTest.java | 182 ++++++++++++++++++ 8 files changed, 347 insertions(+), 29 deletions(-) create mode 100644 resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/builders/ProjectBuildEventTest.java diff --git a/resources/bundles/org.eclipse.core.resources/META-INF/MANIFEST.MF b/resources/bundles/org.eclipse.core.resources/META-INF/MANIFEST.MF index 2ffa5128ccf..48f9109e6db 100644 --- a/resources/bundles/org.eclipse.core.resources/META-INF/MANIFEST.MF +++ b/resources/bundles/org.eclipse.core.resources/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.core.resources; singleton:=true -Bundle-Version: 3.24.100.qualifier +Bundle-Version: 3.25.100.qualifier Bundle-Activator: org.eclipse.core.resources.ResourcesPlugin Bundle-Vendor: %providerName Bundle-Localization: plugin diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/BuildManager.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/BuildManager.java index 170f4d33de5..31517c21ee6 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/BuildManager.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/BuildManager.java @@ -49,6 +49,7 @@ import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IProjectDescription; import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.resources.IResourceStatus; import org.eclipse.core.resources.IncrementalProjectBuilder; @@ -262,6 +263,11 @@ private void basicBuild(int trigger, IncrementalProjectBuilder builder, Map= 0) { - getWorkManager().endUnprotected(depth); - } - if (rule != null) { - try { - Job.getJobManager().endRule(rule); - } catch (IllegalArgumentException e) { - throw handleRuleConflict(false, currentBuilder, e); + // Re-acquire the WS lock and release the rule so POST_PROJECT_BUILD + // sees the same locking state as PRE_PROJECT_BUILD did. + try { + if (depth >= 0) { + getWorkManager().endUnprotected(depth); } - } - // Be sure to clean up after ourselves. - if (clean || currentBuilder.wasForgetStateRequested()) { - currentBuilder.setLastBuiltTree(null); - } else if (currentBuilder.wasRememberStateRequested()) { - // If remember last build state, and FULL_BUILD - // last tree must be set to => null for next build - if (trigger == IncrementalProjectBuilder.FULL_BUILD) { - currentBuilder.setLastBuiltTree(null); + if (rule != null) { + try { + Job.getJobManager().endRule(rule); + } catch (IllegalArgumentException e) { + throw handleRuleConflict(false, currentBuilder, e); + } + } + } finally { + // Guarantee the matching POST_PROJECT_BUILD even if the handoff above threw. + try { + workspace.broadcastProjectBuildEvent(builderProject, builderName, + IResourceChangeEvent.POST_PROJECT_BUILD, trigger); + } finally { + // Be sure to clean up after ourselves. + if (clean || currentBuilder.wasForgetStateRequested()) { + currentBuilder.setLastBuiltTree(null); + } else if (currentBuilder.wasRememberStateRequested()) { + // If remember last build state, and FULL_BUILD + // last tree must be set to => null for next build + if (trigger == IncrementalProjectBuilder.FULL_BUILD) { + currentBuilder.setLastBuiltTree(null); + } + // else don't modify the last built tree + } else { + // remember the current state as the last built state. + ElementTree lastTree = workspace.getElementTree(); + lastTree.immutable(); + currentBuilder.setLastBuiltTree(lastTree); + } + hookEndBuild(builder); } - // else don't modify the last built tree - } else { - // remember the current state as the last built state. - ElementTree lastTree = workspace.getElementTree(); - lastTree.immutable(); - currentBuilder.setLastBuiltTree(lastTree); } - hookEndBuild(builder); } } finally { currentBuilders.remove(currentBuilder); diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/NotificationManager.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/NotificationManager.java index 017afaec0c3..784ad5768cd 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/NotificationManager.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/NotificationManager.java @@ -214,6 +214,20 @@ private void cleanUp(ElementTree lastState, int type) { } } + /** + * Dispatches a per-builder build event (PRE_PROJECT_BUILD / POST_PROJECT_BUILD). + * Unlike {@link #broadcastChanges}, this computes no resource delta and does not + * touch {@code lastPostBuildTree}; the events are pure timing/metadata signals + * nested inside the enclosing workspace build. + */ + public void broadcastProjectBuildEvent(IProject project, String builderName, int type, int buildKind) { + if (!listeners.hasListenerFor(type)) { + return; + } + ResourceChangeEvent event = new ResourceChangeEvent(project, type, buildKind, project, builderName); + notify(getListeners(), event, false); + } + /** * Helper method for the save participant lifecycle computation. */ public void broadcastChanges(IResourceChangeListener listener, int type, IResourceDelta delta) { diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/ResourceChangeEvent.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/ResourceChangeEvent.java index 96ea502d275..07158852431 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/ResourceChangeEvent.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/ResourceChangeEvent.java @@ -12,6 +12,7 @@ * IBM Corporation - initial API and implementation * James Blackburn (Broadcom Corp.) - ongoing development * Lars Vogel - Bug 473427 + * Vogella GmbH - per-builder build events *******************************************************************************/ package org.eclipse.core.internal.events; @@ -32,6 +33,11 @@ public class ResourceChangeEvent extends EventObject implements IResourceChangeE private int trigger = 0; int type; + /** + * The builder id for PRE_PROJECT_BUILD / POST_PROJECT_BUILD events, or null. + */ + private String builderName; + protected ResourceChangeEvent(Object source, int type, IResource resource) { super(source); this.resource = resource; @@ -45,6 +51,12 @@ public ResourceChangeEvent(Object source, int type, int buildKind, IResourceDelt this.type = type; } + public ResourceChangeEvent(Object source, int type, int buildKind, IResource resource, String builderName) { + this(source, type, resource); + this.trigger = buildKind; + this.builderName = builderName; + } + /** * @see IResourceChangeEvent#findMarkerDeltas(String, boolean) */ @@ -107,6 +119,14 @@ public int getType() { return type; } + /** + * @see IResourceChangeEvent#getBuilderName() + */ + @Override + public String getBuilderName() { + return builderName; + } + public void setDelta(IResourceDelta value) { delta = value; } @@ -133,6 +153,12 @@ public String toDebugString() { case PRE_REFRESH : output.append("PRE_REFRESH"); //$NON-NLS-1$ break; + case PRE_PROJECT_BUILD : + output.append("PRE_PROJECT_BUILD"); //$NON-NLS-1$ + break; + case POST_PROJECT_BUILD : + output.append("POST_PROJECT_BUILD"); //$NON-NLS-1$ + break; default : output.append("?"); //$NON-NLS-1$ break; @@ -154,6 +180,9 @@ public String toDebugString() { break; } output.append("\nResource: " + (resource == null ? "null" : resource)); //$NON-NLS-1$ //$NON-NLS-2$ + if (builderName != null) { + output.append("\nBuilder: " + builderName); //$NON-NLS-1$ + } output.append("\nDelta:" + (delta == null ? " null" : ((ResourceDelta) delta).toDeepDebugString())); //$NON-NLS-1$ //$NON-NLS-2$ return output.toString(); } diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/ResourceChangeListenerList.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/ResourceChangeListenerList.java index 3fc2de8496e..7c32a72a0d0 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/ResourceChangeListenerList.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/ResourceChangeListenerList.java @@ -60,6 +60,8 @@ public String toString() { private volatile int count8 = 0; private volatile int count16 = 0; private volatile int count32 = 0; + private volatile int count64 = 0; + private volatile int count128 = 0; /** * The list of listeners. @@ -114,6 +116,12 @@ private void adding(int mask) { if ((mask & 32) != 0) { count32++; } + if ((mask & 64) != 0) { + count64++; + } + if ((mask & 128) != 0) { + count128++; + } } /** @@ -138,6 +146,10 @@ public boolean hasListenerFor(int event) { return count16 > 0; case 32: return count32 > 0; + case 64: + return count64 > 0; + case 128: + return count128 > 0; default: return false; } @@ -170,6 +182,8 @@ public synchronized void clear() { count8 = 0; count16 = 0; count32 = 0; + count64 = 0; + count128 = 0; } private void removing(int mask) { @@ -191,6 +205,12 @@ private void removing(int mask) { if ((mask & 32) != 0) { count32--; } + if ((mask & 64) != 0) { + count64--; + } + if ((mask & 128) != 0) { + count128--; + } } @Override diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Workspace.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Workspace.java index 690cb8fe72e..c6cbc2e0443 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Workspace.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Workspace.java @@ -458,6 +458,15 @@ public void broadcastBuildEvent(Object source, int type, int buildTrigger) { notificationManager.broadcastChanges(tree, event, false); } + /** + * Broadcasts a per-builder build event. The {@code type} must be either + * {@link IResourceChangeEvent#PRE_PROJECT_BUILD} or + * {@link IResourceChangeEvent#POST_PROJECT_BUILD}. + */ + public void broadcastProjectBuildEvent(IProject project, String builderName, int type, int buildTrigger) { + notificationManager.broadcastProjectBuildEvent(project, builderName, type, buildTrigger); + } + /** * Broadcasts an internal workspace lifecycle event to interested * internal listeners. diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IResourceChangeEvent.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IResourceChangeEvent.java index 2d4ad4be70f..b7cb48e56d7 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IResourceChangeEvent.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IResourceChangeEvent.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2014 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -10,6 +10,7 @@ * * Contributors: * IBM Corporation - initial API and implementation + * Vogella GmbH - per-builder build events *******************************************************************************/ package org.eclipse.core.resources; @@ -86,6 +87,20 @@ * method returns the project being refreshed. * The workspace is closed for changes during notification of these events. * + *
  • + * Before-the-fact report that a single builder is about to run on a single + * project. Event type is PRE_PROJECT_BUILD; getSource + * returns the {@link IProject}, getBuilderName the builder id, + * and getBuildKind the kind of build. Fired once per builder, + * nested inside the enclosing PRE_BUILD/POST_BUILD + * pair, with no resource delta. + *
  • + *
  • + * After-the-fact report that a single builder has run on a single project. + * Event type is POST_PROJECT_BUILD; the accessors behave as for + * PRE_PROJECT_BUILD. Under parallel builds, events for different + * projects may be delivered concurrently on different threads. + *
  • * *

    * In order to handle additional event types that may be introduced @@ -179,6 +194,26 @@ public interface IResourceChangeEvent { */ int PRE_REFRESH = 32; + /** + * Event type constant (bit mask) indicating that a single builder is about to + * run on a single project. getSource returns the {@link IProject}, + * getBuilderName the builder id, and getBuildKind the + * kind of build; no resource delta is provided. + * + * @see #getBuilderName() + * @since 3.25 + */ + int PRE_PROJECT_BUILD = 64; + + /** + * Event type constant (bit mask) indicating that a single builder has run on a + * single project. Accessors behave as for {@link #PRE_PROJECT_BUILD}. + * + * @see #getBuilderName() + * @since 3.25 + */ + int POST_PROJECT_BUILD = 128; + /** * Returns all marker deltas of the specified type that are associated * with resource deltas for this event. If includeSubtypes @@ -262,6 +297,19 @@ public interface IResourceChangeEvent { * @see #PRE_CLOSE * @see #PRE_DELETE * @see #PRE_REFRESH + * @see #PRE_PROJECT_BUILD + * @see #POST_PROJECT_BUILD */ int getType(); + + /** + * Returns the id of the builder this event relates to (as declared in the + * project's build spec) for {@link #PRE_PROJECT_BUILD} and + * {@link #POST_PROJECT_BUILD} events, or null for other events. + * + * @since 3.25 + */ + default String getBuilderName() { + return null; + } } diff --git a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/builders/ProjectBuildEventTest.java b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/builders/ProjectBuildEventTest.java new file mode 100644 index 00000000000..8ec50040079 --- /dev/null +++ b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/builders/ProjectBuildEventTest.java @@ -0,0 +1,182 @@ +/******************************************************************************* + * Copyright (c) 2026 Vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.core.tests.internal.builders; + +import static org.eclipse.core.resources.ResourcesPlugin.getWorkspace; +import static org.eclipse.core.tests.resources.ResourceTestUtil.createTestMonitor; +import static org.eclipse.core.tests.resources.ResourceTestUtil.setAutoBuilding; +import static org.eclipse.core.tests.resources.ResourceTestUtil.updateProjectDescription; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.tests.resources.util.WorkspaceResetExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Tests the PRE_PROJECT_BUILD and POST_PROJECT_BUILD events. In particular it + * asserts that events fire even when a builder is short-circuited by the + * needsBuild check (e.g. an incremental build after Project > Clean where no + * source changed). + */ +@ExtendWith(WorkspaceResetExtension.class) +public class ProjectBuildEventTest { + + private record Record(int type, Object source, String builderName, int buildKind) { + } + + private final List events = new CopyOnWriteArrayList<>(); + private final IResourceChangeListener listener = event -> events.add( + new Record(event.getType(), event.getSource(), event.getBuilderName(), event.getBuildKind())); + + @BeforeEach + public void setUp() { + int mask = IResourceChangeEvent.PRE_BUILD | IResourceChangeEvent.POST_BUILD + | IResourceChangeEvent.PRE_PROJECT_BUILD | IResourceChangeEvent.POST_PROJECT_BUILD; + getWorkspace().addResourceChangeListener(listener, mask); + } + + @AfterEach + public void tearDown() { + getWorkspace().removeResourceChangeListener(listener); + } + + @Test + public void testPerBuilderEventsFireOnFullBuild() throws CoreException { + IProject project = createProjectWithBuilder("FULL"); + + events.clear(); + getWorkspace().build(IncrementalProjectBuilder.FULL_BUILD, createTestMonitor()); + + List perBuilder = filter(IResourceChangeEvent.PRE_PROJECT_BUILD, + IResourceChangeEvent.POST_PROJECT_BUILD); + assertEquals(2, perBuilder.size(), "expected one PRE/POST_PROJECT_BUILD pair, got events: " + events); + + Record pre = perBuilder.get(0); + Record post = perBuilder.get(1); + assertEquals(IResourceChangeEvent.PRE_PROJECT_BUILD, pre.type()); + assertEquals(IResourceChangeEvent.POST_PROJECT_BUILD, post.type()); + assertEquals(project, pre.source()); + assertEquals(project, post.source()); + assertEquals(DeltaVerifierBuilder.BUILDER_NAME, pre.builderName()); + assertEquals(DeltaVerifierBuilder.BUILDER_NAME, post.builderName()); + assertEquals(IncrementalProjectBuilder.FULL_BUILD, pre.buildKind()); + assertEquals(IncrementalProjectBuilder.FULL_BUILD, post.buildKind()); + } + + @Test + public void testPerBuilderEventsFireOnCleanBuild() throws CoreException { + createProjectWithBuilder("CLEAN"); + getWorkspace().build(IncrementalProjectBuilder.FULL_BUILD, createTestMonitor()); + + events.clear(); + getWorkspace().build(IncrementalProjectBuilder.CLEAN_BUILD, createTestMonitor()); + List perBuilder = filter(IResourceChangeEvent.PRE_PROJECT_BUILD, + IResourceChangeEvent.POST_PROJECT_BUILD); + assertEquals(2, perBuilder.size(), "CLEAN should fire one PRE/POST_PROJECT_BUILD pair, got: " + events); + assertEquals(IncrementalProjectBuilder.CLEAN_BUILD, perBuilder.get(0).buildKind()); + assertEquals(IncrementalProjectBuilder.CLEAN_BUILD, perBuilder.get(1).buildKind()); + } + + @Test + public void testPerBuilderEventsFireOnIncrementalBuildWithNoDelta() throws CoreException { + createProjectWithBuilder("INC-NO-DELTA"); + // Prime: run a full build so the builder has a last-built tree. + getWorkspace().build(IncrementalProjectBuilder.FULL_BUILD, createTestMonitor()); + + // Second incremental build without any intervening source change. The + // builder should be considered but short-circuited by needsBuild. Events + // must still fire so the Build Monitor view can render the session. + events.clear(); + getWorkspace().build(IncrementalProjectBuilder.INCREMENTAL_BUILD, createTestMonitor()); + List perBuilder = filter(IResourceChangeEvent.PRE_PROJECT_BUILD, + IResourceChangeEvent.POST_PROJECT_BUILD); + assertEquals(2, perBuilder.size(), + "INCREMENTAL with no delta must still fire PRE/POST_PROJECT_BUILD, got: " + events); + } + + @Test + public void testPerBuilderEventsNestedInsideWorkspaceBuild() throws CoreException { + createProjectWithBuilder("NESTED"); + + events.clear(); + getWorkspace().build(IncrementalProjectBuilder.FULL_BUILD, createTestMonitor()); + + List types = new ArrayList<>(); + for (Record r : events) { + types.add(r.type()); + } + int preBuild = types.indexOf(IResourceChangeEvent.PRE_BUILD); + int postBuild = types.indexOf(IResourceChangeEvent.POST_BUILD); + int prePrj = types.indexOf(IResourceChangeEvent.PRE_PROJECT_BUILD); + int postPrj = types.indexOf(IResourceChangeEvent.POST_PROJECT_BUILD); + assertTrue(preBuild >= 0 && postBuild > preBuild, "workspace PRE/POST_BUILD must be present and ordered: " + events); + assertTrue(prePrj > preBuild && prePrj < postBuild, + "PRE_PROJECT_BUILD must be nested inside workspace build: " + events); + assertTrue(postPrj > prePrj && postPrj < postBuild, + "POST_PROJECT_BUILD must come after its PRE and before workspace POST_BUILD: " + events); + } + + @Test + public void testBuilderNameOnlyPopulatedForProjectBuildEvents() throws CoreException { + createProjectWithBuilder("NAME"); + events.clear(); + getWorkspace().build(IncrementalProjectBuilder.FULL_BUILD, createTestMonitor()); + + boolean sawProjectBuildWithName = false; + for (Record r : events) { + if (r.type() == IResourceChangeEvent.PRE_PROJECT_BUILD + || r.type() == IResourceChangeEvent.POST_PROJECT_BUILD) { + assertNotNull(r.builderName(), "per-builder event must carry builder name"); + sawProjectBuildWithName = true; + } else { + assertNull(r.builderName(), "workspace-level event must not carry builder name, got: " + r); + } + } + assertTrue(sawProjectBuildWithName, "expected at least one per-builder event: " + events); + } + + private IProject createProjectWithBuilder(String tag) throws CoreException { + IProject project = getWorkspace().getRoot().getProject("PROJECT-" + tag); + setAutoBuilding(false); + project.create(createTestMonitor()); + project.open(createTestMonitor()); + updateProjectDescription(project).addingCommand(DeltaVerifierBuilder.BUILDER_NAME) + .withTestBuilderId("Builder-" + tag).apply(); + return project; + } + + private List filter(int... types) { + List out = new ArrayList<>(); + for (Record r : events) { + for (int t : types) { + if (r.type() == t) { + out.add(r); + break; + } + } + } + return out; + } +}