[junit5] ability to specify log level for testing

followup for IJ-CR-181003

GitOrigin-RevId: a736aa6b750ebd0061117c2d183ac66ffe286360
This commit is contained in:
Max Medvedev
2025-11-11 16:11:48 +00:00
committed by intellij-monorepo-bot
parent cfe8841906
commit 162f677c8e
9 changed files with 124 additions and 141 deletions
@@ -5,7 +5,6 @@ import com.intellij.codeInsight.multiverse.*
import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.application.readAction
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.diagnostic.LogLevel
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.rootManager
import com.intellij.platform.testFramework.junit5.projectStructure.fixture.withSharedSourceEnabled
@@ -13,7 +12,7 @@ import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiFile
import com.intellij.psi.impl.PsiManagerEx
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.junit5.LogLevelWithClass
import com.intellij.testFramework.junit5.EnableTracingFor
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.junit5.fixture.*
import kotlinx.coroutines.ensureActive
@@ -24,9 +23,8 @@ import org.junit.jupiter.api.Test
import kotlin.time.Duration.Companion.seconds
@TestApplication
@LogLevelWithClass(
category = CodeInsightContextManagerImpl::class,
level = LogLevel.TRACE
@EnableTracingFor(
categoryClasses = [CodeInsightContextManagerImpl::class],
)
internal class FileContextTest {
companion object {
@@ -4,7 +4,6 @@ package com.intellij.psi.impl.file.impl
import com.intellij.codeInsight.multiverse.*
import com.intellij.openapi.application.readAction
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.diagnostic.LogLevel
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
@@ -17,7 +16,7 @@ import com.intellij.psi.PsiManager
import com.intellij.psi.impl.PsiManagerImpl
import com.intellij.psi.util.PsiUtilCore.ensureValid
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.junit5.LogLevelWithClass
import com.intellij.testFramework.junit5.EnableTracingFor
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.junit5.fixture.fileOrDirInProjectFixture
import com.intellij.testFramework.junit5.fixture.moduleInProjectFixture
@@ -27,13 +26,9 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.fail
@LogLevelWithClass(
category = CodeInsightContextManagerImpl::class,
level = LogLevel.TRACE
)
@com.intellij.testFramework.junit5.LogLevel(
category = "#com.intellij.psi.impl.file.impl.MultiverseFileViewProviderCache",
level = LogLevel.TRACE
@EnableTracingFor(
categories = ["#com.intellij.psi.impl.file.impl.MultiverseFileViewProviderCache"],
categoryClasses = [CodeInsightContextManagerImpl::class]
)
@TestApplication
internal class FileMoveTest {
@@ -4,7 +4,6 @@ package com.intellij.psi.impl.file.impl
import com.intellij.codeInsight.multiverse.*
import com.intellij.openapi.application.readAction
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.diagnostic.LogLevel
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
@@ -18,7 +17,7 @@ import com.intellij.psi.impl.PsiManagerImpl
import com.intellij.psi.util.PsiUtilCore.ensureValid
import com.intellij.testFramework.IndexingTestUtil
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.junit5.LogLevelWithClass
import com.intellij.testFramework.junit5.EnableTracingFor
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.junit5.fixture.fileOrDirInProjectFixture
import com.intellij.testFramework.junit5.fixture.moduleInProjectFixture
@@ -28,13 +27,9 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.fail
@LogLevelWithClass(
category = CodeInsightContextManagerImpl::class,
level = LogLevel.TRACE
)
@com.intellij.testFramework.junit5.LogLevel(
category = "#com.intellij.psi.impl.file.impl.MultiverseFileViewProviderCache",
level = LogLevel.TRACE
@EnableTracingFor(
categories = ["#com.intellij.psi.impl.file.impl.MultiverseFileViewProviderCache"],
categoryClasses = [CodeInsightContextManagerImpl::class],
)
@TestApplication
internal class FileMoveWithWaitForIndexingTest {
@@ -0,0 +1,26 @@
// 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.testFramework.junit5
import com.intellij.testFramework.junit5.impl.EnableTracingExtension
import org.jetbrains.annotations.TestOnly
import org.junit.jupiter.api.extension.ExtendWith
import kotlin.reflect.KClass
/**
* Enables tracing of the specified logging categories inside the annotated test class or method.
* The corresponding messages will be present in the log file on disk.
* Also, they will be visible in <DEBUG log> which is printed to str-error if the test fails.
*
* After the annotated test/function finishes, the logger will be restored to DEBUG level.
*
* For convenience, you can specify logging categories by their fully qualified names and/or by their classes.
* If both categories and categoryClasses are specified, all of them will be enabled.
*/
@TestOnly
@Repeatable
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@ExtendWith(EnableTracingExtension::class)
annotation class EnableTracingFor(
val categories: Array<String> = [],
val categoryClasses: Array<KClass<*>> = [],
)
@@ -1,27 +0,0 @@
// 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.testFramework.junit5
import com.intellij.openapi.diagnostic.LogLevel
import com.intellij.testFramework.junit5.impl.LogLevelExtension
import com.intellij.testFramework.junit5.impl.LogLevelWithClassExtension
import org.jetbrains.annotations.TestOnly
import org.junit.jupiter.api.extension.ExtendWith
import kotlin.reflect.KClass
@TestOnly
@Repeatable
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@ExtendWith(LogLevelExtension::class)
annotation class LogLevel(
val category: String,
val level: LogLevel,
)
@TestOnly
@Repeatable
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@ExtendWith(LogLevelWithClassExtension::class)
annotation class LogLevelWithClass(
val category: KClass<*>,
val level: LogLevel,
)
@@ -0,0 +1,54 @@
// 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.testFramework.junit5.impl
import com.intellij.openapi.diagnostic.LogLevel
import com.intellij.openapi.diagnostic.Logger
import com.intellij.testFramework.junit5.EnableTracingFor
import org.jetbrains.annotations.TestOnly
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.InvocationInterceptor
import org.junit.platform.commons.support.AnnotationSupport
@TestOnly
internal class EnableTracingExtension : AbstractInvocationInterceptor() {
override fun <T> intercept(invocation: InvocationInterceptor.Invocation<T>, context: ExtensionContext): T {
val annotations = collectAnnotations<EnableTracingFor>(context).ifEmpty {
return invocation.proceed()
}
val loggers = annotations.flatMap { annotation ->
annotation.categories.map { Logger.getInstance(it) } +
annotation.categoryClasses.map { Logger.getInstance(it.java) }
}
return doWithTracingEnabled(loggers, invocation)
}
private fun <T> doWithTracingEnabled(
infos: List<Logger>,
invocation: InvocationInterceptor.Invocation<T>,
): T {
val valuesBefore = infos.map { info -> enableTracing(info) }
try {
return invocation.proceed()
}
finally {
valuesBefore.asReversed().forEach { it() }
}
}
private fun enableTracing(logger: Logger): () -> Unit {
logger.setLevel(LogLevel.TRACE)
return {
logger.setLevel(LogLevel.INFO)
}
}
private inline fun <reified C : Annotation> collectAnnotations(context: ExtensionContext): List<C> {
val annotations =
AnnotationSupport.findRepeatableAnnotations(context.testClass, C::class.java) +
AnnotationSupport.findRepeatableAnnotations(context.element, C::class.java)
return annotations.distinct()
}
}
@@ -1,79 +0,0 @@
// 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.testFramework.junit5.impl
import com.intellij.openapi.diagnostic.Logger
import com.intellij.testFramework.junit5.LogLevel
import com.intellij.testFramework.junit5.LogLevelWithClass
import org.jetbrains.annotations.TestOnly
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.InvocationInterceptor
import org.junit.platform.commons.support.AnnotationSupport
@TestOnly
internal class LogLevelExtension : AbstractInvocationInterceptor() {
override fun <T> intercept(invocation: InvocationInterceptor.Invocation<T>, context: ExtensionContext): T {
val annotations = collectAnnotations<LogLevel>(context).ifEmpty {
return invocation.proceed()
}
val infos = annotations.map {
LogLevelInfo(Logger.getInstance(it.category), it.level)
}
return doWithLogLevelInstalled(infos, invocation)
}
}
@TestOnly
internal class LogLevelWithClassExtension : AbstractInvocationInterceptor() {
override fun <T> intercept(invocation: InvocationInterceptor.Invocation<T>, context: ExtensionContext): T {
val annotations = collectAnnotations<LogLevelWithClass>(context).ifEmpty {
return invocation.proceed()
}
val infos = annotations.map {
LogLevelInfo(Logger.getInstance(it.category.java), it.level)
}
return doWithLogLevelInstalled(infos, invocation)
}
}
private inline fun <reified C : Annotation> collectAnnotations(context: ExtensionContext): List<C> {
val annotations =
AnnotationSupport.findRepeatableAnnotations(context.testClass, C::class.java) +
AnnotationSupport.findRepeatableAnnotations(context.element, C::class.java)
return annotations.distinct()
}
private data class LogLevelInfo(
val logger: Logger,
val level: com.intellij.openapi.diagnostic.LogLevel,
)
private fun <T> doWithLogLevelInstalled(
infos: List<LogLevelInfo>,
invocation: InvocationInterceptor.Invocation<T>,
): T {
val valuesBefore = infos.map { info ->
setLogLevel(info)
}
try {
return invocation.proceed()
}
finally {
for (handle in valuesBefore.asReversed()) {
handle()
}
}
}
private fun setLogLevel(info: LogLevelInfo): () -> Unit {
val (logger, logLevel) = info
val prevLevel = logger.level
logger.level = logLevel
return {
logger.level = prevLevel
}
}
@@ -0,0 +1,29 @@
// 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.testFramework.junit5.showcase
import com.intellij.openapi.diagnostic.Logger
import com.intellij.testFramework.junit5.EnableTracingFor
import org.junit.jupiter.api.Test
// MyBusinessLogic logger works with the TRACE level inside the test run.
@EnableTracingFor(categoryClasses = [MyBusinessLogic::class])
class JUnit5EnableTracingInTest {
@Test
fun `traces are written to log file`() {
MyBusinessLogic().run()
}
}
private class MyBusinessLogic {
private val LOG = Logger.getInstance(MyBusinessLogic::class.java)
fun run() {
LOG.trace("MyBusinessLogic#run started")
// doing stuff ...
LOG.info("MyBusinessLogic#run finished")
}
}
@@ -4,7 +4,10 @@ package com.intellij.openapi.diagnostic;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.ExceptionUtil;
import org.apache.log4j.Level;
import org.jetbrains.annotations.*;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.lang.reflect.Constructor;
import java.util.Collection;
@@ -488,17 +491,6 @@ public abstract class Logger {
error(getClass() + " should override '#setLevel(LogLevel)'");
}
@ApiStatus.Internal
public LogLevel getLevel() {
if (isTraceEnabled()) {
return LogLevel.TRACE;
}
if (isDebugEnabled()) {
return LogLevel.DEBUG;
}
return LogLevel.INFO;
}
private static final boolean ourRethrowCE = "true".equals(System.getProperty("idea.log.rethrow.ce", "true"));
public static boolean shouldRethrow(@NotNull Throwable t) {