Skip to content

Commit 6d5cb92

Browse files
committed
Fix unit test execution error issue
1 parent 9542833 commit 6d5cb92

File tree

2 files changed

+175
-12
lines changed

2 files changed

+175
-12
lines changed

bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/classpath/BazelClasspathContainerRuntimeResolver.java

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,18 @@ private boolean populateWithSavedContainer(IJavaProject project, ContainerResolu
232232
case IClasspathEntry.CPE_PROJECT: {
233233
// projects need to be resolved properly so we have all the output folders and exported jars on the classpath
234234
var sourceProject = workspaceRoot.getProject(e.getPath().segment(0));
235+
236+
// Check for cycles BEFORE calling beginResolvingProject to avoid depth counter issues
237+
if (resolutionContext.processedProjects.contains(sourceProject)) {
238+
if (LOG.isDebugEnabled()) {
239+
LOG.debug(
240+
"Skipping already processed project '{}' in thread '{}' to avoid cycle",
241+
sourceProject.getName(),
242+
Thread.currentThread().getName());
243+
}
244+
break;
245+
}
246+
235247
if (resolutionContext.beginResolvingProject(sourceProject)) {
236248
try {
237249
// only resolve and add the projects if it was never attempted before
@@ -240,12 +252,6 @@ private boolean populateWithSavedContainer(IJavaProject project, ContainerResolu
240252
// remove from stack again when done resolving
241253
resolutionContext.endResolvingProject(sourceProject);
242254
}
243-
} else if (LOG.isDebugEnabled()) {
244-
// this should not happen in theory because Bazel is an acyclic graph as well as Eclipse doesn't like it but who knows...
245-
LOG.debug(
246-
"Skipping recursive resolution attempt for project '{}' in thread '{}' ({})",
247-
sourceProject,
248-
Thread.currentThread().getName());
249255
}
250256
break;
251257
}
@@ -264,9 +270,74 @@ private boolean populateWithSavedContainer(IJavaProject project, ContainerResolu
264270
}
265271
}
266272

273+
// Add the project's own output folders to the runtime classpath
274+
// This ensures that Eclipse-compiled classes (including test classes) are available at runtime
275+
addProjectOutputFolders(project, resolutionContext);
276+
277+
// Note: Test framework dependencies (like JUnit) are now pre-cached in the container during
278+
// the container build phase (see BazelClasspathManager.saveAndSetContainer), so we no longer
279+
// need to resolve them at runtime. This significantly improves performance for large projects.
280+
267281
return true;
268282
}
269283

284+
/**
285+
* Checks if the given project is a test project based on naming conventions.
286+
*
287+
* Note: This method is kept for potential future use, but test framework dependencies are now handled during
288+
* container creation rather than runtime resolution.
289+
*
290+
* @param project
291+
* the project to check
292+
* @return <code>true</code> if this appears to be a test project, <code>false</code> otherwise
293+
*/
294+
@SuppressWarnings("unused")
295+
private boolean isTestProject(IJavaProject project) {
296+
var projectName = project.getProject().getName();
297+
// Check if project name contains "test" or ends with "-test"
298+
return projectName.contains("test") || projectName.contains("Test");
299+
}
300+
301+
/**
302+
* Adds the project's own output folders to the runtime classpath. This includes both the regular output folder
303+
* (eclipse-bin) and the test output folder (eclipse-testbin).
304+
*
305+
* @param project
306+
* the project whose output folders should be added
307+
* @param resolutionContext
308+
* the resolution context
309+
* @throws CoreException
310+
*/
311+
private void addProjectOutputFolders(IJavaProject project, ContainerResolutionContext resolutionContext)
312+
throws CoreException {
313+
// Add the project's default output location (main compiled classes)
314+
var defaultOutputLocation = project.getOutputLocation();
315+
if (defaultOutputLocation != null) {
316+
var outputEntry = JavaRuntime.newArchiveRuntimeClasspathEntry(defaultOutputLocation);
317+
outputEntry.setClasspathProperty(IRuntimeClasspathEntry.USER_CLASSES);
318+
resolutionContext.add(outputEntry);
319+
if (LOG.isDebugEnabled()) {
320+
LOG.debug("Added default output location to runtime classpath: {}", defaultOutputLocation);
321+
}
322+
}
323+
324+
// For Bazel projects, also add the test output folder explicitly
325+
var bazelProject = BazelCore.create(project.getProject());
326+
if (bazelProject != null) {
327+
var fileSystemMapper = bazelProject.getBazelWorkspace().getBazelProjectFileSystemMapper();
328+
var testOutputFolder = fileSystemMapper.getOutputFolderForTests(bazelProject);
329+
// Test output folder might be different from the default output location
330+
if (!testOutputFolder.getFullPath().equals(defaultOutputLocation)) {
331+
var testOutputEntry = JavaRuntime.newArchiveRuntimeClasspathEntry(testOutputFolder.getFullPath());
332+
testOutputEntry.setClasspathProperty(IRuntimeClasspathEntry.USER_CLASSES);
333+
resolutionContext.add(testOutputEntry);
334+
if (LOG.isDebugEnabled()) {
335+
LOG.debug("Added test output folder to runtime classpath: {}", testOutputFolder.getFullPath());
336+
}
337+
}
338+
}
339+
}
340+
270341
@Override
271342
public IRuntimeClasspathEntry[] resolveRuntimeClasspathEntry(IRuntimeClasspathEntry entry, IJavaProject project)
272343
throws CoreException {
@@ -289,15 +360,31 @@ public IRuntimeClasspathEntry[] resolveRuntimeClasspathEntry(IRuntimeClasspathEn
289360
// this method can be entered recursively; luckily only within the same thread
290361
// therefore we use a ThreadLocal LinkedHashSet to keep track of recursive attempts
291362
var resolutionContext = currentThreadResolutionContet.get();
292-
if (!resolutionContext.beginResolvingProject(project.getProject())) {
363+
364+
// CRITICAL: Check if this is top-level BEFORE incrementing depth
365+
// This ensures ThreadLocal cleanup happens correctly
366+
var isTopLevelResolution = resolutionContext.currentDepth == 0;
367+
368+
// Check for recursive resolution BEFORE calling beginResolvingProject
369+
// to avoid depth counter mismatch
370+
if (resolutionContext.processedProjects.contains(project.getProject())) {
293371
LOG.warn(
294-
"Detected recursive resolution attempt for project '{}' in thread '{}' ({})",
372+
"Detected recursive resolution attempt for project '{}' in thread '{}' - skipping to avoid cycle",
373+
project.getProject().getName(),
374+
Thread.currentThread().getName());
375+
return new IRuntimeClasspathEntry[0];
376+
}
377+
378+
// Now safe to increment depth counter
379+
if (!resolutionContext.beginResolvingProject(project.getProject())) {
380+
// This should never happen now due to the check above, but keep as safety net
381+
LOG.error(
382+
"Unexpected state: beginResolvingProject returned false after cycle check for project '{}' in thread '{}'",
295383
project.getProject().getName(),
296384
Thread.currentThread().getName());
297385
return new IRuntimeClasspathEntry[0];
298386
}
299387

300-
var isTopLevelResolution = resolutionContext.currentDepth == 0;
301388
var stopWatch = StopWatch.startNewStopWatch();
302389
try {
303390
// try the saved container

bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/classpath/BazelClasspathManager.java

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,8 +239,8 @@ TargetProvisioningStrategy getTargetProvisioningStrategy(BazelWorkspace bazelWor
239239
* @param monitor
240240
* @throws CoreException
241241
*/
242-
public void patchClasspathContainer(BazelProject bazelProject, CompileAndRuntimeClasspath classpath, IProgressMonitor progress)
243-
throws CoreException {
242+
public void patchClasspathContainer(BazelProject bazelProject, CompileAndRuntimeClasspath classpath,
243+
IProgressMonitor progress) throws CoreException {
244244
var monitor = SubMonitor.convert(progress);
245245
try {
246246
monitor.beginTask("Patchig classpath: " + bazelProject.getName(), 2);
@@ -347,7 +347,14 @@ void saveAndSetContainer(IJavaProject javaProject, CompileAndRuntimeClasspath cl
347347
: new Path(BazelCoreSharedContstants.CLASSPATH_CONTAINER_ID);
348348

349349
var sourceAttachmentProperties = getSourceAttachmentProperties(javaProject.getProject());
350-
var transativeClasspath = classpath.additionalRuntimeEntries().stream().map(ClasspathEntry::build).collect(toList());
350+
var transativeClasspath =
351+
classpath.additionalRuntimeEntries().stream().map(ClasspathEntry::build).collect(toList());
352+
353+
// For test projects, include test framework dependencies (like JUnit) in the container
354+
// This caches them upfront, avoiding runtime resolution overhead
355+
if (isTestProject(javaProject)) {
356+
addTestFrameworkDependenciesToContainer(javaProject, transativeClasspath);
357+
}
351358

352359
var container = new BazelClasspathContainer(
353360
path,
@@ -376,6 +383,75 @@ private void saveContainerState(IProject project, BazelClasspathContainer contai
376383
}
377384
}
378385

386+
/**
387+
* Checks if the given project is a test project based on naming conventions.
388+
*
389+
* @param project
390+
* the project to check
391+
* @return <code>true</code> if this appears to be a test project, <code>false</code> otherwise
392+
*/
393+
private boolean isTestProject(IJavaProject project) {
394+
var projectName = project.getProject().getName();
395+
return projectName.contains("test") || projectName.contains("Test");
396+
}
397+
398+
/**
399+
* Adds test framework dependencies (like JUnit) to the container's runtime classpath. This pre-caches test
400+
* dependencies in the container, avoiding runtime resolution overhead.
401+
*
402+
* @param project
403+
* the test project
404+
* @param transativeClasspath
405+
* the mutable list of runtime classpath entries to augment
406+
*/
407+
private void addTestFrameworkDependenciesToContainer(IJavaProject project,
408+
List<IClasspathEntry> transativeClasspath) {
409+
try {
410+
// Collect existing paths to avoid duplicates
411+
var existingPaths = new LinkedHashSet<IPath>();
412+
for (IClasspathEntry entry : transativeClasspath) {
413+
existingPaths.add(entry.getPath());
414+
}
415+
416+
// Only process direct classpath entries (containers like JUnit)
417+
// Skip project references to avoid circular dependencies
418+
var rawClasspath = project.getRawClasspath();
419+
for (IClasspathEntry entry : rawClasspath) {
420+
if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
421+
// Skip the Bazel container itself to avoid duplication
422+
if (BazelCoreSharedContstants.CLASSPATH_CONTAINER_ID.equals(entry.getPath().toString())) {
423+
continue;
424+
}
425+
426+
// Get container contents (e.g., JUnit, JRE)
427+
var container = JavaCore.getClasspathContainer(entry.getPath(), project);
428+
if (container != null) {
429+
for (IClasspathEntry containerEntry : container.getClasspathEntries()) {
430+
if (containerEntry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) {
431+
var libPath = containerEntry.getPath();
432+
if (!existingPaths.contains(libPath)) {
433+
// Add test framework JAR to runtime classpath
434+
transativeClasspath.add(containerEntry);
435+
existingPaths.add(libPath);
436+
437+
if (LOG.isDebugEnabled()) {
438+
LOG.debug("Added test framework to container: {}", libPath);
439+
}
440+
}
441+
}
442+
}
443+
}
444+
}
445+
}
446+
} catch (Exception e) {
447+
LOG.warn(
448+
"Failed to add test framework dependencies to container for project '{}': {}",
449+
project.getProject().getName(),
450+
e.getMessage());
451+
// Don't fail container creation if this step fails
452+
}
453+
}
454+
379455
/**
380456
* Updates the classpath of multiple projects belonging to a single {@link BazelWorkspace}.
381457
* <p>

0 commit comments

Comments
 (0)