mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-18 09:34:34 +07:00
PY-86778 [python] Migrate Python test environments from local builds to prebuilt standalone distributions
Replace Gradle-based local Python compilation with prebuilt standalone distributions downloaded from JetBrains cache redirector. Introduces provider-based architecture for test environment management with improved JUnit4/JUnit5 integration. Key changes: - Remove setup-test-environment Gradle module and build infrastructure - Introduce python-test-env modules (core, common, plain, conda, uv, junit4, junit5) - Implement PyEnvironmentProvider/PyEnvironmentSpec abstractions - Add JUnit5 annotations (@RequiresPoetry, @RequiresUv, @RunOnEnvironments) - Implement caching system with PyEnvDownloadCache and variant-specific directories - Predefined environments like: VANILLA_2_7, VANILLA_3_11, VANILLA_3_12, VANILLA_3_13 Benefits: - Faster test environment provisioning (no compilation required) - Consistent cross-platform behavior with identical Python builds - Improved reliability by eliminating platform-specific build failures - Better test framework integration with parameterized environment testing GitOrigin-RevId: a44957c3014f9e1fc7dbec347a7e245675f999c1
This commit is contained in:
committed by
intellij-monorepo-bot
parent
00eafdc932
commit
4f5b07293f
@@ -56,7 +56,6 @@ jvm_library(
|
||||
"//python/python-sdk:sdk_test_lib",
|
||||
"//python/junit5Tests-framework:community-junit5Tests-framework_test_lib",
|
||||
"//python/python-exec-service/execService.python",
|
||||
"//python/python-exec-service/execService.python:execService.python_test_lib",
|
||||
]
|
||||
)
|
||||
### auto-generated section `build intellij.python.community.services.internal.impl` end
|
||||
|
||||
-30
@@ -1,30 +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.python.junit5Tests.env.services.internal.impl
|
||||
|
||||
import com.intellij.python.community.services.internal.impl.VanillaPythonWithPythonInfoImpl
|
||||
import com.intellij.python.junit5Tests.framework.env.PyEnvTestCase
|
||||
import com.intellij.python.junit5Tests.framework.env.PythonBinaryPath
|
||||
import com.intellij.python.junit5Tests.randomBinary
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.Result
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@PyEnvTestCase
|
||||
class PythonWithLanguageLevelImplTest {
|
||||
@Test
|
||||
fun testRainyDay(): Unit = runBlocking {
|
||||
when (val r = VanillaPythonWithPythonInfoImpl.createByPythonBinary(randomBinary)) {
|
||||
is Result.Failure -> Unit
|
||||
is Result.Success -> fail("Unexpected success ${r.result}")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSunnyDay(@PythonBinaryPath pythonBinary: PythonBinary): Unit = runBlocking {
|
||||
val python = VanillaPythonWithPythonInfoImpl.createByPythonBinary(pythonBinary).orThrow()
|
||||
assertEquals(pythonBinary, python.pythonBinary, "Wrong python binary")
|
||||
assertTrue(python.pythonInfo.languageLevel.isPy3K, "Wrong python version")
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,6 @@ jvm_library(
|
||||
"//platform/testFramework/junit5:junit5_test_lib",
|
||||
"//libraries/hamcrest",
|
||||
"//python/python-exec-service/execService.python",
|
||||
"//python/python-exec-service/execService.python:execService.python_test_lib",
|
||||
"//platform/diagnostic",
|
||||
"//platform/util",
|
||||
]
|
||||
|
||||
@@ -71,13 +71,10 @@ jvm_library(
|
||||
"//python/junit5Tests-framework:community-junit5Tests-framework_test_lib",
|
||||
"//python/installer",
|
||||
"//python/python-venv:community-impl-venv",
|
||||
"//python/python-venv:community-impl-venv_test_lib",
|
||||
"//python/setup-test-environment:community-testFramework-testEnv",
|
||||
"//platform/testFramework",
|
||||
"//platform/testFramework:testFramework_test_lib",
|
||||
"//python/python-psi-impl:psi-impl",
|
||||
"//python/python-exec-service/execService.python",
|
||||
"//python/python-exec-service/execService.python:execService.python_test_lib",
|
||||
],
|
||||
exports = [
|
||||
"//python/services/shared",
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
<orderEntry type="module" module-name="intellij.python.community.junit5Tests.framework" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.impl.installer" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.impl.venv" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.testFramework.testEnv" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.psi.impl" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.execService.python" />
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ internal suspend fun getCacheTimeout(): Duration? =
|
||||
@State(name = "SystemPythonService", storages = [Storage("systemPythonService.xml", roamingType = RoamingType.LOCAL)],
|
||||
allowLoadInTests = true)
|
||||
@Internal
|
||||
internal class SystemPythonServiceImpl(scope: CoroutineScope) : SystemPythonService, SimplePersistentStateComponent<MyServiceState>(MyServiceState()) {
|
||||
class SystemPythonServiceImpl(scope: CoroutineScope) : SystemPythonService, SimplePersistentStateComponent<MyServiceState>(MyServiceState()) {
|
||||
private val findPythonsMutex = Mutex()
|
||||
private val _cacheImpl: CompletableDeferred<Cache<EelDescriptor, SystemPython>?> = CompletableDeferred()
|
||||
private suspend fun cache() = _cacheImpl.await()
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
<plugin id="com.intellij.modules.python"/>
|
||||
</dependencies>
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<applicationInitializedListener implementation="com.intellij.python.junit5Tests.env.systemPython.SystemPythonRootsFixer"/>
|
||||
</extensions>
|
||||
<extensions defaultExtensionNs="Pythonid">
|
||||
<systemPythonProvider implementation="com.intellij.python.junit5Tests.env.systemPython.impl.EnvTestPythonProvider"/>
|
||||
<applicationInitializedListener implementation="com.intellij.python.junit5Tests.env.systemPython.impl.SystemPythonRootsFixer"/>
|
||||
</extensions>
|
||||
</idea-plugin>
|
||||
+1
-7
@@ -1,14 +1,8 @@
|
||||
<idea-plugin>
|
||||
<dependencies>
|
||||
<module name="intellij.platform.testFramework.junit5"/>
|
||||
<module name="intellij.python.community.junit5Tests.framework._test"/>
|
||||
<module name="intellij.python.community.services.systemPython"/>
|
||||
</dependencies>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<applicationInitializedListener implementation="com.intellij.python.junit5Tests.env.systemPython.SystemPythonRootsFixer"/>
|
||||
</extensions>
|
||||
<extensions defaultExtensionNs="Pythonid">
|
||||
<systemPythonProvider implementation="com.intellij.python.junit5Tests.env.systemPython.impl.EnvTestPythonProvider"/>
|
||||
<applicationInitializedListener implementation="com.intellij.python.junit5Tests.env.systemPython.impl.SystemPythonRootsFixer"/>
|
||||
</extensions>
|
||||
</idea-plugin>
|
||||
Vendored
-20
@@ -1,20 +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.python.junit5Tests.env.systemPython
|
||||
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonService
|
||||
import com.intellij.python.junit5Tests.framework.env.PyEnvTestCase
|
||||
import com.intellij.testFramework.common.timeoutRunBlocking
|
||||
import com.jetbrains.python.getOrThrow
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@PyEnvTestCase
|
||||
class Py27Test {
|
||||
@Test
|
||||
fun testPy27(): Unit = timeoutRunBlocking {
|
||||
val testEnvironments = SystemPythonService().findSystemPythons()
|
||||
val python27 = testEnvironments.firstOrNull { it.pythonInfo.languageLevel == LanguageLevel.PYTHON27 }
|
||||
?: error("No 2.7 found in $testEnvironments")
|
||||
SystemPythonService().registerSystemPython(python27.pythonBinary).getOrThrow()
|
||||
}
|
||||
}
|
||||
-112
@@ -1,112 +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.python.junit5Tests.env.systemPython
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.platform.eel.EelApi
|
||||
import com.intellij.platform.eel.ExecuteProcessException
|
||||
import com.intellij.platform.eel.ThrowsChecked
|
||||
import com.intellij.platform.eel.provider.getEelDescriptor
|
||||
import com.intellij.platform.eel.provider.toEelApi
|
||||
import com.intellij.platform.eel.provider.utils.readWholeText
|
||||
import com.intellij.platform.eel.spawnProcess
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonProvider
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonService
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonServiceImpl
|
||||
import com.intellij.python.junit5Tests.assertFail
|
||||
import com.intellij.python.junit5Tests.framework.env.PyEnvTestCase
|
||||
import com.intellij.python.junit5Tests.framework.env.PythonBinaryPath
|
||||
import com.intellij.python.junit5Tests.randomBinary
|
||||
import com.intellij.testFramework.common.timeoutRunBlocking
|
||||
import com.intellij.testFramework.junit5.RegistryKey
|
||||
import com.intellij.testFramework.junit5.TestDisposable
|
||||
import com.intellij.testFramework.registerExtension
|
||||
import com.jetbrains.python.PyToolUIInfo
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.MessageError
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
|
||||
import kotlinx.coroutines.async
|
||||
import org.hamcrest.MatcherAssert
|
||||
import org.hamcrest.Matchers
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@PyEnvTestCase
|
||||
class SystemPythonServiceShowCaseTest {
|
||||
|
||||
@ThrowsChecked(ExecuteProcessException::class)
|
||||
@Test
|
||||
fun testListPythons(): Unit = timeoutRunBlocking(10.minutes) {
|
||||
for (systemPython in SystemPythonService().findSystemPythons(forceRefresh = true)) {
|
||||
fileLogger().info("Python found: $systemPython")
|
||||
val eelApi = systemPython.pythonBinary.getEelDescriptor().toEelApi()
|
||||
val process = eelApi.exec.spawnProcess(systemPython.pythonBinary.pathString, "--version").eelIt()
|
||||
val output = async {
|
||||
(if (systemPython.pythonInfo.languageLevel.isPy3K) process.stdout else process.stderr).readWholeText()
|
||||
}
|
||||
Assertions.assertTrue(process.exitCode.await() == 0)
|
||||
val versionString = PythonSdkFlavor.getLanguageLevelFromVersionStringStaticSafe(output.await())!!
|
||||
Assertions.assertEquals(systemPython.pythonInfo.languageLevel, versionString, "Wrong version")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCustomPythonRainyDay(): Unit = timeoutRunBlocking(10.minutes) {
|
||||
SystemPythonService().registerSystemPython(randomBinary).assertFail()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRegister(@PythonBinaryPath path: PythonBinary): Unit = timeoutRunBlocking(10.minutes) {
|
||||
val sut = SystemPythonService()
|
||||
repeat(10) {
|
||||
sut.registerSystemPython(path).orThrow()
|
||||
}
|
||||
val pythons = sut.findSystemPythons(forceRefresh = true).map { it.pythonBinary }
|
||||
MatcherAssert.assertThat("No registered python", pythons, Matchers.hasItem(path))
|
||||
Assertions.assertEquals(pythons.distinct().size, pythons.size, "Duplicates found")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRefresh(@TestDisposable disposable: Disposable): Unit = timeoutRunBlocking(10.minutes) {
|
||||
val provider = CountingTestProvider(Result.failure(MessageError("...")))
|
||||
val sut = SystemPythonService()
|
||||
// Warm up cache before registering test provider
|
||||
sut.findSystemPythons()
|
||||
ApplicationManager.getApplication().registerExtension(SystemPythonProvider.EP, provider, disposable)
|
||||
repeat(10) {
|
||||
sut.findSystemPythons()
|
||||
}
|
||||
Assertions.assertTrue(provider.calls == 0, "Provider should not be called while cache is valid")
|
||||
sut.findSystemPythons(forceRefresh = true)
|
||||
Assertions.assertTrue(provider.calls >= 1, "Provider should be called after force refresh")
|
||||
}
|
||||
|
||||
@RegistryKey("python.system.refresh.minutes", "0")
|
||||
@Test
|
||||
fun testDisableCache(@TestDisposable disposable: Disposable): Unit = timeoutRunBlocking(10.minutes) {
|
||||
val timesToRepeat = 5
|
||||
val provider = CountingTestProvider(Result.success(emptySet()))
|
||||
val sut = SystemPythonServiceImpl(this)
|
||||
ApplicationManager.getApplication().registerExtension(SystemPythonProvider.EP, provider, disposable)
|
||||
repeat(timesToRepeat) {
|
||||
sut.findSystemPythons()
|
||||
}
|
||||
Assertions.assertTrue(provider.calls == timesToRepeat)
|
||||
}
|
||||
|
||||
private class CountingTestProvider(
|
||||
private val result: PyResult<Set<PythonBinary>>,
|
||||
override val uiCustomization: PyToolUIInfo? = null,
|
||||
) : SystemPythonProvider {
|
||||
var calls: Int = 0
|
||||
override suspend fun findSystemPythons(eelApi: EelApi): PyResult<Set<PythonBinary>> {
|
||||
calls++
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
-63
@@ -1,63 +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.python.junit5Tests.env.systemPython.impl
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.platform.eel.EelApi
|
||||
import com.intellij.python.community.impl.venv.createVenv
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonProvider
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonService
|
||||
import com.intellij.python.junit5Tests.framework.env.PyEnvTestCase
|
||||
import com.intellij.python.junit5Tests.framework.env.PythonBinaryPath
|
||||
import com.intellij.testFramework.common.timeoutRunBlocking
|
||||
import com.intellij.testFramework.junit5.TestDisposable
|
||||
import com.intellij.testFramework.registerExtension
|
||||
import com.jetbrains.python.PyToolUIInfo
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.getOrThrow
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import java.nio.file.Path
|
||||
|
||||
@PyEnvTestCase
|
||||
class EnvProviderTest {
|
||||
|
||||
@Test
|
||||
fun testPythonProvider(@PythonBinaryPath python: PythonBinary): Unit = timeoutRunBlocking {
|
||||
val systemPythons = SystemPythonService().findSystemPythons()
|
||||
val systemPythonBinaries = systemPythons.map { it.pythonBinary }
|
||||
assertThat("No env python registered", systemPythonBinaries, Matchers.hasItem(python))
|
||||
|
||||
if (systemPythons.size > 1) {
|
||||
val best = systemPythons.first()
|
||||
for (python in systemPythons.subList(1, systemPythonBinaries.size)) {
|
||||
assertTrue(python.pythonInfo.languageLevel <= best.pythonInfo.languageLevel, "$best is the first, bust worse than $python")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testProviderWithUi(
|
||||
@TestDisposable disposable: Disposable,
|
||||
@PythonBinaryPath python: PythonBinary,
|
||||
@TempDir venvDir: Path,
|
||||
): Unit = timeoutRunBlocking {
|
||||
val venvPython = createVenv(python, venvDir).getOrThrow()
|
||||
val ui = PyToolUIInfo("myui")
|
||||
val provider = InlineTestProvider(setOf(venvPython), ui)
|
||||
ApplicationManager.getApplication().registerExtension(SystemPythonProvider.EP, provider, disposable)
|
||||
val python = SystemPythonService().findSystemPythons(forceRefresh = true).first { it.pythonBinary == venvPython }
|
||||
assertTrue(ui == python.ui, "Wrong UI")
|
||||
}
|
||||
|
||||
private class InlineTestProvider(
|
||||
private val pythons: Set<PythonBinary>,
|
||||
override val uiCustomization: PyToolUIInfo?,
|
||||
) : SystemPythonProvider {
|
||||
override suspend fun findSystemPythons(eelApi: EelApi) = Result.success(pythons)
|
||||
}
|
||||
}
|
||||
-41
@@ -1,41 +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.python.junit5Tests.env.systemPython.impl
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.platform.eel.EelApi
|
||||
import com.intellij.platform.eel.provider.localEel
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonProvider
|
||||
import com.intellij.python.community.testFramework.testEnv.TypeVanillaPython
|
||||
import com.intellij.python.community.testFramework.testEnv.TypeVanillaPython3
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.toSet
|
||||
|
||||
/**
|
||||
* Register tests pythons as system pythons
|
||||
*/
|
||||
internal class EnvTestPythonProvider : SystemPythonProvider {
|
||||
override suspend fun findSystemPythons(eelApi: EelApi): PyResult<Set<PythonBinary>> {
|
||||
var pythons = emptySet<PythonBinary>()
|
||||
if (eelApi == localEel) {
|
||||
// Add Py27 temporary to test Py27
|
||||
// It is perfectly valid not to find any python because some tests might run without a python and still have this module on a class-path
|
||||
pythons = merge(TypeVanillaPython3.getTestEnvironments(), TypeVanillaPython2.getTestEnvironments())
|
||||
.map { (python, closeable) ->
|
||||
Disposer.register(ApplicationManager.getApplication()) {
|
||||
closeable.close()
|
||||
}
|
||||
|
||||
python
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
return Result.success(pythons)
|
||||
}
|
||||
}
|
||||
|
||||
private object TypeVanillaPython2 : TypeVanillaPython("python2.7")
|
||||
+4
-2
@@ -1,18 +1,20 @@
|
||||
// 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.python.junit5Tests.env.systemPython
|
||||
package com.intellij.python.junit5Tests.env.systemPython.impl
|
||||
|
||||
import com.intellij.ide.ApplicationInitializedListener
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonService
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
@TestOnly
|
||||
internal class SystemPythonRootsFixer : ApplicationInitializedListener {
|
||||
override suspend fun execute() {
|
||||
val disposable = ApplicationManager.getApplication()
|
||||
val pythonDirs = SystemPythonService()
|
||||
.findSystemPythons()
|
||||
.map { it.pythonBinary.parent.pathString }
|
||||
.map { it.pythonBinary.toRealPath().parent.pathString }
|
||||
.toTypedArray()
|
||||
VfsRootAccess.allowRootAccess(disposable, *pythonDirs)
|
||||
}
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@ApiStatus.Internal
|
||||
package com.intellij.python.junit5Tests.env.systemPython.impl;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
Reference in New Issue
Block a user