Skip to content

Commit 985256c

Browse files
committed
Enhance stack trace information for rule conflicts in BuildManager
Fixes #2278 When a builder attempts to begin or end a scheduling rule that doesn't match the outer scope rule, the error message now includes: - Builder name and label - Builder class name - Plugin ID - Project name - The conflicting rule This makes it much easier to identify which builder is causing the rule mismatch, especially in complex build scenarios with multiple builders. The enhancement catches IllegalArgumentException at the point where beginRule() and endRule() are called in BuildManager.basicBuild() and wraps it with detailed context before re-throwing.
1 parent fe43e26 commit 985256c

3 files changed

Lines changed: 202 additions & 2 deletions

File tree

resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/BuildManager.java

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,11 @@ private void basicBuild(int trigger, IncrementalProjectBuilder builder, Map<Stri
288288
depth = getWorkManager().beginUnprotected();
289289
// Acquire the rule required for running this builder
290290
if (rule != null) {
291-
Job.getJobManager().beginRule(rule, monitor);
291+
try {
292+
Job.getJobManager().beginRule(rule, monitor);
293+
} catch (IllegalArgumentException e) {
294+
throw handleRuleConflict(true, currentBuilder, e);
295+
}
292296
// Now that we've acquired the rule, changes may have been made concurrently, ensure we're pointing at the
293297
// correct currentTree so delta contains concurrent changes made in areas guarded by the scheduling rule
294298
if (currentTree != null) {
@@ -303,7 +307,11 @@ private void basicBuild(int trigger, IncrementalProjectBuilder builder, Map<Stri
303307
getWorkManager().endUnprotected(depth);
304308
}
305309
if (rule != null) {
306-
Job.getJobManager().endRule(rule);
310+
try {
311+
Job.getJobManager().endRule(rule);
312+
} catch (IllegalArgumentException e) {
313+
throw handleRuleConflict(false, currentBuilder, e);
314+
}
307315
}
308316
// Be sure to clean up after ourselves.
309317
if (clean || currentBuilder.wasForgetStateRequested()) {
@@ -395,6 +403,35 @@ protected void basicBuild(IBuildConfiguration buildConfiguration, final int trig
395403
}
396404
}
397405

406+
/**
407+
* Wraps an {@link IllegalArgumentException} thrown by
408+
* {@link org.eclipse.core.runtime.jobs.IJobManager#beginRule(ISchedulingRule, IProgressMonitor)
409+
* beginRule} or
410+
* {@link org.eclipse.core.runtime.jobs.IJobManager#endRule(ISchedulingRule)
411+
* endRule} with builder context (label, class, plugin id, project) so the
412+
* offending builder can be identified from the stack trace. The conflicting
413+
* rule and the underlying error are preserved via the wrapped cause.
414+
*
415+
* @param beginRule {@code true} if the failure occurred during
416+
* {@code beginRule}, {@code false} for {@code endRule}
417+
*/
418+
private static IllegalArgumentException handleRuleConflict(boolean beginRule, InternalBuilder currentBuilder,
419+
IllegalArgumentException e) {
420+
String label = currentBuilder.getLabel();
421+
String pluginId = currentBuilder.getPluginId();
422+
IProject project = currentBuilder.getProject();
423+
String projectName = project != null ? project.getFullPath().toString() : "<unknown>"; //$NON-NLS-1$
424+
String op = beginRule ? "beginRule" : "endRule"; //$NON-NLS-1$ //$NON-NLS-2$
425+
String enhancedMessage = String.format("%s failed for builder %s ('%s', plugin %s) on project %s: %s", //$NON-NLS-1$
426+
op,
427+
currentBuilder.getClass().getName(),
428+
label != null ? label : "<unknown>", //$NON-NLS-1$
429+
pluginId != null ? pluginId : "<unknown>", //$NON-NLS-1$
430+
projectName,
431+
e.getMessage());
432+
return new IllegalArgumentException(enhancedMessage, e);
433+
}
434+
398435
/**
399436
* Runs all builders on the given project config.
400437
* @return A status indicating if the build succeeded or failed

resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/events/AllEventsTests.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
@Suite
2020
@SelectClasses({ //
21+
BuildManagerRuleConflictMessageTest.class, //
2122
BuildProjectFromMultipleJobsTest.class, //
2223
})
2324
public class AllEventsTests {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Vogella GmbH and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Lars Vogel <Lars.Vogel@vogella.com> - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.core.tests.internal.events;
15+
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
import static org.junit.jupiter.api.Assertions.assertSame;
18+
19+
import java.lang.reflect.Field;
20+
import java.lang.reflect.Method;
21+
import java.lang.reflect.Proxy;
22+
import java.util.Map;
23+
24+
import org.eclipse.core.internal.events.BuildManager;
25+
import org.eclipse.core.internal.events.InternalBuilder;
26+
import org.eclipse.core.resources.IBuildConfiguration;
27+
import org.eclipse.core.resources.IProject;
28+
import org.eclipse.core.resources.IncrementalProjectBuilder;
29+
import org.eclipse.core.runtime.IPath;
30+
import org.eclipse.core.runtime.IProgressMonitor;
31+
import org.junit.jupiter.api.Test;
32+
33+
/**
34+
* Verifies the format of the enhanced {@link IllegalArgumentException} that
35+
* {@code BuildManager} throws when a builder hits a scheduling-rule conflict
36+
* during {@code beginRule} or {@code endRule}. The wrapped message has to
37+
* identify the offending builder so it can be diagnosed from a stack trace.
38+
*/
39+
public class BuildManagerRuleConflictMessageTest {
40+
41+
private static final String BUILDER_LABEL = "My Awesome Builder";
42+
private static final String PLUGIN_ID = "com.example.builders";
43+
private static final String PROJECT_PATH = "/MyProject";
44+
45+
@Test
46+
public void beginRuleConflictMessageIdentifiesOffendingBuilder() throws Exception {
47+
InternalBuilder builder = newStubBuilder(stubProject(PROJECT_PATH));
48+
IllegalArgumentException original = new IllegalArgumentException(
49+
"Attempted to beginRule: P/MyProject/foo, does not match outer scope rule: P/Other");
50+
51+
IllegalArgumentException result = invokeHandleRuleConflict(true, builder, original);
52+
53+
assertThat(result.getMessage()) //
54+
.startsWith("beginRule failed for builder " + builder.getClass().getName()) //
55+
.contains("'" + BUILDER_LABEL + "'") //
56+
.contains("plugin " + PLUGIN_ID) //
57+
.contains("on project " + PROJECT_PATH) //
58+
.endsWith(": " + original.getMessage());
59+
assertSame(original, result.getCause());
60+
}
61+
62+
@Test
63+
public void endRuleConflictMessageIdentifiesOffendingBuilder() throws Exception {
64+
InternalBuilder builder = newStubBuilder(stubProject(PROJECT_PATH));
65+
IllegalArgumentException original = new IllegalArgumentException(
66+
"Attempted to endRule: P/MyProject/foo, does not match the rule of current entry: P/MyProject");
67+
68+
IllegalArgumentException result = invokeHandleRuleConflict(false, builder, original);
69+
70+
assertThat(result.getMessage()) //
71+
.startsWith("endRule failed for builder " + builder.getClass().getName()) //
72+
.contains("'" + BUILDER_LABEL + "'") //
73+
.contains("plugin " + PLUGIN_ID) //
74+
.contains("on project " + PROJECT_PATH) //
75+
.endsWith(": " + original.getMessage());
76+
assertSame(original, result.getCause());
77+
}
78+
79+
@Test
80+
public void messageFallsBackToPlaceholdersWhenBuilderMetadataMissing() throws Exception {
81+
InternalBuilder builder = newStubBuilder(null);
82+
IllegalArgumentException original = new IllegalArgumentException("boom");
83+
setInternalBuilderField(builder, "label", null);
84+
setInternalBuilderField(builder, "pluginId", null);
85+
86+
IllegalArgumentException result = invokeHandleRuleConflict(true, builder, original);
87+
88+
assertThat(result.getMessage()) //
89+
.contains("'<unknown>'") //
90+
.contains("plugin <unknown>") //
91+
.contains("on project <unknown>");
92+
}
93+
94+
private static InternalBuilder newStubBuilder(IProject project) throws Exception {
95+
IncrementalProjectBuilder builder = new IncrementalProjectBuilder() {
96+
@Override
97+
protected IProject[] build(int kind, Map<String, String> args, IProgressMonitor monitor) {
98+
return null;
99+
}
100+
};
101+
setInternalBuilderField(builder, "label", BUILDER_LABEL);
102+
setInternalBuilderField(builder, "pluginId", PLUGIN_ID);
103+
setInternalBuilderField(builder, "buildConfiguration", stubBuildConfiguration(project));
104+
return builder;
105+
}
106+
107+
private static IBuildConfiguration stubBuildConfiguration(IProject project) {
108+
return (IBuildConfiguration) Proxy.newProxyInstance(IBuildConfiguration.class.getClassLoader(),
109+
new Class<?>[] { IBuildConfiguration.class }, (proxy, method, args) -> {
110+
if ("getProject".equals(method.getName())) {
111+
return project;
112+
}
113+
return defaultReturnValue(method.getReturnType());
114+
});
115+
}
116+
117+
private static IProject stubProject(String path) {
118+
IPath fullPath = IPath.fromPortableString(path);
119+
return (IProject) Proxy.newProxyInstance(IProject.class.getClassLoader(),
120+
new Class<?>[] { IProject.class }, (proxy, method, args) -> {
121+
if ("getFullPath".equals(method.getName())) {
122+
return fullPath;
123+
}
124+
return defaultReturnValue(method.getReturnType());
125+
});
126+
}
127+
128+
private static Object defaultReturnValue(Class<?> returnType) {
129+
if (!returnType.isPrimitive()) {
130+
return null;
131+
}
132+
if (returnType == boolean.class) {
133+
return Boolean.FALSE;
134+
}
135+
if (returnType == void.class) {
136+
return null;
137+
}
138+
return 0;
139+
}
140+
141+
private static void setInternalBuilderField(InternalBuilder target, String name, Object value) throws Exception {
142+
Field field = InternalBuilder.class.getDeclaredField(name);
143+
field.setAccessible(true);
144+
field.set(target, value);
145+
}
146+
147+
private static IllegalArgumentException invokeHandleRuleConflict(boolean beginRule, InternalBuilder builder,
148+
IllegalArgumentException original) throws Exception {
149+
Method method = BuildManager.class.getDeclaredMethod("handleRuleConflict", boolean.class, InternalBuilder.class,
150+
IllegalArgumentException.class);
151+
method.setAccessible(true);
152+
try {
153+
return (IllegalArgumentException) method.invoke(null, beginRule, builder, original);
154+
} catch (java.lang.reflect.InvocationTargetException e) {
155+
Throwable cause = e.getCause();
156+
if (cause instanceof RuntimeException re) {
157+
throw re;
158+
}
159+
throw new RuntimeException(cause);
160+
}
161+
}
162+
}

0 commit comments

Comments
 (0)