PY-78951 Fix run a script with a configuration to a non exisiting .env file

GitOrigin-RevId: bdf1fd3e1e6c4af1e004e36897ed72937a3f01a0
This commit is contained in:
Anton Efimchuk
2025-11-04 19:53:50 +00:00
committed by intellij-monorepo-bot
parent 943655cbdc
commit 908477caab
2 changed files with 115 additions and 39 deletions
@@ -1,8 +1,6 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.run;
import com.intellij.codeWithMe.ClientId;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.ExecutionResult;
import com.intellij.execution.configurations.RunProfile;
import com.intellij.execution.configurations.RunProfileState;
@@ -12,16 +10,18 @@ import com.intellij.execution.runners.AsyncProgramRunner;
import com.intellij.execution.runners.DefaultProgramRunnerKt;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.ui.RunContentDescriptor;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.AppUIExecutor;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.util.concurrency.AppExecutorUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.concurrency.AsyncPromise;
import org.jetbrains.concurrency.CancellablePromise;
import org.jetbrains.concurrency.Promise;
import org.jetbrains.concurrency.Promises;
import java.util.concurrent.Callable;
public class PythonRunner extends AsyncProgramRunner<RunnerSettings> {
@Override
public @NotNull String getRunnerId() {
@@ -37,12 +37,12 @@ public class PythonRunner extends AsyncProgramRunner<RunnerSettings> {
* {@link PythonCommandLineState} inheritors must be ready to be called on any thread, so we can run then on the background thread.
* Any other state must be invoked on EDT only
*/
private static void execute(@NotNull RunProfileState profileState, @NotNull Runnable runnable) {
var clientIdRunnable = ClientId.decorateRunnable(runnable);
private static <R> CancellablePromise<R> execute(@NotNull RunProfileState profileState, @NotNull Callable<R> supplier) {
if (profileState instanceof PythonCommandLineState) {
AppExecutorUtil.getAppExecutorService().execute(clientIdRunnable);
} else {
ApplicationManager.getApplication().invokeAndWait(clientIdRunnable);
return executeInBgt(supplier);
}
else {
return executeInEdt(supplier);
}
}
@@ -58,24 +58,34 @@ public class PythonRunner extends AsyncProgramRunner<RunnerSettings> {
FileDocumentManager.getInstance().saveAllDocuments();
AsyncPromise<RunContentDescriptor> promise = new AsyncPromise<>();
execute(state, () -> {
try {
ExecutionResult executionResult;
if (state instanceof PythonCommandLineState) {
// TODO [cloud-api.python] profile functionality must be applied here:
// - com.jetbrains.django.run.DjangoServerRunConfiguration.patchCommandLineFirst() - host:port is put in user data
executionResult = ((PythonCommandLineState)state).execute(env.getExecutor());
}
else {
executionResult = state.execute(env.getExecutor(), this);
}
ApplicationManager.getApplication().invokeLater(
() -> promise.setResult(DefaultProgramRunnerKt.showRunContent(executionResult, env)),
ModalityState.any());
return execute(state, () -> {
ExecutionResult executionResult;
if (state instanceof PythonCommandLineState) {
// TODO [cloud-api.python] profile functionality must be applied here:
// - com.jetbrains.django.run.DjangoServerRunConfiguration.patchCommandLineFirst() - host:port is put in user data
executionResult = ((PythonCommandLineState)state).execute(env.getExecutor());
}
catch (ExecutionException e) {
else {
executionResult = state.execute(env.getExecutor(), this);
}
return executionResult;
}).thenAsync((ExecutionResult executionResult) -> {
return executeInEdt(() -> DefaultProgramRunnerKt.showRunContent(executionResult, env));
});
}
private static <V> CancellablePromise<V> executeInEdt(Callable<V> callable) {
return AppUIExecutor.onUiThread().submit(callable);
}
private static <V> CancellablePromise<V> executeInBgt(Callable<V> callable) {
AsyncPromise<V> promise = new AsyncPromise<>();
AppExecutorUtil.getAppExecutorService().execute(() -> {
try {
V result = callable.call();
promise.setResult(result);
}
catch (Exception e) {
promise.setError(e);
}
});
+79 -13
View File
@@ -15,12 +15,13 @@ import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.Ref
import com.intellij.testFramework.UsefulTestCase.assertSameElements
import com.jetbrains.env.PyEnvTestCase
import com.jetbrains.env.PyExecutionFixtureTestTask
import com.jetbrains.env.PyTestTask
import com.jetbrains.python.run.PythonConfigurationType
import com.jetbrains.python.run.PythonRunConfiguration
import com.jetbrains.python.run.PythonRunner
import org.assertj.core.api.Assertions.assertThat
import org.junit.Assert
import org.junit.Test
import java.util.concurrent.CountDownLatch
@@ -29,10 +30,59 @@ import java.util.concurrent.TimeUnit
class PyRunTest : PyEnvTestCase() {
@Test
fun testInputRedirection() {
test {
val result = runConfiguration {
redirectInput = true
inputFile = "input.txt"
}
assertThat(result.output).hasSameElementsAs(listOf("100500\n")).describedAs("Output should match expected values")
assertThat(result.state).isEqualTo(ProcessState.Finished(0))
}
}
@Test
fun testRunSucceedsWhenEnvFileAbsent() {
test {
val result = runConfiguration {
envFile = absent()
}
assertThat(result.state).isEqualTo(ProcessState.NotStarted)
}
}
// DSL entry-point bound to the test class to access protected helpers
private fun test(block: TestDslBuilder.() -> Unit) {
val builder = TestDslBuilder { runPythonTestWithException(it) }
builder.block()
}
}
// --- Minimal DSL for tests in this file ---
private class RunConfigurationDsl {
var envFile: EnvFileSpec = EnvFileSpec.None
var redirectInput: Boolean? = null
var inputFile: String? = null
}
private sealed interface EnvFileSpec {
data object None : EnvFileSpec
data object Absent : EnvFileSpec
}
private fun absent(): EnvFileSpec = EnvFileSpec.Absent
private class TestDslBuilder(private val executor: (PyTestTask) -> Unit) {
fun runConfiguration(block: RunConfigurationDsl.() -> Unit): RunResult {
val runCfg = RunConfigurationDsl().apply { block() }
val output = mutableListOf<String>()
var processState: ProcessState = ProcessState.NotStarted
// Execute the same scenario as input redirection test, but honoring the DSL
val task = object : PyExecutionFixtureTestTask("/runConfig/inputRedirection") {
override fun runTestOn(sdkHome: String, existingSdk: Sdk?) {
val project = project
val settings = RunManager.getInstance(project).createConfiguration("test", PythonConfigurationType::class.java)
val settings = RunManager.getInstance(project).createConfiguration("test-missing-env", PythonConfigurationType::class.java)
val runConfiguration = settings.configuration as PythonRunConfiguration
runConfiguration.apply {
@@ -40,33 +90,42 @@ class PyRunTest : PyEnvTestCase() {
this.sdk = existingSdk
scriptName = "inputRedirection.py"
workingDirectory = myFixture.tempDirPath
isRedirectInput = true
inputFile = "${myFixture.tempDirPath}/input.txt"
// Input redirection: use DSL value if provided, default to true and input.txt (to keep older tests working)
isRedirectInput = runCfg.redirectInput ?: false
val input = runCfg.inputFile ?: "input.txt"
inputFile = "${myFixture.tempDirPath}/$input"
when (runCfg.envFile) {
is EnvFileSpec.Absent -> {
// point to a non-existent .env file; run must still succeed
envFilePaths = listOf("${myFixture.tempDirPath}/.env")
}
else -> Unit
}
}
val executor = DefaultRunExecutor.getRunExecutorInstance()
val runner = ProgramRunner.getRunner(executor.id, settings.configuration) as PythonRunner
val env = ExecutionEnvironment(executor, runner, settings, project)
var processNotStarted = false
val connection = project.messageBus.connect()
val latch = CountDownLatch(1)
val output = mutableListOf<String>()
val handlerRef = Ref<ProcessHandler>()
connection.subscribe(ExecutionManager.EXECUTION_TOPIC, object : ExecutionListener {
override fun processStarted(executorId: String, env: ExecutionEnvironment, handler: ProcessHandler) {
super.processStarted(executorId, env, handler)
processState = ProcessState.Started
handlerRef.set(handler)
}
override fun processTerminated(executorId: String, env: ExecutionEnvironment, handler: ProcessHandler, exitCode: Int) {
super.processTerminated(executorId, env, handler, exitCode)
processState = ProcessState.Finished(exitCode)
latch.countDown()
}
override fun processNotStarted(executorId: String, e: ExecutionEnvironment) {
if (executorId == executor.id && e == env) {
processNotStarted = true
latch.countDown()
}
}
@@ -82,6 +141,7 @@ class PyRunTest : PyEnvTestCase() {
}
override fun processTerminated(event: ProcessEvent) {
processState = ProcessState.Finished(event.exitCode)
processHandler.removeProcessListener(this)
latch.countDown()
}
@@ -94,13 +154,19 @@ class PyRunTest : PyEnvTestCase() {
handlerRef.get()?.destroyProcess()
Assert.assertTrue("Test frozen", await)
connection.disconnect()
if (processNotStarted) {
Assert.fail("Failed to start the script")
}
assertSameElements("Incorrect output", output, listOf("100500\n"))
}
}
runPythonTestWithException(task)
executor(task)
return RunResult(output, processState)
}
}
}
private data class RunResult(val output: List<String>, val state: ProcessState)
// Represent the process state for the executed run configuration
private sealed interface ProcessState {
data object NotStarted : ProcessState
data object Started : ProcessState
data class Finished(val exitCode: Int) : ProcessState
}