mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-20 13:31:28 +07:00
[IJI-3114] bazel: resolve test dependencies using manifest file on windows test runs
GitOrigin-RevId: 396be8ad504c795723e54631b1eeb2a06b4c853b
This commit is contained in:
committed by
intellij-monorepo-bot
parent
72b00d5935
commit
984c33b9ed
@@ -22,6 +22,8 @@
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/testResources" type="java-test-resource" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
@@ -124,6 +126,7 @@
|
||||
<orderEntry type="module" module-name="intellij.xml.psi" />
|
||||
<orderEntry type="module" module-name="intellij.java.syntax" />
|
||||
<orderEntry type="module" module-name="intellij.java.psi" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework.common" scope="TEST" />
|
||||
</component>
|
||||
<component name="copyright">
|
||||
<Base>
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.intellij.openapi.progress.ProgressManager;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.project.ProjectBundle;
|
||||
import com.intellij.openapi.projectRoots.*;
|
||||
import com.intellij.openapi.projectRoots.testFramework.TestJdkAnnotationsFilesProvider;
|
||||
import com.intellij.openapi.roots.AnnotationOrderRootType;
|
||||
import com.intellij.openapi.roots.JavadocOrderRootType;
|
||||
import com.intellij.openapi.roots.OrderRootType;
|
||||
@@ -454,10 +455,16 @@ public final class JavaSdkImpl extends JavaSdk {
|
||||
// Bazel-provided test dependencies, from runfiles tree
|
||||
String testSrcDir = System.getenv("TEST_SRCDIR");
|
||||
if (testSrcDir != null && !testSrcDir.isBlank()) {
|
||||
Path root1 = Path.of(testSrcDir, "community+/java/jdkAnnotations");
|
||||
Path root2 = Path.of(testSrcDir, "_main/java/jdkAnnotations");
|
||||
|
||||
Path rootPath = Files.isDirectory(root1) ? root1 : Files.isDirectory(root2) ? root2 : null;
|
||||
ServiceLoader<TestJdkAnnotationsFilesProvider> providerClasses = ServiceLoader.load(TestJdkAnnotationsFilesProvider.class);
|
||||
var iterator = providerClasses.iterator();
|
||||
if (!iterator.hasNext()) {
|
||||
throw new IllegalStateException("TestJdkAnnotationsFilesProvider service provider not found");
|
||||
}
|
||||
TestJdkAnnotationsFilesProvider provider = iterator.next();
|
||||
if (iterator.hasNext()) {
|
||||
throw new IllegalStateException("more than one TestJdkAnnotationsFilesProvider service providers found. Only one is expected");
|
||||
}
|
||||
Path rootPath = provider.getJdkAnnotationsPath();
|
||||
if (rootPath != null) {
|
||||
String path = FileUtil.toSystemIndependentName(rootPath.toString());
|
||||
root = refresh ? lfs.refreshAndFindFileByPath(path) : lfs.findFileByPath(path);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.projectRoots.testFramework;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
|
||||
public interface TestJdkAnnotationsFilesProvider {
|
||||
Path getJdkAnnotationsPath();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.intellij.openapi.projectRoots.files;
|
||||
|
||||
import com.intellij.testFramework.common.BazelTestUtil;
|
||||
import com.intellij.openapi.projectRoots.testFramework.TestJdkAnnotationsFilesProvider;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class TestJdkAnnotationsFilesProviderImpl implements TestJdkAnnotationsFilesProvider {
|
||||
@Override
|
||||
public Path getJdkAnnotationsPath() {
|
||||
return BazelTestUtil.findRunfilesDirectoryUnderCommunityOrUltimate("java/jdkAnnotations");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
com.intellij.openapi.projectRoots.files.TestJdkAnnotationsFilesProviderImpl
|
||||
@@ -38,6 +38,7 @@ public final class JUnit5BazelRunner {
|
||||
private static final int EXIT_CODE_TEST_RUNNER_FAILURE = 2;
|
||||
private static final int EXIT_CODE_TEST_FAILURE_OOM = 137;
|
||||
|
||||
private static final String bazelEnvRunfilesManifestOnly = "RUNFILES_MANIFEST_ONLY";
|
||||
private static final String bazelEnvSelfLocation = "SELF_LOCATION";
|
||||
private static final String bazelEnvTestTmpDir = "TEST_TMPDIR";
|
||||
private static final String bazelEnvRunFilesDir = "RUNFILES_DIR";
|
||||
@@ -422,10 +423,13 @@ public final class JUnit5BazelRunner {
|
||||
}
|
||||
|
||||
private static Boolean isBazelTestRun() {
|
||||
return Stream.of(bazelEnvSelfLocation, bazelEnvTestTmpDir, bazelEnvRunFilesDir, bazelEnvJavaRunFilesDir)
|
||||
return Stream.of(bazelEnvTestTmpDir, bazelEnvRunFilesDir, bazelEnvJavaRunFilesDir)
|
||||
.allMatch(bazelTestEnv -> {
|
||||
var bazelTestEnvValue = System.getenv(bazelTestEnv);
|
||||
return bazelTestEnvValue != null && !bazelTestEnvValue.isBlank();
|
||||
}) && Stream.of(bazelEnvSelfLocation, bazelEnvRunfilesManifestOnly).anyMatch(bazelTestEnv -> {
|
||||
var bazelTestEnvValue = System.getenv(bazelTestEnv);
|
||||
return bazelTestEnvValue != null && !bazelTestEnvValue.isBlank();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -497,6 +501,10 @@ public final class JUnit5BazelRunner {
|
||||
List<Path> paths = (List<Path>)getBaseUrls.invoke(classLoader);
|
||||
|
||||
String bazelTestSelfLocation = System.getenv(bazelEnvSelfLocation);
|
||||
// the relevant jars are expected to be next to the classloader when no SELF_LOCATION is set (singlejar, windows runs)
|
||||
if (bazelTestSelfLocation == null || bazelTestSelfLocation.isBlank()) {
|
||||
return new HashSet<>(paths);
|
||||
}
|
||||
Path bazelTestSelfLocationDir = Path.of(bazelTestSelfLocation).getParent().toAbsolutePath();
|
||||
return paths.stream()
|
||||
.filter(p -> bazelTestSelfLocationDir.equals(p.toAbsolutePath().getParent()))
|
||||
|
||||
@@ -4,10 +4,8 @@ package com.intellij.testFramework.common
|
||||
import com.intellij.testFramework.common.bazel.BazelLabel
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.absolute
|
||||
import kotlin.io.path.isDirectory
|
||||
import kotlin.io.path.isRegularFile
|
||||
import kotlin.io.path.useLines
|
||||
import kotlin.io.path.*
|
||||
import kotlin.math.max
|
||||
|
||||
@ApiStatus.Experimental
|
||||
object BazelTestUtil {
|
||||
@@ -15,6 +13,7 @@ object BazelTestUtil {
|
||||
// also https://leimao.github.io/blog/Bazel-Test-Outputs/
|
||||
private const val TEST_SRCDIR_ENV_NAME = "TEST_SRCDIR"
|
||||
private const val TEST_UNDECLARED_OUTPUTS_DIR_ENV_NAME = "TEST_UNDECLARED_OUTPUTS_DIR"
|
||||
private const val RUNFILES_MANIFEST_ONLY_ENV_NAME = "RUNFILES_MANIFEST_ONLY"
|
||||
|
||||
@JvmStatic
|
||||
val isUnderBazelTest: Boolean =
|
||||
@@ -27,7 +26,13 @@ object BazelTestUtil {
|
||||
*/
|
||||
@JvmStatic
|
||||
val bazelTestRepoMapping: Map<String, RepoMappingEntry> by lazy {
|
||||
bazelTestRunfilesPath.resolve("_repo_mapping").useLines { lines ->
|
||||
val repoMappingFile = when {
|
||||
bazelTestRunfilesPath.resolve("_repo_mapping").exists() -> bazelTestRunfilesPath.resolve("_repo_mapping")
|
||||
Path.of(bazelRunfilesManifestResolver.get("_repo_mapping")).exists() ->
|
||||
Path.of(bazelRunfilesManifestResolver.get("_repo_mapping"))
|
||||
else -> error("repo_mapping file not found.")
|
||||
}
|
||||
repoMappingFile.useLines { lines ->
|
||||
lines
|
||||
.filter { it.isNotBlank() && it.isNotEmpty() }
|
||||
.map { parseRepoEntry(it) }
|
||||
@@ -36,6 +41,18 @@ object BazelTestUtil {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val bazelRunfilesManifestResolver: BazelRunfilesManifest by lazy {
|
||||
BazelRunfilesManifest()
|
||||
}
|
||||
|
||||
// Bazel sets RUNFILES_MANIFEST_ONLY=1 on platforms that only support manifest-based runfiles (e.g., Windows).
|
||||
// Cache it to avoid repeated env lookups and branching cost in hot paths.
|
||||
private val runfilesManifestOnly: Boolean by lazy {
|
||||
val v = System.getenv(RUNFILES_MANIFEST_ONLY_ENV_NAME)
|
||||
v != null && v.isNotBlank() && v == "1"
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute path to the base of the runfiles tree (your test dependencies too),
|
||||
* see [Test encyclopedia](https://bazel.build/reference/test-encyclopedia#initial-conditions)
|
||||
@@ -71,15 +88,29 @@ object BazelTestUtil {
|
||||
val repoEntry = bazelTestRepoMapping.getOrElse(label.repo) {
|
||||
error("Unable to determine dependency path '${label.asLabel}'")
|
||||
}
|
||||
val file = bazelTestRunfilesPath
|
||||
.resolve(repoEntry.runfilesRelativePath)
|
||||
.let { if (label.packageName.isNotEmpty()) it.resolve(label.packageName) else it }
|
||||
.resolve(label.target)
|
||||
// Build a single relative key used both for runfiles tree and for manifest lookup
|
||||
val manifestKey = buildString {
|
||||
append(repoEntry.runfilesRelativePath)
|
||||
if (label.packageName.isNotEmpty()) {
|
||||
append('/')
|
||||
append(label.packageName)
|
||||
}
|
||||
append('/')
|
||||
append(label.target)
|
||||
}
|
||||
|
||||
// Fast path for manifest-only environments: avoid touching filesystem entirely
|
||||
if (runfilesManifestOnly) {
|
||||
val resolved = Path.of(bazelRunfilesManifestResolver.get(manifestKey))
|
||||
if (resolved.isRegularFile() || resolved.isDirectory()) return resolved
|
||||
error("Unable to find test dependency '${label.asLabel}' at $resolved")
|
||||
}
|
||||
|
||||
// Typical path-based runfiles: try direct path first, then fall back to manifest (covers mixed layouts)
|
||||
val file = bazelTestRunfilesPath.resolve(manifestKey)
|
||||
return when {
|
||||
file.isRegularFile() || file.isDirectory() -> file.toAbsolutePath()
|
||||
else -> {
|
||||
error("Unable to find test dependency '${label.asLabel}' at $file")
|
||||
}
|
||||
else -> error("Unable to find test dependency '${label.asLabel}' at $file")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,8 +129,15 @@ object BazelTestUtil {
|
||||
*/
|
||||
@JvmStatic
|
||||
fun findRunfilesDirectoryUnderCommunityOrUltimate(relativePath: String): Path {
|
||||
val root1 = bazelTestRunfilesPath.resolve("community+").resolve(relativePath)
|
||||
val root2 = bazelTestRunfilesPath.resolve("_main").resolve(relativePath)
|
||||
val (root1, root2) = if (runfilesManifestOnly) {
|
||||
val root1key = "community+/${relativePath}"
|
||||
val root2key = "_main/${relativePath}"
|
||||
Path.of(bazelRunfilesManifestResolver.get(root1key)) to
|
||||
Path.of(bazelRunfilesManifestResolver.get(root2key))
|
||||
} else {
|
||||
bazelTestRunfilesPath.resolve("community+").resolve(relativePath) to
|
||||
bazelTestRunfilesPath.resolve("_main").resolve(relativePath)
|
||||
}
|
||||
|
||||
val root1exists = root1.isDirectory()
|
||||
val root2exists = root2.isDirectory()
|
||||
@@ -126,3 +164,99 @@ object BazelTestUtil {
|
||||
return RepoMappingEntry( parts[1], parts[2])
|
||||
}
|
||||
}
|
||||
|
||||
class BazelRunfilesManifest() {
|
||||
companion object {
|
||||
// https://fuchsia.googlesource.com/fuchsia/+/HEAD/build/bazel/BAZEL_RUNFILES.md?format%2F%2F#how-runfiles-libraries-really-work
|
||||
private const val RUNFILES_MANIFEST_FILE_ENV_NAME = "RUNFILES_MANIFEST_FILE"
|
||||
}
|
||||
|
||||
private val bazelRunFilesManifest: Map<String, String> by lazy {
|
||||
val file = Path.of(System.getenv(RUNFILES_MANIFEST_FILE_ENV_NAME))
|
||||
require(file.exists()) { "RUNFILES_MANIFEST_FILE does not exist: $file" }
|
||||
file.useLines { lines ->
|
||||
lines
|
||||
.filter { it.isNotBlank() && it.isNotEmpty() }
|
||||
.map { parseManifestEntry(it) }
|
||||
.distinct()
|
||||
.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
private val calculatedManifestEntries: MutableMap<String, String> = mutableMapOf()
|
||||
|
||||
private fun parseManifestEntry(line: String): Pair<String, String> {
|
||||
val parts = line.split(" ", limit = 2)
|
||||
require(parts.size == 2) { "runfiles_manifest line must have exactly 2 space-separated values: '$line'" }
|
||||
return parts[0] to parts[1]
|
||||
}
|
||||
|
||||
fun get(key: String) : String {
|
||||
val valueByFullKey = bazelRunFilesManifest[key]
|
||||
if (valueByFullKey != null) {
|
||||
return valueByFullKey
|
||||
}
|
||||
|
||||
// required to resolve directories as in the manifest entries
|
||||
// only files are present as keys, but not directories
|
||||
return calculatedManifestEntries.computeIfAbsent(key, {
|
||||
val subset = bazelRunFilesManifest.filter { it.key.startsWith(key) }
|
||||
val calculatedValue = if (subset.isNotEmpty()) {
|
||||
val longestKey = findLongestCommonPrefix(subset.keys)
|
||||
val longestValue = findLongestCommonPrefix(subset.values.toSet())
|
||||
mapByQuery(longestKey, longestValue, key)
|
||||
} else {
|
||||
key
|
||||
}
|
||||
|
||||
return@computeIfAbsent calculatedValue
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fun findLongestCommonPrefix(paths: Set<String>?): String {
|
||||
// Handle null/empty gracefully
|
||||
if (paths.isNullOrEmpty()) return ""
|
||||
|
||||
// Normalize and split into path segments without using regex; ignore empty segments
|
||||
val partsList: List<List<String>> = paths
|
||||
.map { it.trim('/') }
|
||||
.map { p -> if (p.isEmpty()) emptyList() else p.split('/').filter { it.isNotEmpty() } }
|
||||
|
||||
if (partsList.isEmpty()) return ""
|
||||
|
||||
// Find the shortest path length to limit comparisons
|
||||
val minLength = partsList.minOf { it.size }
|
||||
|
||||
val sb = StringBuilder()
|
||||
for (i in 0 until minLength) {
|
||||
val segment = partsList[0][i]
|
||||
if (partsList.any { it[i] != segment }) break
|
||||
if (sb.isNotEmpty()) sb.append('/')
|
||||
sb.append(segment)
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun mapByQuery(fullKey: String, value: String, queryKey: String): String {
|
||||
require(fullKey.isNotEmpty() && value.isNotEmpty() && queryKey.isNotEmpty()) { "Query cannot be empty" }
|
||||
|
||||
val fullKeyParts: Array<String> = fullKey.split("/").dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
val valueParts: Array<String> = value.split("/").dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
val queryKeyParts: Array<String?> = queryKey.split("/").dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
|
||||
require(fullKeyParts.size >= queryKeyParts.size) { "Query key $queryKey is longer than full key $fullKey" }
|
||||
|
||||
val removeCount = fullKeyParts.size - queryKeyParts.size
|
||||
val endIndex = max(valueParts.size - removeCount, 0)
|
||||
|
||||
return buildString {
|
||||
for (i in 0..<endIndex) {
|
||||
if (i > 0) {
|
||||
append('/')
|
||||
}
|
||||
append(valueParts[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user