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 1e982f9b5a11..64aa02d895a4 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 @@ -3,8 +3,10 @@ package com.intellij.python.junit5Tests.framework.env.impl import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.process.CapturingProcessHandler +import com.intellij.execution.process.ProcessNotCreatedException import com.intellij.ide.util.PropertiesComponent 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.intellij.python.junit5Tests.framework.resolvePythonTool @@ -31,7 +33,20 @@ internal class VanillaPythonEnvExtension : PythonEnvExtensionBase() 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()) { 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 2b5300100338..d7fa08decf54 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 @@ -11,21 +11,27 @@ import org.jetbrains.annotations.NonNls import java.nio.file.Path import java.util.concurrent.ConcurrentHashMap import kotlin.Result.Companion.failure +import kotlin.io.path.exists +import kotlin.io.path.isDirectory /** * 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 */ abstract class PythonType(private val tag: @NonNls String) { - private companion object { - val cache: MutableMap, List> = ConcurrentHashMap() + companion object { + private val cache: MutableMap, List> = ConcurrentHashMap() + private const val PYTHON_FOR_TESTS: String = "PYTHON_FOR_TESTS" + private val customPython: String? get() = System.getenv(PYTHON_FOR_TESTS) + val BUILD_KTS_MESSAGE: String = "`build.gradle.kts` from the same module ${PythonType::class} sits in. Be sure to read it first: you will need to run `gradle build` there" + val customPythonMessage: String? get() = customPython?.let { "You are using custom python $it set by $PYTHON_FOR_TESTS env var" } } /** * Returns all test environments ordered from newest (highest) to oldest: each must be closed after the test. * If in doubt, take first */ - suspend fun getTestEnvironments(vararg additionalTags: @NonNls String): Flow> { + suspend fun getTestEnvironments(vararg additionalTags: @NonNls String, ensureAtLeastOnePython: Boolean = false): Flow> { val key = setOf(*additionalTags) val pythons = cache.getOrPut(key) { PyEnvTestSettings @@ -42,9 +48,33 @@ abstract class PythonType(private val tag: @NonNls String) { } .sortedByDescending { (_, languageLevel) -> languageLevel } .map { (path, _) -> path } + }.toMutableList() + val customPythonDir = if (pythons.isEmpty() && ensureAtLeastOnePython) { + val pythonStr = customPython + if (pythonStr == null) { + error(""" + To run this test you need a python interpreter. You have two options: + + 1. Use $BUILD_KTS_MESSAGE. + 2. Set env variable $PYTHON_FOR_TESTS to the executable python file you already have, i.e: `/bin/python3` or `c:\python\python.exe` + """.trimIndent()) + } + val pythonPath = Path.of(pythonStr) + val pythonBinary = when { + pythonPath.isDirectory() -> VirtualEnvReader.Instance.findPythonInPythonRoot(pythonPath) + pythonPath.exists() -> pythonPath + else -> null + } ?: error("$PYTHON_FOR_TESTS env var points to something that is not a python: $pythonPath") + pythonBinary.parent + } + else { + null + } + if (customPythonDir != null) { + pythons.add(customPythonDir) } return pythons.asFlow() - .filter { typeMatchesEnv(it, *additionalTags) } + .filter { it == customPythonDir || typeMatchesEnv(it, *additionalTags) } .map { envDir -> pythonPathToEnvironment( VirtualEnvReader.Instance.findPythonInPythonRoot(envDir) @@ -57,7 +87,7 @@ abstract class PythonType(private val tag: @NonNls String) { * Returns sdk, (whatever it means) test environment and closable that must be closed after the test */ suspend fun createSdkClosableEnv(vararg additionalTags: @NonNls String): Result> = - getTestEnvironments(*additionalTags).firstOrNull()?.let { (env, closable) -> + getTestEnvironments(*additionalTags, ensureAtLeastOnePython = true).firstOrNull()?.let { (env, closable) -> Result.success(Triple(createSdkFor(env), closable, env)) } ?: failure(AssertionError("No python found. See ${PyEnvTestSettings::class} class for more info"))