From 794caaf88155f5dfae7da7bcfcced11b994ca233 Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Thu, 11 Dec 2025 08:48:47 +0800 Subject: [PATCH 1/2] Fix unit test execution error issue --- ...azelClasspathContainerRuntimeResolver.java | 103 ++++++++++++++++-- .../core/classpath/BazelClasspathManager.java | 82 +++++++++++++- 2 files changed, 174 insertions(+), 11 deletions(-) diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/classpath/BazelClasspathContainerRuntimeResolver.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/classpath/BazelClasspathContainerRuntimeResolver.java index d7051bda..9e47f9aa 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/classpath/BazelClasspathContainerRuntimeResolver.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/classpath/BazelClasspathContainerRuntimeResolver.java @@ -243,6 +243,18 @@ private boolean populateWithSavedContainer(IJavaProject project, ContainerResolu case IClasspathEntry.CPE_PROJECT: { // projects need to be resolved properly so we have all the output folders and exported jars on the classpath var sourceProject = workspaceRoot.getProject(e.getPath().segment(0)); + + // Check for cycles BEFORE calling beginResolvingProject to avoid depth counter issues + if (resolutionContext.processedProjects.contains(sourceProject)) { + if (LOG.isDebugEnabled()) { + LOG.debug( + "Skipping already processed project '{}' in thread '{}' to avoid cycle", + sourceProject.getName(), + Thread.currentThread().getName()); + } + break; + } + if (resolutionContext.beginResolvingProjectIfNeverProcessedBefore(sourceProject)) { try { // only resolve and add the projects if it was never attempted before @@ -251,12 +263,6 @@ private boolean populateWithSavedContainer(IJavaProject project, ContainerResolu // remove from stack again when done resolving resolutionContext.endResolvingProject(sourceProject); } - } else if (LOG.isDebugEnabled()) { - // this should not happen in theory because Bazel is an acyclic graph as well as Eclipse doesn't like it but who knows... - LOG.debug( - "Skipping recursive resolution attempt for project '{}' in thread '{}' ({})", - sourceProject, - Thread.currentThread().getName()); } break; } @@ -275,9 +281,74 @@ private boolean populateWithSavedContainer(IJavaProject project, ContainerResolu } } + // Add the project's own output folders to the runtime classpath + // This ensures that Eclipse-compiled classes (including test classes) are available at runtime + addProjectOutputFolders(project, resolutionContext); + + // Note: Test framework dependencies (like JUnit) are now pre-cached in the container during + // the container build phase (see BazelClasspathManager.saveAndSetContainer), so we no longer + // need to resolve them at runtime. This significantly improves performance for large projects. + return true; } + /** + * Checks if the given project is a test project based on naming conventions. + * + * Note: This method is kept for potential future use, but test framework dependencies are now handled during + * container creation rather than runtime resolution. + * + * @param project + * the project to check + * @return true if this appears to be a test project, false otherwise + */ + @SuppressWarnings("unused") + private boolean isTestProject(IJavaProject project) { + var projectName = project.getProject().getName(); + // Check if project name contains "test" or ends with "-test" + return projectName.contains("test") || projectName.contains("Test"); + } + + /** + * Adds the project's own output folders to the runtime classpath. This includes both the regular output folder + * (eclipse-bin) and the test output folder (eclipse-testbin). + * + * @param project + * the project whose output folders should be added + * @param resolutionContext + * the resolution context + * @throws CoreException + */ + private void addProjectOutputFolders(IJavaProject project, ContainerResolutionContext resolutionContext) + throws CoreException { + // Add the project's default output location (main compiled classes) + var defaultOutputLocation = project.getOutputLocation(); + if (defaultOutputLocation != null) { + var outputEntry = JavaRuntime.newArchiveRuntimeClasspathEntry(defaultOutputLocation); + outputEntry.setClasspathProperty(IRuntimeClasspathEntry.USER_CLASSES); + resolutionContext.add(outputEntry); + if (LOG.isDebugEnabled()) { + LOG.debug("Added default output location to runtime classpath: {}", defaultOutputLocation); + } + } + + // For Bazel projects, also add the test output folder explicitly + var bazelProject = BazelCore.create(project.getProject()); + if (bazelProject != null) { + var fileSystemMapper = bazelProject.getBazelWorkspace().getBazelProjectFileSystemMapper(); + var testOutputFolder = fileSystemMapper.getOutputFolderForTests(bazelProject); + // Test output folder might be different from the default output location + if (!testOutputFolder.getFullPath().equals(defaultOutputLocation)) { + var testOutputEntry = JavaRuntime.newArchiveRuntimeClasspathEntry(testOutputFolder.getFullPath()); + testOutputEntry.setClasspathProperty(IRuntimeClasspathEntry.USER_CLASSES); + resolutionContext.add(testOutputEntry); + if (LOG.isDebugEnabled()) { + LOG.debug("Added test output folder to runtime classpath: {}", testOutputFolder.getFullPath()); + } + } + } + } + @Override public IRuntimeClasspathEntry[] resolveRuntimeClasspathEntry(IRuntimeClasspathEntry entry, IJavaProject project) throws CoreException { @@ -300,15 +371,31 @@ public IRuntimeClasspathEntry[] resolveRuntimeClasspathEntry(IRuntimeClasspathEn // this method can be entered recursively; luckily only within the same thread // therefore we use a ThreadLocal LinkedHashSet to keep track of recursive attempts var resolutionContext = currentThreadResolutionContet.get(); + + // CRITICAL: Check if this is top-level BEFORE incrementing depth + // This ensures ThreadLocal cleanup happens correctly + var isTopLevelResolution = resolutionContext.currentDepth == 0; + + // Check for recursive resolution BEFORE calling beginResolvingProject + // to avoid depth counter mismatch if (!resolutionContext.beginResolvingProjectIfNeverProcessedBefore(project.getProject())) { LOG.warn( - "Detected recursive resolution attempt for project '{}' in thread '{}' ({})", + "Detected recursive resolution attempt for project '{}' in thread '{}' - skipping to avoid cycle", + project.getProject().getName(), + Thread.currentThread().getName()); + return new IRuntimeClasspathEntry[0]; + } + + // Now safe to increment depth counter + if (!resolutionContext.beginResolvingProject(project.getProject())) { + // This should never happen now due to the check above, but keep as safety net + LOG.error( + "Unexpected state: beginResolvingProject returned false after cycle check for project '{}' in thread '{}'", project.getProject().getName(), Thread.currentThread().getName()); return new IRuntimeClasspathEntry[0]; } - var isTopLevelResolution = resolutionContext.currentDepth == 0; var stopWatch = StopWatch.startNewStopWatch(); try { // try the saved container diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/classpath/BazelClasspathManager.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/classpath/BazelClasspathManager.java index f60cf35a..15a21705 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/classpath/BazelClasspathManager.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/classpath/BazelClasspathManager.java @@ -239,8 +239,8 @@ TargetProvisioningStrategy getTargetProvisioningStrategy(BazelWorkspace bazelWor * @param monitor * @throws CoreException */ - public void patchClasspathContainer(BazelProject bazelProject, CompileAndRuntimeClasspath classpath, IProgressMonitor progress) - throws CoreException { + public void patchClasspathContainer(BazelProject bazelProject, CompileAndRuntimeClasspath classpath, + IProgressMonitor progress) throws CoreException { var monitor = SubMonitor.convert(progress); try { monitor.beginTask("Patchig classpath: " + bazelProject.getName(), 2); @@ -347,7 +347,14 @@ void saveAndSetContainer(IJavaProject javaProject, CompileAndRuntimeClasspath cl : new Path(BazelCoreSharedContstants.CLASSPATH_CONTAINER_ID); var sourceAttachmentProperties = getSourceAttachmentProperties(javaProject.getProject()); - var transativeClasspath = classpath.additionalRuntimeEntries().stream().map(ClasspathEntry::build).collect(toList()); + var transativeClasspath = + classpath.additionalRuntimeEntries().stream().map(ClasspathEntry::build).collect(toList()); + + // For test projects, include test framework dependencies (like JUnit) in the container + // This caches them upfront, avoiding runtime resolution overhead + if (isTestProject(javaProject)) { + addTestFrameworkDependenciesToContainer(javaProject, transativeClasspath); + } var container = new BazelClasspathContainer( path, @@ -376,6 +383,75 @@ private void saveContainerState(IProject project, BazelClasspathContainer contai } } + /** + * Checks if the given project is a test project based on naming conventions. + * + * @param project + * the project to check + * @return true if this appears to be a test project, false otherwise + */ + private boolean isTestProject(IJavaProject project) { + var projectName = project.getProject().getName(); + return projectName.contains("test") || projectName.contains("Test"); + } + + /** + * Adds test framework dependencies (like JUnit) to the container's runtime classpath. This pre-caches test + * dependencies in the container, avoiding runtime resolution overhead. + * + * @param project + * the test project + * @param transativeClasspath + * the mutable list of runtime classpath entries to augment + */ + private void addTestFrameworkDependenciesToContainer(IJavaProject project, + List transativeClasspath) { + try { + // Collect existing paths to avoid duplicates + var existingPaths = new LinkedHashSet(); + for (IClasspathEntry entry : transativeClasspath) { + existingPaths.add(entry.getPath()); + } + + // Only process direct classpath entries (containers like JUnit) + // Skip project references to avoid circular dependencies + var rawClasspath = project.getRawClasspath(); + for (IClasspathEntry entry : rawClasspath) { + if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) { + // Skip the Bazel container itself to avoid duplication + if (BazelCoreSharedContstants.CLASSPATH_CONTAINER_ID.equals(entry.getPath().toString())) { + continue; + } + + // Get container contents (e.g., JUnit, JRE) + var container = JavaCore.getClasspathContainer(entry.getPath(), project); + if (container != null) { + for (IClasspathEntry containerEntry : container.getClasspathEntries()) { + if (containerEntry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) { + var libPath = containerEntry.getPath(); + if (!existingPaths.contains(libPath)) { + // Add test framework JAR to runtime classpath + transativeClasspath.add(containerEntry); + existingPaths.add(libPath); + + if (LOG.isDebugEnabled()) { + LOG.debug("Added test framework to container: {}", libPath); + } + } + } + } + } + } + } + } catch (Exception e) { + LOG.warn( + "Failed to add test framework dependencies to container for project '{}': {}", + project.getProject().getName(), + e.getMessage()); + // Don't fail container creation if this step fails + } + } + /** * Updates the classpath of multiple projects belonging to a single {@link BazelWorkspace}. *

From 4e3ea4e66446b148875835511ff73b19514cef10 Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Mon, 5 Jan 2026 15:11:00 +0800 Subject: [PATCH 2/2] fix: only increment depth counter for new projects in cycle detection Fixed bug where currentDepth was unconditionally incremented in beginResolvingProject(), causing potential counter mismatches. Now only increments when project is actually added to processedProjects. Removed redundant cycle checks in calling code since the method now handles duplicate projects correctly on its own. --- .../BazelClasspathContainerRuntimeResolver.java | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/classpath/BazelClasspathContainerRuntimeResolver.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/classpath/BazelClasspathContainerRuntimeResolver.java index 9e47f9aa..25703f90 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/classpath/BazelClasspathContainerRuntimeResolver.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/classpath/BazelClasspathContainerRuntimeResolver.java @@ -263,6 +263,11 @@ private boolean populateWithSavedContainer(IJavaProject project, ContainerResolu // remove from stack again when done resolving resolutionContext.endResolvingProject(sourceProject); } + } else if (LOG.isDebugEnabled()) { + LOG.debug( + "Skipping already processed project '{}' in thread '{}' to avoid cycle", + sourceProject.getName(), + Thread.currentThread().getName()); } break; } @@ -372,7 +377,7 @@ public IRuntimeClasspathEntry[] resolveRuntimeClasspathEntry(IRuntimeClasspathEn // therefore we use a ThreadLocal LinkedHashSet to keep track of recursive attempts var resolutionContext = currentThreadResolutionContet.get(); - // CRITICAL: Check if this is top-level BEFORE incrementing depth + // CRITICAL: Check if this is top-level BEFORE calling beginResolvingProject // This ensures ThreadLocal cleanup happens correctly var isTopLevelResolution = resolutionContext.currentDepth == 0; @@ -386,16 +391,6 @@ public IRuntimeClasspathEntry[] resolveRuntimeClasspathEntry(IRuntimeClasspathEn return new IRuntimeClasspathEntry[0]; } - // Now safe to increment depth counter - if (!resolutionContext.beginResolvingProject(project.getProject())) { - // This should never happen now due to the check above, but keep as safety net - LOG.error( - "Unexpected state: beginResolvingProject returned false after cycle check for project '{}' in thread '{}'", - project.getProject().getName(), - Thread.currentThread().getName()); - return new IRuntimeClasspathEntry[0]; - } - var stopWatch = StopWatch.startNewStopWatch(); try { // try the saved container