Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,11 @@ private void basicBuild(int trigger, IncrementalProjectBuilder builder, Map<Stri
depth = getWorkManager().beginUnprotected();
// Acquire the rule required for running this builder
if (rule != null) {
Job.getJobManager().beginRule(rule, monitor);
try {
Comment thread
vogella marked this conversation as resolved.
Job.getJobManager().beginRule(rule, monitor);
} catch (IllegalArgumentException e) {
throw handleRuleConflict(true, currentBuilder, e);
}
// Now that we've acquired the rule, changes may have been made concurrently, ensure we're pointing at the
// correct currentTree so delta contains concurrent changes made in areas guarded by the scheduling rule
if (currentTree != null) {
Expand All @@ -303,7 +307,11 @@ private void basicBuild(int trigger, IncrementalProjectBuilder builder, Map<Stri
getWorkManager().endUnprotected(depth);
}
if (rule != null) {
Job.getJobManager().endRule(rule);
try {
Job.getJobManager().endRule(rule);
} catch (IllegalArgumentException e) {
throw handleRuleConflict(false, currentBuilder, e);
}
}
// Be sure to clean up after ourselves.
if (clean || currentBuilder.wasForgetStateRequested()) {
Expand Down Expand Up @@ -395,6 +403,35 @@ protected void basicBuild(IBuildConfiguration buildConfiguration, final int trig
}
}

/**
* Wraps an {@link IllegalArgumentException} thrown by
* {@link org.eclipse.core.runtime.jobs.IJobManager#beginRule(ISchedulingRule, IProgressMonitor)
* beginRule} or
* {@link org.eclipse.core.runtime.jobs.IJobManager#endRule(ISchedulingRule)
* endRule} with builder context (label, class, plugin id, project) so the
* offending builder can be identified from the stack trace. The conflicting
* rule and the underlying error are preserved via the wrapped cause.
*
* @param beginRule {@code true} if the failure occurred during
* {@code beginRule}, {@code false} for {@code endRule}
*/
private static IllegalArgumentException handleRuleConflict(boolean beginRule, InternalBuilder currentBuilder,
IllegalArgumentException e) {
String label = currentBuilder.getLabel();
String pluginId = currentBuilder.getPluginId();
IProject project = currentBuilder.getProject();
String projectName = project != null ? project.getFullPath().toString() : "<unknown>"; //$NON-NLS-1$
String op = beginRule ? "beginRule" : "endRule"; //$NON-NLS-1$ //$NON-NLS-2$
String enhancedMessage = String.format("%s failed for builder %s ('%s', plugin %s) on project %s: %s", //$NON-NLS-1$
op,
currentBuilder.getClass().getName(),
label != null ? label : "<unknown>", //$NON-NLS-1$
pluginId != null ? pluginId : "<unknown>", //$NON-NLS-1$
projectName,
e.getMessage());
return new IllegalArgumentException(enhancedMessage, e);
}

/**
* Runs all builders on the given project config.
* @return A status indicating if the build succeeded or failed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

@Suite
@SelectClasses({ //
BuildManagerRuleConflictMessageTest.class, //
BuildProjectFromMultipleJobsTest.class, //
})
public class AllEventsTests {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*******************************************************************************
* 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
*
* Contributors:
* Lars Vogel <Lars.Vogel@vogella.com> - initial API and implementation
*******************************************************************************/
package org.eclipse.core.tests.internal.events;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertSame;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;

import org.eclipse.core.internal.events.BuildManager;
import org.eclipse.core.internal.events.InternalBuilder;
import org.eclipse.core.resources.IBuildConfiguration;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.junit.jupiter.api.Test;

/**
* Verifies the format of the enhanced {@link IllegalArgumentException} that
* {@code BuildManager} throws when a builder hits a scheduling-rule conflict
* during {@code beginRule} or {@code endRule}. The wrapped message has to
* identify the offending builder so it can be diagnosed from a stack trace.
*/
public class BuildManagerRuleConflictMessageTest {

private static final String BUILDER_LABEL = "My Awesome Builder";
private static final String PLUGIN_ID = "com.example.builders";
private static final String PROJECT_PATH = "/MyProject";

@Test
public void beginRuleConflictMessageIdentifiesOffendingBuilder() throws Exception {
InternalBuilder builder = newStubBuilder(stubProject(PROJECT_PATH));
IllegalArgumentException original = new IllegalArgumentException(
"Attempted to beginRule: P/MyProject/foo, does not match outer scope rule: P/Other");

IllegalArgumentException result = invokeHandleRuleConflict(true, builder, original);

assertThat(result.getMessage()) //
.startsWith("beginRule failed for builder " + builder.getClass().getName()) //
.contains("'" + BUILDER_LABEL + "'") //
.contains("plugin " + PLUGIN_ID) //
.contains("on project " + PROJECT_PATH) //
.endsWith(": " + original.getMessage());
assertSame(original, result.getCause());
}

@Test
public void endRuleConflictMessageIdentifiesOffendingBuilder() throws Exception {
InternalBuilder builder = newStubBuilder(stubProject(PROJECT_PATH));
IllegalArgumentException original = new IllegalArgumentException(
"Attempted to endRule: P/MyProject/foo, does not match the rule of current entry: P/MyProject");

IllegalArgumentException result = invokeHandleRuleConflict(false, builder, original);

assertThat(result.getMessage()) //
.startsWith("endRule failed for builder " + builder.getClass().getName()) //
.contains("'" + BUILDER_LABEL + "'") //
.contains("plugin " + PLUGIN_ID) //
.contains("on project " + PROJECT_PATH) //
.endsWith(": " + original.getMessage());
assertSame(original, result.getCause());
}

@Test
public void messageFallsBackToPlaceholdersWhenBuilderMetadataMissing() throws Exception {
InternalBuilder builder = newStubBuilder(null);
IllegalArgumentException original = new IllegalArgumentException("boom");
setInternalBuilderField(builder, "label", null);
setInternalBuilderField(builder, "pluginId", null);

IllegalArgumentException result = invokeHandleRuleConflict(true, builder, original);

assertThat(result.getMessage()) //
.contains("'<unknown>'") //
.contains("plugin <unknown>") //
.contains("on project <unknown>");
}

private static InternalBuilder newStubBuilder(IProject project) throws Exception {
IncrementalProjectBuilder builder = new IncrementalProjectBuilder() {
@Override
protected IProject[] build(int kind, Map<String, String> args, IProgressMonitor monitor) {
return null;
}
};
setInternalBuilderField(builder, "label", BUILDER_LABEL);
setInternalBuilderField(builder, "pluginId", PLUGIN_ID);
setInternalBuilderField(builder, "buildConfiguration", stubBuildConfiguration(project));
return builder;
}

private static IBuildConfiguration stubBuildConfiguration(IProject project) {
return (IBuildConfiguration) Proxy.newProxyInstance(IBuildConfiguration.class.getClassLoader(),
new Class<?>[] { IBuildConfiguration.class }, (proxy, method, args) -> {
if ("getProject".equals(method.getName())) {
return project;
}
return defaultReturnValue(method.getReturnType());
});
}

private static IProject stubProject(String path) {
IPath fullPath = IPath.fromPortableString(path);
return (IProject) Proxy.newProxyInstance(IProject.class.getClassLoader(),
new Class<?>[] { IProject.class }, (proxy, method, args) -> {
if ("getFullPath".equals(method.getName())) {
return fullPath;
}
return defaultReturnValue(method.getReturnType());
});
}

private static Object defaultReturnValue(Class<?> returnType) {
if (!returnType.isPrimitive()) {
return null;
}
if (returnType == boolean.class) {
return Boolean.FALSE;
}
if (returnType == void.class) {
return null;
}
return 0;
}

private static void setInternalBuilderField(InternalBuilder target, String name, Object value) throws Exception {
Field field = InternalBuilder.class.getDeclaredField(name);
field.setAccessible(true);
field.set(target, value);
}

private static IllegalArgumentException invokeHandleRuleConflict(boolean beginRule, InternalBuilder builder,
IllegalArgumentException original) throws Exception {
Method method = BuildManager.class.getDeclaredMethod("handleRuleConflict", boolean.class, InternalBuilder.class,
IllegalArgumentException.class);
method.setAccessible(true);
try {
return (IllegalArgumentException) method.invoke(null, beginRule, builder, original);
} catch (java.lang.reflect.InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException re) {
throw re;
}
throw new RuntimeException(cause);
}
}
}
Loading