From 1be9caaac92e3baf84dc2f8b8061f8fd99ce247d Mon Sep 17 00:00:00 2001 From: "Ilya.Kazakevich" Date: Sun, 2 Feb 2025 05:37:32 +0100 Subject: [PATCH] Python tests: separate conda from vanilla python env types for tests. With this change we can move conda into the separate module to decouple test framework from community.impl GitOrigin-RevId: 589dc29f39d9d46252681c8f31b8fe482e63595f --- .idea/modules.xml | 2 +- .../env/impl/CondaPythonEnvExtension.kt | 4 +- .../env/impl/PythonEnvExtensionBase.kt | 4 +- .../env/impl/VanillaPythonEnvExtension.kt | 6 +- .../junit5Tests/framework/env/sdkFixture.kt | 3 +- .../impl/EnvTestPythonProvider.kt | 4 +- ...python.community.testFramework.testEnv.iml | 1 + .../testFramework/testEnv/PythonType.kt | 81 +++---------------- .../testFramework/testEnv/TypeConda.kt | 52 ++++++++++++ .../testEnv/TypeVanillaPython3.kt | 28 +++++++ .../src/com/jetbrains/python/tools/sdk.kt | 18 +---- .../env/python/conda/LocalCondaRule.kt | 4 +- 12 files changed, 110 insertions(+), 97 deletions(-) create mode 100644 python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/TypeConda.kt create mode 100644 python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/TypeVanillaPython3.kt diff --git a/.idea/modules.xml b/.idea/modules.xml index 27ad19ae9e8f..ec315aad9b90 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -943,7 +943,6 @@ - @@ -957,6 +956,7 @@ + diff --git a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/CondaPythonEnvExtension.kt b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/CondaPythonEnvExtension.kt index 8666f5beec12..942fc3800f5b 100644 --- a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/CondaPythonEnvExtension.kt +++ b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/CondaPythonEnvExtension.kt @@ -1,11 +1,11 @@ // 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.framework.env.impl -import com.intellij.python.community.testFramework.testEnv.PythonType +import com.intellij.python.community.testFramework.testEnv.TypeConda import com.intellij.python.junit5Tests.framework.env.CondaEnv import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv /** * Mark [PyCondaEnv] param with [CondaEnv] annotation and register this extension with [com.intellij.python.junit5Tests.framework.env.PyEnvTestCaseWithConda] */ -internal class CondaPythonEnvExtension : PythonEnvExtensionBase(CondaEnv::class, PythonType.Conda, PyCondaEnv::class) \ No newline at end of file +internal class CondaPythonEnvExtension : PythonEnvExtensionBase(CondaEnv::class, TypeConda, PyCondaEnv::class) \ No newline at end of file diff --git a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/PythonEnvExtensionBase.kt b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/PythonEnvExtensionBase.kt index ba07f4a277b1..594c64aff5ec 100644 --- a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/PythonEnvExtensionBase.kt +++ b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/PythonEnvExtensionBase.kt @@ -52,8 +52,8 @@ internal abstract class PythonEnvExtensionBase { - val (env, autoClosable) = runBlocking { - pythonType.getTestEnvironment(*additionalTags).getOrElse { + val (_, autoClosable, env) = runBlocking { + pythonType.createSdkClosableEnv(*additionalTags).getOrElse { // Logging due to IDEA-356206 LOG.warn(it) val message = "Couldn't find python to run test against ($pythonType , ${additionalTags.toList()})" diff --git a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/VanillaPythonEnvExtension.kt b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/VanillaPythonEnvExtension.kt index 9a42fe095728..113d664ea409 100644 --- a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/VanillaPythonEnvExtension.kt +++ b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/VanillaPythonEnvExtension.kt @@ -6,7 +6,7 @@ import com.intellij.execution.process.CapturingProcessHandler import com.intellij.ide.util.PropertiesComponent import com.intellij.openapi.util.SystemInfoRt import com.intellij.python.community.impl.poetry.poetryPath -import com.intellij.python.community.testFramework.testEnv.PythonType +import com.intellij.python.community.testFramework.testEnv.TypeVanillaPython3 import com.intellij.python.junit5Tests.framework.env.PythonBinaryPath import com.jetbrains.python.PythonBinary import java.nio.file.Path @@ -16,9 +16,9 @@ import java.nio.file.Path * * It also searches for poetry and stores it in [PropertiesComponent] */ -internal class VanillaPythonEnvExtension : PythonEnvExtensionBase( +internal class VanillaPythonEnvExtension : PythonEnvExtensionBase( annotation = PythonBinaryPath::class, - pythonType = PythonType.VanillaPython3, + pythonType = TypeVanillaPython3, envType = PythonBinary::class, lazy = false, additionalTags = arrayOf("poetry") diff --git a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/sdkFixture.kt b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/sdkFixture.kt index 8b84eddadc33..89c27bff7a78 100644 --- a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/sdkFixture.kt +++ b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/sdkFixture.kt @@ -8,6 +8,7 @@ import com.intellij.openapi.module.Module import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.openapi.projectRoots.Sdk import com.intellij.python.community.testFramework.testEnv.PythonType +import com.intellij.python.community.testFramework.testEnv.TypeVanillaPython3 import com.intellij.testFramework.junit5.fixture.TestFixture import com.intellij.testFramework.junit5.fixture.testFixture import com.jetbrains.python.sdk.configuration.createVirtualEnvSynchronously @@ -25,7 +26,7 @@ typealias PySdkFixture = TestFixture * Creates [Sdk] (if you only need a python path, use [PythonBinaryPath] or [CondaEnv]) */ fun pySdkFixture( - pythonType: PythonType<*> = PythonType.VanillaPython3, + pythonType: PythonType<*> = TypeVanillaPython3, ): PySdkFixture = testFixture { val (sdk, autoClosable) = com.jetbrains.python.tools.createSdk(LocalPython(pythonType)) sdk.persist() diff --git a/python/services/system-python/tests/com/intellij/python/junit5Tests/env/systemPython/impl/EnvTestPythonProvider.kt b/python/services/system-python/tests/com/intellij/python/junit5Tests/env/systemPython/impl/EnvTestPythonProvider.kt index dd3043083c86..f43c11e06472 100644 --- a/python/services/system-python/tests/com/intellij/python/junit5Tests/env/systemPython/impl/EnvTestPythonProvider.kt +++ b/python/services/system-python/tests/com/intellij/python/junit5Tests/env/systemPython/impl/EnvTestPythonProvider.kt @@ -5,7 +5,7 @@ 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.testFramework.testEnv.PythonType +import com.intellij.python.community.testFramework.testEnv.TypeVanillaPython3 import com.jetbrains.python.PythonBinary import com.jetbrains.python.systemPythonSpi.SystemPythonProvider import kotlinx.coroutines.flow.map @@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.toSet internal class EnvTestPythonProvider : SystemPythonProvider { override suspend fun findSystemPythons(eelApi: EelApi): Set { if (eelApi != localEel) return emptySet() - return PythonType.VanillaPython3 + return TypeVanillaPython3 .getTestEnvironments() .map { (python, closeable) -> Disposer.register(ApplicationManager.getApplication()) { diff --git a/python/setup-test-environment/intellij.python.community.testFramework.testEnv.iml b/python/setup-test-environment/intellij.python.community.testFramework.testEnv.iml index 1d54991f73f5..e10464c00407 100644 --- a/python/setup-test-environment/intellij.python.community.testFramework.testEnv.iml +++ b/python/setup-test-environment/intellij.python.community.testFramework.testEnv.iml @@ -15,5 +15,6 @@ + \ No newline at end of file diff --git a/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/PythonType.kt b/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/PythonType.kt index 34d711fc7674..bfe9d9289e96 100644 --- a/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/PythonType.kt +++ b/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/PythonType.kt @@ -1,32 +1,22 @@ // 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.community.testFramework.testEnv -import com.intellij.execution.processTools.getResultStdout -import com.intellij.execution.target.local.LocalTargetEnvironmentRequest -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess +import com.intellij.openapi.projectRoots.Sdk import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.PythonBinary -import com.jetbrains.python.packaging.findCondaExecutableRelativeToEnv -import com.jetbrains.python.sdk.conda.TargetEnvironmentRequestCommandExecutor import com.jetbrains.python.sdk.flavors.PythonSdkFlavor -import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv -import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity import com.jetbrains.python.venvReader.VirtualEnvReader -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* -import kotlinx.coroutines.runBlocking import org.jetbrains.annotations.NonNls import java.nio.file.Path import java.util.concurrent.ConcurrentHashMap import kotlin.Result.Companion.failure /** - * Gradle script installs two types of python: conda and vanilla. Env could be obtained by [getTestEnvironment] which also provides closable + * Gradle script installs two types of python: conda and vanilla. Env could be obtained by [createSdkClosableEnv] which also provides closable * to clean up after the usage */ -sealed class PythonType(private val tag: @NonNls String? = null) { +abstract class PythonType(private val tag: @NonNls String) { private companion object { val cache: MutableMap, List> = ConcurrentHashMap() } @@ -65,72 +55,23 @@ sealed class PythonType(private val tag: @NonNls String? = null) { /** - * Returns first (whatever it means) test environment and closable that must be closed after the test + * Returns sdk, (whatever it means) test environment and closable that must be closed after the test */ - suspend fun getTestEnvironment(vararg additionalTags: @NonNls String): Result> = - getTestEnvironments(*additionalTags).firstOrNull()?.let { Result.success(it) } + suspend fun createSdkClosableEnv(vararg additionalTags: @NonNls String): Result> = + getTestEnvironments(*additionalTags).firstOrNull()?.let { (env, closable) -> + Result.success(Triple(createSdkFor(env), closable, env)) + } ?: failure(AssertionError("No python found. See ${PyEnvTestSettings::class} class for more info")) + protected abstract suspend fun createSdkFor(t: T): Sdk + protected abstract suspend fun pythonPathToEnvironment(pythonBinary: PythonBinary, envDir: Path): Pair - data object VanillaPython3 : PythonType() { - // Python is directly executable - override suspend fun pythonPathToEnvironment(pythonBinary: PythonBinary, envDir: Path): Pair { - val disposable = Disposer.newDisposable("Python tests disposable for VfsRootAccess") - // We might have python installation outside the project root, but we still need to have access to it. - VfsRootAccess.allowRootAccess(disposable, pythonBinary.parent.toString()) - return Pair(pythonBinary, AutoCloseable { - Disposer.dispose(disposable) - }) - } - } - - - data object Conda : PythonType("conda") { - - override suspend fun pythonPathToEnvironment(pythonBinary: PythonBinary, envDir: Path): Pair { - // First, find python binary, then calculate conda from it as env stores "conda" as a regular env - val condaPath = findCondaExecutableRelativeToEnv(pythonBinary) ?: error("Conda root $pythonBinary doesn't have conda binary") - - // We'll remove then on close - val condaEnvsBeforeTest = getCondaNames(condaPath) - - val cleanupCondas = AutoCloseable { - runBlocking(Dispatchers.IO) { - val condasToRemove = getCondaNames(condaPath) - condasToRemove.removeAll(condaEnvsBeforeTest) - for (envName in condasToRemove) { - println("Removing $envName") - - for (arg in arrayOf("--name", "-p")) { - val args = arrayOf(condaPath.toString(), "remove", arg, envName, "--all", "-y") - Runtime.getRuntime().exec(args).getResultStdout().getOrElse { - logger().warn(it) - } - } - } - } - } - return Pair(PyCondaEnv(PyCondaEnvIdentity.UnnamedEnv(envDir.toString(), isBase = true), condaPath.toString()), cleanupCondas) - } - - private suspend fun getCondaNames(condaPath: Path) = - PyCondaEnv.getEnvs(TargetEnvironmentRequestCommandExecutor(LocalTargetEnvironmentRequest()), - condaPath.toString()).getOrThrow() - .map { it.envIdentity.userReadableName } - .toMutableSet() - } - - @RequiresBackgroundThread private fun typeMatchesEnv(env: Path, vararg additionalTags: @NonNls String): Boolean { val envTags = loadEnvTags(env) - - for (badTag in PythonType::class.sealedSubclasses.filterNot { it.isInstance(this) }.map { it.objectInstance!!.tag }) { - if (badTag != null && badTag in envTags) return false - } - return (tag == null || tag in envTags) && additionalTags.all { it in envTags } + return tag in envTags && additionalTags.all { it in envTags } } } \ No newline at end of file diff --git a/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/TypeConda.kt b/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/TypeConda.kt new file mode 100644 index 000000000000..2653b4130dd3 --- /dev/null +++ b/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/TypeConda.kt @@ -0,0 +1,52 @@ +// 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.community.testFramework.testEnv + +import com.intellij.execution.processTools.getResultStdout +import com.intellij.execution.target.local.LocalTargetEnvironmentRequest +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.projectRoots.Sdk +import com.jetbrains.python.PythonBinary +import com.jetbrains.python.packaging.findCondaExecutableRelativeToEnv +import com.jetbrains.python.sdk.conda.TargetEnvironmentRequestCommandExecutor +import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv +import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import java.nio.file.Path + +data object TypeConda : PythonType("conda") { + override suspend fun createSdkFor(env: PyCondaEnv): Sdk = + env.createSdkFromThisEnv(null, emptyList()) + + override suspend fun pythonPathToEnvironment(pythonBinary: PythonBinary, envDir: Path): Pair { + // First, find python binary, then calculate conda from it as env stores "conda" as a regular env + val condaPath = findCondaExecutableRelativeToEnv(pythonBinary) ?: error("Conda root $pythonBinary doesn't have conda binary") + + // We'll remove then on close + val condaEnvsBeforeTest = getCondaNames(condaPath) + + val cleanupCondas = AutoCloseable { + runBlocking(Dispatchers.IO) { + val condasToRemove = getCondaNames(condaPath) + condasToRemove.removeAll(condaEnvsBeforeTest) + for (envName in condasToRemove) { + println("Removing $envName") + + for (arg in arrayOf("--name", "-p")) { + val args = arrayOf(condaPath.toString(), "remove", arg, envName, "--all", "-y") + Runtime.getRuntime().exec(args).getResultStdout().getOrElse { + logger().warn(it) + } + } + } + } + } + return Pair(PyCondaEnv(PyCondaEnvIdentity.UnnamedEnv(envDir.toString(), isBase = true), condaPath.toString()), cleanupCondas) + } + + private suspend fun getCondaNames(condaPath: Path) = + PyCondaEnv.Companion.getEnvs(TargetEnvironmentRequestCommandExecutor(LocalTargetEnvironmentRequest()), + condaPath.toString()).getOrThrow() + .map { it.envIdentity.userReadableName } + .toMutableSet() +} \ No newline at end of file diff --git a/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/TypeVanillaPython3.kt b/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/TypeVanillaPython3.kt new file mode 100644 index 000000000000..10b592a9aa8e --- /dev/null +++ b/python/setup-test-environment/src/com/intellij/python/community/testFramework/testEnv/TypeVanillaPython3.kt @@ -0,0 +1,28 @@ +// 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.community.testFramework.testEnv + +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.projectRoots.SdkType +import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess +import com.intellij.testFramework.utils.vfs.refreshAndGetVirtualFile +import com.jetbrains.python.PyNames +import com.jetbrains.python.PythonBinary +import java.nio.file.Path + +data object TypeVanillaPython3 : PythonType("python3") { + override suspend fun createSdkFor(python: PythonBinary): Sdk = + SdkConfigurationUtil.setupSdk(emptyArray(), python.refreshAndGetVirtualFile(), + SdkType.findByName(PyNames.PYTHON_SDK_ID_NAME)!!, null, null) + + // Python is directly executable + override suspend fun pythonPathToEnvironment(pythonBinary: PythonBinary, envDir: Path): Pair { + val disposable = Disposer.newDisposable("Python tests disposable for VfsRootAccess") + // We might have python installation outside the project root, but we still need to have access to it. + VfsRootAccess.allowRootAccess(disposable, pythonBinary.parent.toString()) + return Pair(pythonBinary, AutoCloseable { + Disposer.dispose(disposable) + }) + } +} \ No newline at end of file diff --git a/python/testFramework/src/com/jetbrains/python/tools/sdk.kt b/python/testFramework/src/com/jetbrains/python/tools/sdk.kt index 792bad30926a..f004a43c8ac3 100644 --- a/python/testFramework/src/com/jetbrains/python/tools/sdk.kt +++ b/python/testFramework/src/com/jetbrains/python/tools/sdk.kt @@ -6,10 +6,9 @@ import com.intellij.execution.target.TargetEnvironmentConfiguration import com.intellij.openapi.application.writeAction import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.openapi.projectRoots.Sdk -import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil import com.intellij.python.community.testFramework.testEnv.PythonType +import com.intellij.python.community.testFramework.testEnv.TypeVanillaPython3 import com.intellij.remote.RemoteSdkException -import com.intellij.testFramework.utils.vfs.refreshAndGetVirtualFile import com.jetbrains.python.sdk.PythonSdkType import com.jetbrains.python.sdk.flavors.PyFlavorAndData import com.jetbrains.python.sdk.flavors.PyFlavorData @@ -24,7 +23,7 @@ import org.junit.Assume * To be used with [createSdk] */ sealed class SdkCreationRequest { - data class LocalPython(val pythonType: PythonType<*> = PythonType.VanillaPython3) : SdkCreationRequest() + data class LocalPython(val pythonType: PythonType<*> = TypeVanillaPython3) : SdkCreationRequest() data class RemotePython(val targetConfig: TargetEnvironmentConfiguration) : SdkCreationRequest() } @@ -34,17 +33,8 @@ sealed class SdkCreationRequest { suspend fun createSdk(request: SdkCreationRequest): Pair = withContext(Dispatchers.IO) { when (request) { is SdkCreationRequest.LocalPython -> { - when (val pythonType = request.pythonType) { - is PythonType.Conda -> { - val (env, closable) = pythonType.getTestEnvironment().getOrThrow() - Pair(env.createSdkFromThisEnv(null, emptyList()), closable) - } - is PythonType.VanillaPython3 -> { - val (python, closable) = pythonType.getTestEnvironment().getOrThrow() - Pair(SdkConfigurationUtil.setupSdk(emptyArray(), python.refreshAndGetVirtualFile(), - PythonSdkType.getInstance(), null, null), closable) - } - } + val (sdk, closable, _) = request.pythonType.createSdkClosableEnv().getOrThrow() + Pair(sdk, closable) } is SdkCreationRequest.RemotePython -> { val targetData = PyTargetAwareAdditionalData(PyFlavorAndData(PyFlavorData.Empty, UnixPythonSdkFlavor.getInstance()), diff --git a/python/testSrc/com/jetbrains/env/python/conda/LocalCondaRule.kt b/python/testSrc/com/jetbrains/env/python/conda/LocalCondaRule.kt index 14137f58da9a..9c2fb7100888 100644 --- a/python/testSrc/com/jetbrains/env/python/conda/LocalCondaRule.kt +++ b/python/testSrc/com/jetbrains/env/python/conda/LocalCondaRule.kt @@ -3,7 +3,7 @@ package com.jetbrains.env.python.conda import com.intellij.execution.target.FullPathOnTarget import com.intellij.execution.target.local.LocalTargetEnvironmentRequest -import com.intellij.python.community.testFramework.testEnv.PythonType +import com.intellij.python.community.testFramework.testEnv.TypeConda import com.jetbrains.python.sdk.conda.TargetCommandExecutor import com.jetbrains.python.sdk.conda.TargetEnvironmentRequestCommandExecutor import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand @@ -34,7 +34,7 @@ class LocalCondaRule : ExternalResource() { override fun before() { super.before() - val (condaPathEnv, autoCloseable) = runBlocking { PythonType.Conda.getTestEnvironment().getOrElse { throw AssumptionViolatedException("No conda found, run gradle script to install test env") } } + val (_, autoCloseable, condaPathEnv) = runBlocking { TypeConda.createSdkClosableEnv().getOrElse { throw AssumptionViolatedException("No conda found, run gradle script to install test env") } } condaPath = Path.of(condaPathEnv.fullCondaPathOnTarget) if (!condaPath.isExecutable()) {