mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
AT-3932 Exceptions reported as failed tests on TC should inherit the test's codeowner
GitOrigin-RevId: 4c29989cb680436226c4c1ac3bbd17ac8058c278
This commit is contained in:
committed by
intellij-monorepo-bot
parent
4b0231e8fd
commit
aa939bb5c2
@@ -54,6 +54,15 @@ public final class TestLocationStorage {
|
||||
|
||||
public static final Logger LOG = Logger.getLogger(TestLocationStorage.class.getName());
|
||||
|
||||
/**
|
||||
* Holds extracted location information for a test class.
|
||||
*
|
||||
* @param moduleName the module name derived from the JAR path
|
||||
* @param packagePath the package path extracted from bytecode
|
||||
* @param fileName the source file name extracted from bytecode
|
||||
*/
|
||||
public record TestLocationInfo(String moduleName, String packagePath, String fileName) {}
|
||||
|
||||
/**
|
||||
* Path to the test location artifact file (NDJSON format)
|
||||
*/
|
||||
@@ -187,60 +196,108 @@ public final class TestLocationStorage {
|
||||
.replace("\t", "\\t");
|
||||
}
|
||||
|
||||
static void recordTestLocation(TestIdentifier testIdentifier, TestExecutionResult.Status status, String testName) {
|
||||
/**
|
||||
* Extracts location information for a test identified by its {@link TestIdentifier}.
|
||||
*
|
||||
* <p>This method resolves the test class from the identifier, loads it using the context
|
||||
* class loader, and extracts module name, package path, and source file name.</p>
|
||||
*
|
||||
* @param testIdentifier the JUnit Platform test identifier
|
||||
* @return location info, or {@code null} if resolution fails (failures are logged)
|
||||
*/
|
||||
public static TestLocationInfo getTestLocationInfo(TestIdentifier testIdentifier) {
|
||||
TestSource source = testIdentifier.getSource().orElse(null);
|
||||
String className = getClassNameFromTestSource(source);
|
||||
|
||||
if (className == null) {
|
||||
LOG.info("Cannot find class name for " + testName);
|
||||
return;
|
||||
LOG.info("Cannot find class name for " + testIdentifier.getDisplayName());
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
|
||||
if (classLoader == null) {
|
||||
LOG.info("Could not find class loader for test " + testName);
|
||||
return;
|
||||
LOG.info("Could not find class loader for test " + testIdentifier.getDisplayName());
|
||||
return null;
|
||||
}
|
||||
|
||||
Class<?> testClass = Class.forName(className, false, classLoader);
|
||||
return getTestLocationInfo(testClass);
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOG.info(e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts location information for a test class.
|
||||
*
|
||||
* <p>This method extracts module name, package path, and source file name from
|
||||
* the class's code source location and bytecode.</p>
|
||||
*
|
||||
* @param testClass the test class
|
||||
* @return location info, or {@code null} if resolution fails (failures are logged)
|
||||
*/
|
||||
public static TestLocationInfo getTestLocationInfo(Class<?> testClass) {
|
||||
try {
|
||||
CodeSource codeSource = testClass.getProtectionDomain().getCodeSource();
|
||||
|
||||
if (codeSource != null && codeSource.getLocation() != null) {
|
||||
Path jarPath = Paths.get(codeSource.getLocation().toURI());
|
||||
String moduleName = getModuleName(jarPath);
|
||||
if (moduleName == null) {
|
||||
LOG.info("No module found for " + codeSource.getLocation());
|
||||
return;
|
||||
}
|
||||
String packagePath = getPackagePath(testClass);
|
||||
String fileName = getFileName(testClass);
|
||||
if (fileName == null) {
|
||||
LOG.info("No file found for " + codeSource.getLocation());
|
||||
return;
|
||||
}
|
||||
boolean failed = (status == TestExecutionResult.Status.FAILED);
|
||||
if (codeSource == null || codeSource.getLocation() == null) {
|
||||
LOG.info("No code source for " + testClass.getName());
|
||||
return null;
|
||||
}
|
||||
|
||||
String json = String.format(
|
||||
"{\"test\":\"%s\",\"module\":\"%s\",\"package\":\"%s\",\"file\":\"%s\",\"failed\":%s}%n",
|
||||
escapeJson(testName),
|
||||
escapeJson(moduleName),
|
||||
escapeJson(packagePath),
|
||||
escapeJson(fileName),
|
||||
failed
|
||||
);
|
||||
Path jarPath = Paths.get(codeSource.getLocation().toURI());
|
||||
String moduleName = getModuleName(jarPath);
|
||||
if (moduleName == null) {
|
||||
LOG.info("No module found for " + codeSource.getLocation());
|
||||
return null;
|
||||
}
|
||||
|
||||
LOG.info("Writing to " + TEST_LOCATION_ARTIFACT.toAbsolutePath());
|
||||
synchronized (TEST_LOCATION_ARTIFACT) {
|
||||
// Ensure parent directory exists
|
||||
Path parentDir = TEST_LOCATION_ARTIFACT.getParent();
|
||||
if (parentDir != null && !Files.exists(parentDir)) {
|
||||
Files.createDirectories(parentDir);
|
||||
}
|
||||
Files.writeString(TEST_LOCATION_ARTIFACT, json,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.APPEND);
|
||||
String packagePath = getPackagePath(testClass);
|
||||
String fileName = getFileName(testClass);
|
||||
if (fileName == null) {
|
||||
LOG.info("No file found for " + codeSource.getLocation());
|
||||
return null;
|
||||
}
|
||||
|
||||
return new TestLocationInfo(moduleName, packagePath, fileName);
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOG.info(e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static void recordTestLocation(TestIdentifier testIdentifier, TestExecutionResult.Status status, String testName) {
|
||||
TestLocationInfo info = getTestLocationInfo(testIdentifier);
|
||||
if (info == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean failed = (status == TestExecutionResult.Status.FAILED);
|
||||
|
||||
String json = String.format(
|
||||
"{\"test\":\"%s\",\"module\":\"%s\",\"package\":\"%s\",\"file\":\"%s\",\"failed\":%s}%n",
|
||||
escapeJson(testName),
|
||||
escapeJson(info.moduleName()),
|
||||
escapeJson(info.packagePath()),
|
||||
escapeJson(info.fileName()),
|
||||
failed
|
||||
);
|
||||
|
||||
LOG.info("Writing to " + TEST_LOCATION_ARTIFACT.toAbsolutePath());
|
||||
try {
|
||||
synchronized (TEST_LOCATION_ARTIFACT) {
|
||||
// Ensure parent directory exists
|
||||
Path parentDir = TEST_LOCATION_ARTIFACT.getParent();
|
||||
if (parentDir != null && !Files.exists(parentDir)) {
|
||||
Files.createDirectories(parentDir);
|
||||
}
|
||||
Files.writeString(TEST_LOCATION_ARTIFACT, json,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.APPEND);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
|
||||
+2
-3
@@ -25,9 +25,8 @@ open class CurrentTestMethodProvider : TestExecutionListener {
|
||||
di.direct.instance<CurrentTestMethod>().set(
|
||||
TestMethod(
|
||||
name = methodSource.methodName,
|
||||
clazz = methodSource.javaClass.name,
|
||||
clazzSimpleName = methodSource.javaClass.simpleName,
|
||||
displayName = testIdentifier.displayName
|
||||
displayName = testIdentifier.displayName,
|
||||
testClass = methodSource.javaClass,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.intellij.ide.starter.ci.teamcity
|
||||
|
||||
/**
|
||||
* Resolves code owner for the current test.
|
||||
* Default implementation is no-op; override in DI for actual resolution.
|
||||
*/
|
||||
interface CodeOwnerResolver {
|
||||
/**
|
||||
* Resolves the code owner for the current test method.
|
||||
* Returns null if no test is currently running or if owner cannot be determined.
|
||||
*/
|
||||
fun getOwnerGroupName(): String?
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op implementation used when code owner resolution is not available.
|
||||
*/
|
||||
object NoOpCodeOwnerResolver : CodeOwnerResolver {
|
||||
override fun getOwnerGroupName(): String? = null
|
||||
}
|
||||
+15
@@ -27,6 +27,8 @@ open class TeamCityCIServer(
|
||||
null
|
||||
}
|
||||
) : CIServer {
|
||||
private val codeOwnerResolver by lazy { di.direct.instance<CodeOwnerResolver>() }
|
||||
|
||||
override fun publishArtifact(source: Path, artifactPath: String, artifactName: String) {
|
||||
TeamCityClient.publishTeamCityArtifacts(source = source, artifactPath = artifactPath, artifactName = artifactName)
|
||||
}
|
||||
@@ -42,6 +44,7 @@ open class TeamCityCIServer(
|
||||
"##teamcity[testFailed name='%s' message='%s' details='%s' flowId='%s' nodeId='%s' parentNodeId='0']",
|
||||
generifiedTestName, message.processStringForTC(), details.processStringForTC(), flowId, generifiedTestName
|
||||
))
|
||||
addCodeOwnerMetadata(generifiedTestName, flowId)
|
||||
if (isJetbrainsBuildserver) {
|
||||
addTestMetadata(testName = generifiedTestName, TeamCityMetadataType.LINK, flowId = flowId, name = "Start bisect", value = "https://ij-perf.labs.jb.gg/bisect/launcher?buildId=$buildId")
|
||||
}
|
||||
@@ -59,6 +62,17 @@ open class TeamCityCIServer(
|
||||
logOutput(String.format("##teamcity[testFinished name='%s' flowId='%s' nodeId='%s' parentNodeId='0']", generifiedTestName, flowId, generifiedTestName))
|
||||
}
|
||||
|
||||
private fun addCodeOwnerMetadata(testName: String, flowId: String) {
|
||||
val owner = codeOwnerResolver.getOwnerGroupName() ?: return
|
||||
addTestMetadata(
|
||||
testName = testName,
|
||||
type = TeamCityMetadataType.TEXT,
|
||||
flowId = flowId,
|
||||
name = CODE_OWNER_METADATA_NAME,
|
||||
value = owner,
|
||||
)
|
||||
}
|
||||
|
||||
override fun reportTestFailure(testName: String, message: String, details: String, linkToLogs: String?) {
|
||||
reportTest(testName, message, details, linkToLogs, isFailure = true)
|
||||
}
|
||||
@@ -249,6 +263,7 @@ open class TeamCityCIServer(
|
||||
|
||||
companion object {
|
||||
const val LOCAL_RUN_ID = "LOCAL_RUN_SNAPSHOT"
|
||||
private const val CODE_OWNER_METADATA_NAME = "Code Owner"
|
||||
|
||||
fun String.processStringForTC(): String {
|
||||
//todo replace to intellij.platform.testFramework.util.teamcity.escapeStringForTeamCity when module is published
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.intellij.ide.starter.di
|
||||
import com.intellij.ide.starter.buildTool.BuildTool
|
||||
import com.intellij.ide.starter.ci.CIServer
|
||||
import com.intellij.ide.starter.ci.NoCIServer
|
||||
import com.intellij.ide.starter.ci.teamcity.NoOpCodeOwnerResolver
|
||||
import com.intellij.ide.starter.ci.teamcity.CodeOwnerResolver
|
||||
import com.intellij.ide.starter.community.PublicIdeDownloader
|
||||
import com.intellij.ide.starter.config.ConfigurationStorage
|
||||
import com.intellij.ide.starter.config.ScrambleToolProvider
|
||||
@@ -99,6 +101,7 @@ private var _di = DI {
|
||||
bindSingleton<TargetResolver> { LocalOnlyTargetResolver }
|
||||
bindSingleton<ScrambleToolProvider> { object : ScrambleToolProvider {} }
|
||||
bindSingleton<DevBuildServerRunner> { NoOpDevBuildServerRunner }
|
||||
bindSingleton<CodeOwnerResolver> { NoOpCodeOwnerResolver }
|
||||
}.apply {
|
||||
logOutput("Starter DI was initialized")
|
||||
}
|
||||
|
||||
+4
-2
@@ -4,12 +4,14 @@ import java.time.LocalDateTime
|
||||
|
||||
data class TestMethod(
|
||||
val name: String,
|
||||
val clazz: String,
|
||||
val clazzSimpleName: String,
|
||||
val displayName: String,
|
||||
val testClass: Class<*>,
|
||||
val startTime: LocalDateTime = LocalDateTime.now(),
|
||||
var arguments: List<Any> = emptyList(),
|
||||
) {
|
||||
val clazzSimpleName: String = testClass.simpleName
|
||||
val clazz: String = testClass.name
|
||||
|
||||
fun argsString(): String = arguments.takeIf { it.isNotEmpty() }?.joinToString(prefix = "(", postfix = ")", separator = " ") ?: ""
|
||||
|
||||
fun fullName(): String {
|
||||
|
||||
Reference in New Issue
Block a user