diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 259fdcb..015573f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,4 +63,4 @@ jobs: - name: Maven compile run: mvn compile -Pci - name: Maven test - run: mvn test -Djava.library.path=../target/release -Pci -Dtest='!BazelClasspathContainerTest,!BazelProjectViewTest' + run: mvn test -Djava.library.path=../target/release -Pci -Dtest='!BazelClasspathContainerTest,!BazelProjectViewTest,!BazelProjectCreatorTest' diff --git a/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs b/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs index 2602ca8..9e090e9 100644 --- a/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs +++ b/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs @@ -396,6 +396,31 @@ pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeQueryTargets( } } +#[no_mangle] +pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeIsTestTarget( + mut env: JNIEnv, + _class: JClass, + handle: jlong, + target_label: JString, +) -> jboolean { + let state = match get_state(&mut env, handle) { + Some(s) => s, + None => return 0, + }; + let label: String = match env.get_string(&target_label) { + Ok(s) => s.into(), + Err(_) => return 0, + }; + let graph = state.graph.lock().unwrap_or_else(|e| e.into_inner()); + let is_test = graph.get_target_kind(&label) == bazel_graph::TargetKind::JavaTest; + drop(graph); + if is_test { + 1 + } else { + 0 + } +} + #[no_mangle] pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativePopulateGraph( mut env: JNIEnv, diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelBridge.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelBridge.java index 445bd39..b1c2e0a 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelBridge.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelBridge.java @@ -85,6 +85,15 @@ public void shutdown() { } } + public String getWorkspacePath() { + rwLock.readLock().lock(); + try { + return lastWorkspacePath; + } finally { + rwLock.readLock().unlock(); + } + } + public String[] discoverTargets(String[] scopePatterns) { return discoverTargets(scopePatterns, null); } @@ -381,10 +390,17 @@ private long snapshotHandleNullable() { private native String[] nativeGetTransitiveWorkspaceDeps(long handle, String[] targetLabels); private native String[] nativeSyncIncremental(long handle, String[] changedFilePaths); private native String nativeGetAspectBuildStats(long handle); + private native boolean nativeIsTestTarget(long handle, String targetLabel); public String getAspectBuildStats() { long h = snapshotHandleNullable(); if (h == -1) return null; return nativeGetAspectBuildStats(h); } + + public boolean isTestTarget(String targetLabel) { + long h = snapshotHandleNullable(); + if (h == -1) return false; + return nativeIsTestTarget(h, targetLabel); + } } diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainer.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainer.java index 5d5c908..c631fc8 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainer.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainer.java @@ -77,11 +77,22 @@ private IClasspathEntry parseEntry(String raw) { case "LIB": IPath jarPath = Path.fromPortableString(path); if (!fileExists(path, jarPath)) { - if (WARNED_MISSING_PATHS.add(path)) { - LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", - "Skipping non-existent JAR: " + path)); + String workspacePath = BazelBridge.getInstance().getWorkspacePath(); + if (workspacePath != null) { + String fallback = BazelExternalRepoResolver.resolveFallbackJar( + path, workspacePath); + if (fallback != null) { + jarPath = Path.fromPortableString(fallback); + path = fallback; + } + } + if (!fileExists(path, jarPath)) { + if (WARNED_MISSING_PATHS.add(path)) { + LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", + "Skipping non-existent JAR: " + path)); + } + return null; } - return null; } IPath srcPath = sourcePath != null ? Path.fromPortableString(sourcePath) : null; if (srcPath != null && !fileExists(sourcePath, srcPath)) { @@ -146,6 +157,13 @@ private IClasspathEntry parseEntry(String raw) { } return JavaCore.newProjectEntry(Path.fromPortableString("/" + projectName)); case "SRC": + if (isTest) { + IClasspathAttribute[] testAttrs = new IClasspathAttribute[]{ + JavaCore.newClasspathAttribute(IClasspathAttribute.TEST, "true") + }; + return JavaCore.newSourceEntry(Path.fromPortableString(path), + null, null, null, testAttrs); + } return JavaCore.newSourceEntry(Path.fromPortableString(path)); default: return null; diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java index 729cb2d..a2f25e5 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java @@ -207,9 +207,9 @@ private Object handleBuildTarget(List arguments) { LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", "Pre-debug build complete for " + projectName)); - BazelRuntimeClasspathEntryResolver.clearCacheForProject(projectName); BazelClasspathContainer.resetWarnings(); - BazelClasspathManager.setMergedClasspathContainer(project, false); + BazelClasspathManager.setMergedClasspathContainer(project, true); + BazelRuntimeClasspathEntryResolver.clearCacheForProject(projectName); return null; } catch (Exception e) { diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelExternalRepoResolver.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelExternalRepoResolver.java new file mode 100644 index 0000000..4bf0e54 --- /dev/null +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelExternalRepoResolver.java @@ -0,0 +1,131 @@ +package com.bazel.jdt; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +public final class BazelExternalRepoResolver { + private static final Logger LOG = Logger.getLogger(BazelExternalRepoResolver.class.getName()); + private static final ConcurrentHashMap OUTPUT_BASE_CACHE = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap JAR_FALLBACK_CACHE = new ConcurrentHashMap<>(); + private static final int MAX_JAR_SEARCH_DEPTH = 3; + + private BazelExternalRepoResolver() {} + + static String resolveOutputBase(String workspacePath) { + return OUTPUT_BASE_CACHE.computeIfAbsent(workspacePath, ws -> { + try { + Path bazelOut = new File(ws, "bazel-out").toPath(); + if (Files.exists(bazelOut)) { + Path resolved = bazelOut.toRealPath(); + Path execroot = resolved.getParent(); + if (execroot != null) { + Path execrootParent = execroot.getParent(); + if (execrootParent != null) { + Path outputBase = execrootParent.getParent(); + if (outputBase != null + && Files.isDirectory(outputBase.resolve("external"))) { + String result = outputBase.toString(); + LOG.info("Resolved output_base from bazel-out symlink: " + result); + return result; + } + } + } + } + } catch (IOException e) { + LOG.warning("Failed to resolve bazel-out symlink: " + e.getMessage()); + } + + try { + ProcessBuilder pb = new ProcessBuilder("bazel", "info", "output_base"); + pb.directory(new File(ws)); + pb.redirectErrorStream(true); + Process proc = pb.start(); + String output = new String(proc.getInputStream().readAllBytes()).trim(); + int exitCode = proc.waitFor(); + if (exitCode == 0 && !output.isEmpty() && new File(output).isDirectory()) { + LOG.info("Resolved output_base from bazel info: " + output); + return output; + } + } catch (Exception e) { + LOG.warning("Failed to run bazel info output_base: " + e.getMessage()); + } + + return null; + }); + } + + static String resolveFallbackJar(String missingPath, String workspacePath) { + return JAR_FALLBACK_CACHE.computeIfAbsent(missingPath, path -> { + String outputBase = resolveOutputBase(workspacePath); + if (outputBase == null) return null; + + String repoName = extractRepoName(path); + if (repoName == null) return null; + + File repoDir = new File(outputBase, "external/" + repoName); + if (!repoDir.isDirectory()) { + repoDir = findBzlmodRepoDir(outputBase, repoName); + } + if (repoDir == null || !repoDir.isDirectory()) return null; + + String found = findJarInDirectory(repoDir, MAX_JAR_SEARCH_DEPTH); + if (found != null) { + LOG.info("Fallback JAR resolved: " + path + " -> " + found); + } + return found; + }); + } + + static String extractRepoName(String path) { + int idx = path.indexOf("/external/"); + if (idx < 0) return null; + String afterExternal = path.substring(idx + "/external/".length()); + int slash = afterExternal.indexOf('/'); + if (slash <= 0) return null; + return afterExternal.substring(0, slash); + } + + static File findBzlmodRepoDir(String outputBase, String repoName) { + File externalRoot = new File(outputBase, "external"); + if (!externalRoot.isDirectory()) return null; + File[] candidates = externalRoot.listFiles((dir, name) -> + name.contains("~~") && name.endsWith("~" + repoName)); + if (candidates != null && candidates.length > 0) { + return candidates[0]; + } + return null; + } + + private static String findJarInDirectory(File dir, int maxDepth) { + if (maxDepth <= 0 || !dir.isDirectory()) return null; + File[] files = dir.listFiles(); + if (files == null) return null; + + for (File f : files) { + if (f.isFile() && f.getName().endsWith(".jar") + && !f.getName().endsWith("-sources.jar")) { + return f.getAbsolutePath(); + } + } + for (File f : files) { + if (f.isDirectory()) { + String found = findJarInDirectory(f, maxDepth - 1); + if (found != null) return found; + } + } + return null; + } + + static void setOutputBaseForTest(String workspacePath, String outputBase) { + OUTPUT_BASE_CACHE.put(workspacePath, outputBase); + } + + static void resetCaches() { + OUTPUT_BASE_CACHE.clear(); + JAR_FALLBACK_CACHE.clear(); + } +} diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java index 9f4a62b..01c1590 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java @@ -20,6 +20,7 @@ import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; +import org.eclipse.jdt.core.IClasspathAttribute; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; @@ -37,12 +38,18 @@ private BazelProjectCreator() {} public static IProject createProjectForPackage( String workspacePath, String packagePath, String targetLabel, IProgressMonitor monitor) { - return createProjectForPackage(workspacePath, packagePath, targetLabel, monitor, false); + return createProjectForPackage(workspacePath, packagePath, targetLabel, monitor, false, false); } public static IProject createProjectForPackage( String workspacePath, String packagePath, String targetLabel, IProgressMonitor monitor, boolean deferContainerResolution) { + return createProjectForPackage(workspacePath, packagePath, targetLabel, monitor, deferContainerResolution, false); + } + + public static IProject createProjectForPackage( + String workspacePath, String packagePath, String targetLabel, + IProgressMonitor monitor, boolean deferContainerResolution, boolean isTestProject) { try { String projectName = LabelUtils.toProjectName(packagePath); IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); @@ -90,7 +97,7 @@ public static IProject createProjectForPackage( preCreateResourceFilter(project); ensureNatures(project, monitor); String inferredSourceRoot = SourceRootUtils.inferSourceRoot(workspacePath, packagePath); - configureClasspath(project, packagePath, workspacePath, targetLabel, inferredSourceRoot, monitor, deferContainerResolution); + configureClasspath(project, packagePath, workspacePath, targetLabel, inferredSourceRoot, monitor, deferContainerResolution, isTestProject); return project; } catch (Exception e) { @@ -165,7 +172,8 @@ private static void removeJavaBuilder(IProject project, IProgressMonitor monitor private static void configureClasspath(IProject project, String packageName, String workspacePath, String targetLabel, String inferredSourceRoot, - IProgressMonitor monitor, boolean deferContainerResolution) throws CoreException { + IProgressMonitor monitor, boolean deferContainerResolution, + boolean isTestProject) throws CoreException { IJavaProject javaProject = JavaCore.create(project); List sourceEntries = new ArrayList<>(); @@ -179,7 +187,7 @@ private static void configureClasspath(IProject project, String packageName, linkedFolder.createLink(new Path(srcDir.getAbsolutePath()), 0, monitor); } IPath sourcePath = new Path("/" + project.getName() + "/" + linkedName); - sourceEntries.add(JavaCore.newSourceEntry(sourcePath)); + sourceEntries.add(newSourceEntry(sourcePath, isTestProject)); } } @@ -188,15 +196,18 @@ private static void configureClasspath(IProject project, String packageName, if (inferredSourceRoot != null) { try { SourceRootUtils.configureLinkedSourceFolder( - project, workspacePath, inferredSourceRoot, packageName, entries, monitor); + project, workspacePath, inferredSourceRoot, packageName, entries, + monitor, isTestProject); } catch (Exception e) { LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", "Failed to create linked source folder for " + packageName + ", falling back to linked package folder: " + e.getMessage())); - configureLinkedPackageFolder(project, workspacePath, packageName, entries, monitor); + configureLinkedPackageFolder(project, workspacePath, packageName, entries, + monitor, isTestProject); } } else { - configureLinkedPackageFolder(project, workspacePath, packageName, entries, monitor); + configureLinkedPackageFolder(project, workspacePath, packageName, entries, + monitor, isTestProject); } } else { entries.addAll(sourceEntries); @@ -216,14 +227,24 @@ private static void configureClasspath(IProject project, String packageName, private static void configureLinkedPackageFolder(IProject project, String workspacePath, String packageName, List entries, - IProgressMonitor monitor) throws CoreException { + IProgressMonitor monitor, boolean isTestProject) throws CoreException { String linkedName = "_pkg"; org.eclipse.core.resources.IFolder linkedFolder = project.getFolder(linkedName); if (!linkedFolder.exists()) { File packageDir = new File(workspacePath, packageName); linkedFolder.createLink(new Path(packageDir.getAbsolutePath()), 0, monitor); } - entries.add(JavaCore.newSourceEntry(new Path("/" + project.getName() + "/" + linkedName))); + entries.add(newSourceEntry(new Path("/" + project.getName() + "/" + linkedName), isTestProject)); + } + + static IClasspathEntry newSourceEntry(IPath path, boolean isTestProject) { + if (isTestProject) { + IClasspathAttribute[] attrs = new IClasspathAttribute[]{ + JavaCore.newClasspathAttribute(IClasspathAttribute.TEST, "true") + }; + return JavaCore.newSourceEntry(path, null, null, null, attrs); + } + return JavaCore.newSourceEntry(path); } private static void addJreContainerEntry(List entries) { diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java index 5cff04a..7154265 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java @@ -175,8 +175,9 @@ public void run(IProgressMonitor pm) throws CoreException { for (String targetLabel : finalTargets) { try { String packagePath = extractPackageName(targetLabel); + boolean isTestTarget = bridge.isTestTarget(targetLabel); IProject project = BazelProjectCreator.createProjectForPackage( - workspacePath, packagePath, targetLabel, pm, true); + workspacePath, packagePath, targetLabel, pm, true, isTestTarget); if (firstProject && project != null) { if (TargetProjectMapping.readWorkspaceConfig(project) == null) { diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelRuntimeClasspathEntryResolver.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelRuntimeClasspathEntryResolver.java index 24bc16f..78c3245 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelRuntimeClasspathEntryResolver.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelRuntimeClasspathEntryResolver.java @@ -90,8 +90,6 @@ private IRuntimeClasspathEntry[] buildEntries(IJavaProject project) { result.add(rte); } - LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "Resolved " + result.size() + " runtime classpath entries for " + project.getElementName())); return result.toArray(EMPTY); } catch (Exception e) { LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/SourceRootUtils.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/SourceRootUtils.java index 50122bc..bb2381e 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/SourceRootUtils.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/SourceRootUtils.java @@ -17,7 +17,7 @@ import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.jdt.core.IClasspathEntry; -import org.eclipse.jdt.core.JavaCore; + public final class SourceRootUtils { @@ -178,6 +178,13 @@ static String linkedFolderName(String sourceRoot) { public static void configureLinkedSourceFolder(IProject project, String workspacePath, String sourceRoot, String packagePath, List entries, IProgressMonitor monitor) throws CoreException { + configureLinkedSourceFolder(project, workspacePath, sourceRoot, packagePath, entries, + monitor, false); + } + + public static void configureLinkedSourceFolder(IProject project, String workspacePath, + String sourceRoot, String packagePath, List entries, + IProgressMonitor monitor, boolean isTestProject) throws CoreException { String topFolderName = linkedFolderName(sourceRoot); String prefix = sourceRoot + "/"; String declPath = packagePath.startsWith(prefix) @@ -191,7 +198,7 @@ public static void configureLinkedSourceFolder(IProject project, String workspac } linkedFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor); IPath sourcePath = new Path("/" + project.getName() + "/" + topFolderName); - entries.add(JavaCore.newSourceEntry(sourcePath)); + entries.add(BazelProjectCreator.newSourceEntry(sourcePath, isTestProject)); LOG.info("Configured linked source folder '" + topFolderName + "' → " + sourceRoot + " for project " + project.getName()); return; @@ -219,7 +226,7 @@ public static void configureLinkedSourceFolder(IProject project, String workspac leafFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor); IPath sourcePath = new Path("/" + project.getName() + "/" + topFolderName); - entries.add(JavaCore.newSourceEntry(sourcePath)); + entries.add(BazelProjectCreator.newSourceEntry(sourcePath, isTestProject)); LOG.info("Configured linked source folder '" + topFolderName + "/" + declPath + "' → " + packagePath + " for project " + project.getName()); diff --git a/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelClasspathContainerTest.java b/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelClasspathContainerTest.java index 437a004..2ac778f 100644 --- a/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelClasspathContainerTest.java +++ b/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelClasspathContainerTest.java @@ -2,7 +2,9 @@ import static org.junit.Assert.*; +import org.eclipse.jdt.core.IClasspathAttribute; import org.eclipse.jdt.core.IClasspathContainer; +import org.eclipse.jdt.core.IClasspathEntry; import org.junit.Test; public class BazelClasspathContainerTest { @@ -36,4 +38,46 @@ public void getPathReturnsContainerPath() { BazelClasspathContainer c = new BazelClasspathContainer(new String[0]); assertSame(BazelClasspathContainer.CONTAINER_PATH, c.getPath()); } + + @Test + public void srcEntryWithIsTestTrueHasTestAttribute() { + BazelClasspathContainer c = new BazelClasspathContainer( + new String[]{"SRC|/myproject/src||true"}); + IClasspathEntry[] entries = c.getClasspathEntries(); + assertEquals(1, entries.length); + IClasspathEntry entry = entries[0]; + assertEquals(IClasspathEntry.CPE_SOURCE, entry.getEntryKind()); + boolean hasTest = false; + for (IClasspathAttribute attr : entry.getExtraAttributes()) { + if (IClasspathAttribute.TEST.equals(attr.getName()) + && "true".equals(attr.getValue())) { + hasTest = true; + } + } + assertTrue("SRC entry with isTest=true should have TEST attribute", hasTest); + } + + @Test + public void srcEntryWithIsTestFalseHasNoTestAttribute() { + BazelClasspathContainer c = new BazelClasspathContainer( + new String[]{"SRC|/myproject/src||false"}); + IClasspathEntry[] entries = c.getClasspathEntries(); + assertEquals(1, entries.length); + for (IClasspathAttribute attr : entries[0].getExtraAttributes()) { + assertNotEquals("SRC entry with isTest=false should not have TEST attribute", + IClasspathAttribute.TEST, attr.getName()); + } + } + + @Test + public void srcEntryWithMissingIsTestFieldHasNoTestAttribute() { + BazelClasspathContainer c = new BazelClasspathContainer( + new String[]{"SRC|/myproject/src"}); + IClasspathEntry[] entries = c.getClasspathEntries(); + assertEquals(1, entries.length); + for (IClasspathAttribute attr : entries[0].getExtraAttributes()) { + assertNotEquals("SRC entry with no isTest field should not have TEST attribute", + IClasspathAttribute.TEST, attr.getName()); + } + } } diff --git a/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelExternalRepoResolverTest.java b/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelExternalRepoResolverTest.java new file mode 100644 index 0000000..ede62b7 --- /dev/null +++ b/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelExternalRepoResolverTest.java @@ -0,0 +1,149 @@ +package com.bazel.jdt; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class BazelExternalRepoResolverTest { + + private Path tempDir; + + @Before + public void setUp() throws IOException { + tempDir = Files.createTempDirectory("bazel-resolver-test"); + BazelExternalRepoResolver.resetCaches(); + } + + @After + public void tearDown() { + BazelExternalRepoResolver.resetCaches(); + deleteRecursive(tempDir.toFile()); + } + + @Test + public void extractRepoNameFromExternalPath() { + assertEquals("junit_junit", + BazelExternalRepoResolver.extractRepoName( + "/private/var/tmp/_bazel/execroot/ws/external/junit_junit/jar/_ijar/downloaded-ijar.jar")); + } + + @Test + public void extractRepoNameFromBazelOutPath() { + assertEquals("maven", + BazelExternalRepoResolver.extractRepoName( + "/workspace/bazel-out/k8-fastbuild/bin/external/maven/com/junit/junit/4.12/_ijar/downloaded-ijar.jar")); + } + + @Test + public void extractRepoNameReturnsNullForNonExternalPath() { + assertNull(BazelExternalRepoResolver.extractRepoName( + "/workspace/bazel-out/k8-fastbuild/bin/3rdparty/libjunit.jar")); + } + + @Test + public void extractRepoNameReturnsNullForEmptyAfterExternal() { + assertNull(BazelExternalRepoResolver.extractRepoName( + "/workspace/external/")); + } + + @Test + public void findBzlmodRepoDirMatchesTildePattern() throws IOException { + String outputBase = tempDir.toString(); + File bzlmodDir = new File(outputBase, "external/rules_jvm_external~~maven~maven"); + assertTrue(bzlmodDir.mkdirs()); + + File result = BazelExternalRepoResolver.findBzlmodRepoDir(outputBase, "maven"); + assertNotNull(result); + assertEquals(bzlmodDir.getAbsolutePath(), result.getAbsolutePath()); + } + + @Test + public void findBzlmodRepoDirReturnsNullWhenNoMatch() throws IOException { + String outputBase = tempDir.toString(); + File externalDir = new File(outputBase, "external"); + assertTrue(externalDir.mkdirs()); + + assertNull(BazelExternalRepoResolver.findBzlmodRepoDir(outputBase, "maven")); + } + + @Test + public void resolveFallbackJarFindsJarInExternalRepo() throws IOException { + String outputBase = tempDir.toString(); + File repoDir = new File(outputBase, "external/junit_junit/jar"); + assertTrue(repoDir.mkdirs()); + File jar = new File(repoDir, "downloaded.jar"); + assertTrue(jar.createNewFile()); + + String wsPath = tempDir.resolve("workspace").toString(); + BazelExternalRepoResolver.setOutputBaseForTest(wsPath, outputBase); + + String missingPath = outputBase + "/execroot/ws/external/junit_junit/jar/_ijar/downloaded-ijar.jar"; + String result = BazelExternalRepoResolver.resolveFallbackJar(missingPath, wsPath); + + assertNotNull("Should resolve fallback JAR", result); + assertEquals(jar.getAbsolutePath(), result); + } + + @Test + public void resolveFallbackJarSkipsSourcesJar() throws IOException { + String outputBase = tempDir.toString(); + File repoDir = new File(outputBase, "external/guava/jar"); + assertTrue(repoDir.mkdirs()); + assertTrue(new File(repoDir, "guava-31.1-sources.jar").createNewFile()); + File classesJar = new File(repoDir, "guava-31.1.jar"); + assertTrue(classesJar.createNewFile()); + + String wsPath = tempDir.resolve("workspace").toString(); + BazelExternalRepoResolver.setOutputBaseForTest(wsPath, outputBase); + + String missingPath = outputBase + "/execroot/ws/external/guava/jar/_ijar/downloaded-ijar.jar"; + String result = BazelExternalRepoResolver.resolveFallbackJar(missingPath, wsPath); + + assertNotNull(result); + assertEquals(classesJar.getAbsolutePath(), result); + } + + @Test + public void resolveFallbackJarReturnsNullWhenNoJarExists() throws IOException { + String outputBase = tempDir.toString(); + File repoDir = new File(outputBase, "external/missing_repo"); + assertTrue(repoDir.mkdirs()); + + String wsPath = tempDir.resolve("workspace").toString(); + BazelExternalRepoResolver.setOutputBaseForTest(wsPath, outputBase); + + String missingPath = outputBase + "/execroot/ws/external/missing_repo/jar/foo.jar"; + String result = BazelExternalRepoResolver.resolveFallbackJar(missingPath, wsPath); + + assertNull(result); + } + + @Test + public void resolveFallbackJarReturnsNullForNonExternalPath() { + String wsPath = tempDir.resolve("workspace").toString(); + BazelExternalRepoResolver.setOutputBaseForTest(wsPath, tempDir.toString()); + + String result = BazelExternalRepoResolver.resolveFallbackJar( + "/workspace/bazel-out/bin/3rdparty/libjunit.jar", wsPath); + assertNull(result); + } + + private static void deleteRecursive(File file) { + if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + deleteRecursive(child); + } + } + } + file.delete(); + } +} diff --git a/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelProjectCreatorTest.java b/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelProjectCreatorTest.java new file mode 100644 index 0000000..91724f6 --- /dev/null +++ b/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelProjectCreatorTest.java @@ -0,0 +1,37 @@ +package com.bazel.jdt; + +import static org.junit.Assert.*; + +import org.eclipse.jdt.core.IClasspathAttribute; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.core.runtime.Path; +import org.junit.Test; + +public class BazelProjectCreatorTest { + + @Test + public void newSourceEntryWithTestProjectHasTestAttribute() { + IClasspathEntry entry = BazelProjectCreator.newSourceEntry( + new Path("/myproject/src"), true); + assertEquals(IClasspathEntry.CPE_SOURCE, entry.getEntryKind()); + boolean hasTest = false; + for (IClasspathAttribute attr : entry.getExtraAttributes()) { + if (IClasspathAttribute.TEST.equals(attr.getName()) + && "true".equals(attr.getValue())) { + hasTest = true; + } + } + assertTrue("Source entry for test project should have TEST attribute", hasTest); + } + + @Test + public void newSourceEntryWithNonTestProjectHasNoTestAttribute() { + IClasspathEntry entry = BazelProjectCreator.newSourceEntry( + new Path("/myproject/src"), false); + assertEquals(IClasspathEntry.CPE_SOURCE, entry.getEntryKind()); + for (IClasspathAttribute attr : entry.getExtraAttributes()) { + assertNotEquals("Source entry for non-test project should not have TEST attribute", + IClasspathAttribute.TEST, attr.getName()); + } + } +} diff --git a/bazel-jdt-bridge/vscode-extension/src/debugAdapter.ts b/bazel-jdt-bridge/vscode-extension/src/debugAdapter.ts index c1c64a9..b7302b6 100644 --- a/bazel-jdt-bridge/vscode-extension/src/debugAdapter.ts +++ b/bazel-jdt-bridge/vscode-extension/src/debugAdapter.ts @@ -68,6 +68,28 @@ export class BazelDebugConfigurationProvider implements vscode.DebugConfiguratio } } + // The test runner pre-resolves classPaths before this handler runs, + // so newly-built JARs (e.g. the test's own output) are missing. + // Re-resolve from the updated container and merge new entries. + // We merge rather than replace to keep test-runner JARs (RemoteTestRunner etc.). + if (config.mainClass && Array.isArray(config.classPaths)) { + try { + const resolved = await vscode.commands.executeCommand<[string[], string[]]>( + 'java.execute.workspaceCommand', + 'vscode.java.resolveClasspath', config.mainClass, config.projectName); + if (resolved && Array.isArray(resolved[1])) { + const existing = new Set(config.classPaths as string[]); + for (const entry of resolved[1]) { + if (!existing.has(entry)) { + (config.classPaths as string[]).push(entry); + } + } + } + } catch { + // Best-effort: fall back to the pre-resolved classpath + } + } + return config; } }