[platform testFramework] provide ExtensionContext instead of the uniqueId in testFixtures

Merge-request: IJ-MR-150244
Merged-by: Shumaf Lovpache <soarex16@gmail.com>

GitOrigin-RevId: 0760acef462e162d66b7d7465f55e1c2ba1939c8
This commit is contained in:
Shumaf Lovpache
2025-01-14 18:05:12 +00:00
committed by intellij-monorepo-bot
parent 0649285d74
commit 4d76d2cac6
6 changed files with 135 additions and 16 deletions

View File

@@ -0,0 +1,25 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.testFramework.junit5.fixture
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.platform.commons.support.AnnotationSupport
import kotlin.jvm.optionals.getOrNull
internal class TestContextImpl(private val context: ExtensionContext) : TestContext {
override val uniqueId: String
get() = context.uniqueId
override val testName: String
get() = context.displayName
override fun <T : Annotation> findAnnotation(clazz: Class<T>): T? {
var extContext: ExtensionContext? = context
while (extContext != null) {
extContext.element.flatMap { AnnotationSupport.findAnnotation(it, clazz) }.getOrNull()?.let {
return it
}
extContext = extContext.parent.getOrNull()
}
return null
}
}

View File

@@ -38,7 +38,7 @@ internal class TestFixtureExtension : BeforeAllCallback,
for (field in fields) {
field.isAccessible = true
val fixture = field.get(testInstance) as TestFixtureImpl<*>
pendingFixtures.add(fixture.init(testScope, context.uniqueId))
pendingFixtures.add(fixture.init(testScope, TestContextImpl(context)))
}
awaitFixtureInitialization(testScope, pendingFixtures)
context.getStore(ExtensionContext.Namespace.GLOBAL).put("TestFixtureExtension", testScope)

View File

@@ -32,17 +32,17 @@ internal class TestFixtureImpl<T>(
}
}
fun init(testScope: CoroutineScope, uniqueId: String): Deferred<ScopedValue<T>> {
fun init(testScope: CoroutineScope, context: TestContext): Deferred<ScopedValue<T>> {
val state = _state
if (state !is TestFixtureInitializer<*>) {
@Suppress("UNCHECKED_CAST")
return state as Deferred<ScopedValue<T>>
}
return initSync(testScope, uniqueId)
return initSync(testScope, context)
}
@Synchronized // for simplicity; can be made atomic if needed
private fun initSync(testScope: CoroutineScope, uniqueId: String): Deferred<ScopedValue<T>> {
private fun initSync(testScope: CoroutineScope, context: TestContext): Deferred<ScopedValue<T>> {
val state = _state
if (state !is TestFixtureInitializer<*>) {
@Suppress("UNCHECKED_CAST")
@@ -58,10 +58,10 @@ internal class TestFixtureImpl<T>(
testScope.launch(CoroutineName(debugString)) {
@Suppress("UNCHECKED_CAST")
val initializer = state as TestFixtureInitializer<T>
val scope = TestFixtureInitializerReceiverImpl<T>(testScope, uniqueId)
val scope = TestFixtureInitializerReceiverImpl<T>(testScope, context)
val (fixture, tearDown) = try {
with(initializer) {
scope.initFixture(uniqueId) as InitializedTestFixtureData<T>
scope.initFixture(context) as InitializedTestFixtureData<T>
}
}
catch (t: Throwable) {
@@ -95,7 +95,7 @@ private typealias ScopedValue<T> = Pair<T, CoroutineScope>
private class TestFixtureInitializerReceiverImpl<T>(
private val testScope: CoroutineScope,
private val uniqueId: String,
private val context: TestContext,
) : TestFixtureInitializer.R<T> {
/**
@@ -104,7 +104,7 @@ private class TestFixtureInitializerReceiverImpl<T>(
private val _dependencies = LinkedHashSet<CoroutineScope>()
override suspend fun <T> TestFixture<T>.init(): T {
val (fixture, fixtureScope) = (this as TestFixtureImpl<T>).init(testScope, uniqueId).await()
val (fixture, fixtureScope) = (this as TestFixtureImpl<T>).init(testScope, context).await()
_dependencies.add(fixtureScope)
return fixture
}

View File

@@ -74,11 +74,11 @@ fun projectFixture(
@TestOnly
fun TestFixture<Project>.moduleFixture(
name: String? = null,
): TestFixture<Module> = testFixture(name ?: "unnamed module") { id ->
): TestFixture<Module> = testFixture(name ?: "unnamed module") { context ->
val project = this@moduleFixture.init()
val manager = ModuleManager.getInstance(project)
val module = writeAction {
manager.newNonPersistentModule(name ?: id, "")
manager.newNonPersistentModule(name ?: context.uniqueId, "")
}
initialized(module) {
writeAction {
@@ -123,8 +123,8 @@ fun TestFixture<Project>.moduleFixture(
}
@TestOnly
fun disposableFixture(): TestFixture<Disposable> = testFixture { debugString ->
val disposable = Disposer.newCheckedDisposable(debugString)
fun disposableFixture(): TestFixture<Disposable> = testFixture { context ->
val disposable = Disposer.newCheckedDisposable(context.uniqueId)
initialized(disposable) {
Disposer.dispose(disposable)
}

View File

@@ -38,18 +38,33 @@ sealed interface TestFixture<out T> {
fun get(): T
}
sealed interface TestContext {
/**
* Unique test or container ID, for example [org.junit.jupiter.api.extension.ExtensionContext.getUniqueId]
*/
val uniqueId: String
/**
* Display name for the current test or container, for example [org.junit.jupiter.api.extension.ExtensionContext.getDisplayName]
*/
val testName: String
/**
* Returns the annotation with which a test or container is marked or null if there is none.
*/
fun <T : Annotation> findAnnotation(clazz: Class<T>): T?
}
/**
* Represents the business logic for building a fixture (or resource) and destroying it.
*/
fun interface TestFixtureInitializer<T> {
/**
* @param uniqueId unique test or container ID, same as [org.junit.jupiter.api.extension.ExtensionContext.getUniqueId]
*
* TODO consider passing whole ExtensionContext here
* @param context [TestContext] which provides information about the test and allows to query annotations
*/
@OverrideOnly
suspend fun R<T>.initFixture(uniqueId: String): InitializedTestFixture<T>
suspend fun R<T>.initFixture(context: TestContext): InitializedTestFixture<T>
sealed interface InitializedTestFixture<T>

View File

@@ -0,0 +1,79 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.testFramework.junit5.fixture
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.DynamicTest
import org.junit.jupiter.api.DynamicTest.dynamicTest
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestFactory
private fun myFixture(): TestFixture<MyAnnotation?> = testFixture { context ->
initialized(context.findAnnotation(MyAnnotation::class.java)) { }
}
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
private annotation class MyAnnotation(val x: Int = 0)
private fun doTestContextTest(fixture: TestFixture<MyAnnotation?>, expectedValue: Int) {
val annotation = fixture.get()
assertNotNull(annotation)
assertEquals(expectedValue, annotation!!.x)
}
@MyAnnotation(x = 1)
@TestFixtures
class FixtureContextTest {
companion object {
@JvmStatic
private val classLevelFixture = myFixture()
}
private val testLevelFixture = myFixture()
@Test
fun `class level annotation with class level fixture`() = doTestContextTest(classLevelFixture, 1)
@Test
fun `class level annotation with test level fixture`() = doTestContextTest(testLevelFixture, 1)
@Test
@MyAnnotation(x = 2)
fun `test level annotation with class level fixture`() = doTestContextTest(classLevelFixture, 1)
@Test
@MyAnnotation(x = 2)
fun `test level annotation with test level fixture`() = doTestContextTest(testLevelFixture, 2)
@TestFactory
@MyAnnotation(x = 3)
fun `dynamic test`(): List<DynamicTest> {
return buildList {
repeat(3) {
add(dynamicTest("test $it") {
doTestContextTest(testLevelFixture, 3)
})
}
}
}
@Nested
@MyAnnotation(x = 4)
inner class InnerClass1 {
@Test
fun `inner class level annotation with outer class level fixture`() = doTestContextTest(classLevelFixture, 1)
}
@Nested
inner class InnerClass2 {
private val testLevelFixture = myFixture()
@Test
fun `outer class level annotation with test level fixture`() = doTestContextTest(classLevelFixture, 1)
@Test
@MyAnnotation(x = 5)
fun `test level annotation with inner test level fixture`() = doTestContextTest(testLevelFixture, 5)
}
}