All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jetbrains.kotlinx.jupyter.test.protocol.ExecuteTests.kt Maven / Gradle / Ivy

package org.jetbrains.kotlinx.jupyter.test.protocol

import ch.qos.logback.classic.Level.DEBUG
import ch.qos.logback.classic.Level.OFF
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldNotBeEmpty
import io.kotest.matchers.comparables.shouldBeGreaterThan
import io.kotest.matchers.paths.shouldBeAFile
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldNotContain
import io.kotest.matchers.types.shouldBeTypeOf
import jupyter.kotlin.providers.UserHandlesProvider
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.jsonPrimitive
import org.jetbrains.kotlinx.jupyter.LoggingManager
import org.jetbrains.kotlinx.jupyter.api.MimeTypes
import org.jetbrains.kotlinx.jupyter.api.Notebook
import org.jetbrains.kotlinx.jupyter.api.SessionOptions
import org.jetbrains.kotlinx.jupyter.config.currentKotlinVersion
import org.jetbrains.kotlinx.jupyter.messaging.CommMsg
import org.jetbrains.kotlinx.jupyter.messaging.CommOpen
import org.jetbrains.kotlinx.jupyter.messaging.DisplayDataResponse
import org.jetbrains.kotlinx.jupyter.messaging.EXECUTION_INTERRUPTED_MESSAGE
import org.jetbrains.kotlinx.jupyter.messaging.ExecuteErrorReply
import org.jetbrains.kotlinx.jupyter.messaging.ExecuteRequest
import org.jetbrains.kotlinx.jupyter.messaging.ExecuteSuccessReply
import org.jetbrains.kotlinx.jupyter.messaging.ExecutionCount
import org.jetbrains.kotlinx.jupyter.messaging.ExecutionResultMessage
import org.jetbrains.kotlinx.jupyter.messaging.InputReply
import org.jetbrains.kotlinx.jupyter.messaging.InputRequest
import org.jetbrains.kotlinx.jupyter.messaging.InterruptRequest
import org.jetbrains.kotlinx.jupyter.messaging.IsCompleteReply
import org.jetbrains.kotlinx.jupyter.messaging.IsCompleteRequest
import org.jetbrains.kotlinx.jupyter.messaging.KernelInfoReply
import org.jetbrains.kotlinx.jupyter.messaging.KernelInfoReplyMetadata
import org.jetbrains.kotlinx.jupyter.messaging.KernelInfoRequest
import org.jetbrains.kotlinx.jupyter.messaging.KernelStatus
import org.jetbrains.kotlinx.jupyter.messaging.Message
import org.jetbrains.kotlinx.jupyter.messaging.MessageStatus
import org.jetbrains.kotlinx.jupyter.messaging.MessageType
import org.jetbrains.kotlinx.jupyter.messaging.OpenDebugPortReply
import org.jetbrains.kotlinx.jupyter.messaging.ProvidedCommMessages
import org.jetbrains.kotlinx.jupyter.messaging.StatusReply
import org.jetbrains.kotlinx.jupyter.messaging.StreamResponse
import org.jetbrains.kotlinx.jupyter.protocol.JupyterSocket
import org.jetbrains.kotlinx.jupyter.protocol.JupyterSocketBase
import org.jetbrains.kotlinx.jupyter.protocol.JupyterSocketInfo
import org.jetbrains.kotlinx.jupyter.protocol.MessageFormat
import org.jetbrains.kotlinx.jupyter.repl.EvaluatedSnippetMetadata
import org.jetbrains.kotlinx.jupyter.test.NotebookMock
import org.jetbrains.kotlinx.jupyter.test.assertStartsWith
import org.jetbrains.kotlinx.jupyter.test.testLoggerFactory
import org.jetbrains.kotlinx.jupyter.util.jsonObject
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import org.junit.jupiter.api.condition.EnabledForJreRange
import org.junit.jupiter.api.condition.JRE
import org.junit.jupiter.api.parallel.Execution
import org.junit.jupiter.api.parallel.ExecutionMode
import org.zeromq.ZMQ
import java.io.File
import java.net.URLClassLoader
import java.nio.file.Files
import java.util.concurrent.TimeUnit
import kotlin.io.path.readText
import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

fun JsonObject.string(key: String): String {
    return (get(key) as JsonPrimitive).content
}

@Timeout(100, unit = TimeUnit.SECONDS)
@Execution(ExecutionMode.SAME_THREAD)
class ExecuteTests : KernelServerTestsBase(runServerInSeparateProcess = true) {
    private var _context: ZMQ.Context? = null
    override val context: ZMQ.Context
        get() = _context!!

    private var shell: JupyterSocket? = null
    private var control: JupyterSocket? = null
    private var ioPub: JupyterSocket? = null
    private var stdin: JupyterSocket? = null

    private val shellSocket: JupyterSocketBase get() = shell!!
    private val controlSocket: JupyterSocketBase get() = control!!
    private val ioPubSocket: JupyterSocketBase get() = ioPub!!
    private val stdinSocket: JupyterSocketBase get() = stdin!!

    override fun beforeEach() {
        try {
            _context = ZMQ.context(1)
            shell =
                createClientSocket(JupyterSocketInfo.SHELL).apply {
                    makeRelaxed()
                }
            ioPub = createClientSocket(JupyterSocketInfo.IOPUB)
            stdin = createClientSocket(JupyterSocketInfo.STDIN)
            control = createClientSocket(JupyterSocketInfo.CONTROL)

            ioPub?.subscribe(byteArrayOf())
            shell?.connect()
            ioPub?.connect()
            stdin?.connect()
            control?.connect()
        } catch (e: Throwable) {
            afterEach()
            throw e
        }
    }

    override fun afterEach() {
        listOf(::shell, ::ioPub, ::stdin, ::control).forEach { socketProp ->
            socketProp.get()?.close()
            socketProp.set(null)
        }
        context.term()
        _context = null
    }

    private fun doExecute(
        code: String,
        hasResult: Boolean = true,
        ioPubChecker: (JupyterSocketBase) -> Unit = {},
        executeRequestSent: () -> Unit = {},
        executeReplyChecker: (Message) -> Unit = {},
        inputs: List = emptyList(),
        allowStdin: Boolean = true,
        storeHistory: Boolean = true,
    ): Any? {
        try {
            shellSocket.sendMessage(
                MessageType.EXECUTE_REQUEST,
                content = ExecuteRequest(code, allowStdin = allowStdin, storeHistory = storeHistory),
            )
            executeRequestSent()
            inputs.forEach {
                val request = stdinSocket.receiveMessage()
                request.content.shouldBeTypeOf()
                stdinSocket.sendMessage(MessageType.INPUT_REPLY, InputReply(it))
            }

            var msg = shellSocket.receiveMessage()
            assertEquals(MessageType.EXECUTE_REPLY, msg.type)
            executeReplyChecker(msg)

            msg = ioPubSocket.receiveMessage()
            assertEquals(MessageType.STATUS, msg.type)
            assertEquals(KernelStatus.BUSY, (msg.content as StatusReply).status)
            msg = ioPubSocket.receiveMessage()
            assertEquals(MessageType.EXECUTE_INPUT, msg.type)

            ioPubChecker(ioPubSocket)

            var response: Any? = null
            if (hasResult) {
                msg = ioPubSocket.receiveMessage()
                val content = msg.content as ExecutionResultMessage
                assertEquals(MessageType.EXECUTE_RESULT, msg.type)
                response = content.data
            }

            msg = ioPubSocket.receiveMessage()
            assertEquals(MessageType.STATUS, msg.type)
            assertEquals(KernelStatus.IDLE, (msg.content as StatusReply).status)
            return response
        } catch (e: Throwable) {
            afterEach()
            throw e
        }
    }

    private fun executeWithNoStdin(code: String) {
        doExecute(
            code,
            hasResult = false,
            allowStdin = false,
            ioPubChecker = {
                val msg = it.receiveMessage()
                assertEquals(MessageType.ERROR, msg.type)
                assertStartsWith("Input from stdin is unsupported by the client", (msg.content as ExecuteErrorReply).value)
            },
        )
    }

    private fun doIsComplete(code: String): String {
        try {
            shellSocket.sendMessage(MessageType.IS_COMPLETE_REQUEST, content = IsCompleteRequest(code))

            val responseMsg = shellSocket.receiveMessage()
            assertEquals(MessageType.IS_COMPLETE_REPLY, responseMsg.type)

            val content = responseMsg.content as IsCompleteReply
            return content.status
        } catch (e: Throwable) {
            afterEach()
            throw e
        }
    }

    private fun interruptExecution() {
        controlSocket.sendMessage(MessageType.INTERRUPT_REQUEST, InterruptRequest())
    }

    private fun requestKernelInfo(): Message {
        shellSocket.sendMessage(MessageType.KERNEL_INFO_REQUEST, KernelInfoRequest())
        val responseMsg = shellSocket.receiveMessage()
        responseMsg.type shouldBe MessageType.KERNEL_INFO_REPLY
        val content = responseMsg.content
        content.shouldBeTypeOf()
        return responseMsg
    }

    private inline fun  JupyterSocketBase.receiveMessageOfType(messageType: MessageType): T {
        val msg = receiveMessage()
        assertEquals(messageType, msg.type)
        val content = msg.content
        content.shouldBeTypeOf()
        return content
    }

    private fun JupyterSocketBase.receiveStreamResponse(): String {
        return receiveMessageOfType(MessageType.STREAM).text
    }

    private fun JupyterSocketBase.receiveErrorResponse(): String {
        return receiveMessageOfType(MessageType.ERROR).value
    }

    private fun JupyterSocketBase.receiveDisplayDataResponse(): DisplayDataResponse {
        return receiveMessageOfType(MessageType.DISPLAY_DATA)
    }

    private fun JupyterSocketBase.receiveUpdateDisplayDataResponse(): DisplayDataResponse {
        return receiveMessageOfType(MessageType.UPDATE_DISPLAY_DATA)
    }

    @Test
    fun testExecute() {
        val res = doExecute("2+2") as JsonObject
        assertEquals("4", res.string(MimeTypes.PLAIN_TEXT))
    }

    @Test
    fun testOutput() {
        val code =
            """
            for (i in 1..5) {
                Thread.sleep(200)
                print(i)
            }
            """.trimIndent()

        fun checker(ioPub: JupyterSocketBase) {
            for (i in 1..5) {
                val msg = ioPub.receiveMessage()
                assertEquals(MessageType.STREAM, msg.type)
                assertEquals(i.toString(), (msg.content as StreamResponse).text)
            }
        }

        val res = doExecute(code, false, ::checker)
        assertNull(res)
    }

    @Test
    fun testOutputMagic() {
        val code =
            """
            %output --max-buffer=2 --max-time=10000
            for (i in 1..5) {
                print(i)
            }
            """.trimIndent()

        val expected = arrayOf("12", "34", "5")

        fun checker(ioPub: JupyterSocketBase) {
            for (el in expected) {
                val msgText = ioPub.receiveStreamResponse()
                assertEquals(el, msgText)
            }
        }

        val res = doExecute(code, false, ::checker)
        assertNull(res)
    }

    @Test
    fun testOutputStrings() {
        val repetitions = 5
        val code =
            """
            for (i in 1..$repetitions) {
                Thread.sleep(200)
                println("text" + i)
            }
            """.trimIndent()

        fun checker(ioPub: JupyterSocketBase) {
            val lineSeparator = System.lineSeparator()
            val actualText = (1..repetitions).joinToString("") { ioPub.receiveStreamResponse() }
            val expectedText = (1..repetitions).joinToString("") { i -> "text$i$lineSeparator" }
            actualText shouldBe expectedText
        }

        val res = doExecute(code, false, ::checker)
        assertNull(res)
    }

    @Test
    fun testReadLine() {
        val code =
            """
            val answer1 = readLine()!!.toInt()
            val answer2 = notebook.prompt("Your answer:").toInt()
            answer1 + answer2
            """.trimIndent()
        val res = doExecute(code, inputs = listOf("42", "43"))
        res shouldBe jsonObject(MimeTypes.PLAIN_TEXT to "85")
    }

    @Test
    fun testCompiledData() {
        doExecute(
            """
            SessionOptions.serializeScriptData = true
            """.trimIndent(),
            hasResult = false,
        )

        val code =
            """
            val xyz = 42
            """.trimIndent()
        val res =
            doExecute(
                code,
                hasResult = false,
                executeReplyChecker = { message ->
                    val metadata = message.data.metadata
                    assertTrue(metadata is JsonObject)
                    val snippetMetadata =
                        MessageFormat.decodeFromJsonElement(
                            metadata["eval_metadata"] ?: JsonNull,
                        )
                    val compiledData = snippetMetadata?.compiledData
                    assertNotNull(compiledData)

                    val deserializer = org.jetbrains.kotlinx.jupyter.compiler.CompiledScriptsSerializer()
                    val dir = Files.createTempDirectory("kotlin-jupyter-exec-test")
                    dir.toFile().deleteOnExit()
                    val classesDir = dir.resolve("classes")
                    val sourcesDir = dir.resolve("sources")

                    val names = deserializer.deserializeAndSave(compiledData, classesDir, sourcesDir)
                    val kClassName = names.single()
                    val classLoader = URLClassLoader(arrayOf(classesDir.toUri().toURL()), ClassLoader.getSystemClassLoader())
                    val loadedClass = classLoader.loadClass(kClassName).kotlin

                    @Suppress("UNCHECKED_CAST")
                    val xyzProperty = loadedClass.memberProperties.single { it.name == "xyz" } as KProperty1
                    val constructor = loadedClass.constructors.single()

                    val userHandlesProvider =
                        object : UserHandlesProvider {
                            override val notebook: Notebook = NotebookMock
                            override val sessionOptions: SessionOptions
                                get() = throw NotImplementedError()
                        }

                    val instance = constructor.call(emptyArray(), userHandlesProvider)
                    xyzProperty.get(instance) shouldBe 42

                    val sourceFile = sourcesDir.resolve("Line_1.kts")
                    sourceFile.shouldBeAFile()
                    sourceFile.readText() shouldBe "val xyz = 42"
                },
            )
        assertNull(res)
    }

    @Test
    fun testLibraryLoadingErrors() {
        doExecute(
            """
            USE {
                import("xyz.ods")
            }
            """.trimIndent(),
            false,
            ioPubChecker = {
                val msgText = it.receiveErrorResponse()
                assertTrue("The problem is found in one of the loaded libraries" in msgText)
            },
        )
    }

    @Test
    fun testCounter() {
        fun checkCounter(
            message: Message,
            expectedCounter: Int,
        ) {
            val data = message.data.content as ExecuteSuccessReply
            assertEquals(ExecutionCount(expectedCounter), data.executionCount)
        }
        val res1 = doExecute("41", executeReplyChecker = { checkCounter(it, 1) })
        val res2 = doExecute("42", executeReplyChecker = { checkCounter(it, 2) })
        val res3 =
            doExecute(
                " \"\${Out[1]} \${Out[2]}\" ",
                storeHistory = false,
                executeReplyChecker = { checkCounter(it, 3) },
            )
        val res4 =
            doExecute(
                "val x = try { Out[3] } catch(e: ArrayIndexOutOfBoundsException) { null }; x",
                storeHistory = false,
                executeReplyChecker = { checkCounter(it, 3) },
            )

        assertEquals(jsonObject(MimeTypes.PLAIN_TEXT to "41"), res1)
        assertEquals(jsonObject(MimeTypes.PLAIN_TEXT to "42"), res2)
        assertEquals(jsonObject(MimeTypes.PLAIN_TEXT to "41 42"), res3)
        assertEquals(jsonObject(MimeTypes.PLAIN_TEXT to "null"), res4)
    }

    @Test
    fun testReadLineWithNoStdin() {
        executeWithNoStdin("readLine() ?: \"blah\"")
    }

    @Test
    fun testStdinReadWithNoStdin() {
        executeWithNoStdin("System.`in`.read()")
    }

    @Test
    fun testIsComplete() {
        assertEquals("complete", doIsComplete("2 + 2"))
        assertEquals("incomplete", doIsComplete("fun f() : Int { return 1"))
        val loggingManager = LoggingManager(testLoggerFactory)
        assertEquals(if (runServerInSeparateProcess) DEBUG else OFF, loggingManager.mainLoggerLevel())
    }

    @Test
    fun testLoggerAppender() {
        val file = File.createTempFile("kotlin-jupyter-logger-appender-test", ".txt")
        doExecute("%logHandler add f1 --file ${file.absolutePath}", false)
        val result1 = doExecute("2 + 2")
        assertEquals(jsonObject(MimeTypes.PLAIN_TEXT to "4"), result1)

        doExecute("%logHandler remove f1", false)
        val result2 = doExecute("3 + 4")
        assertEquals(jsonObject(MimeTypes.PLAIN_TEXT to "7"), result2)

        val logText = file.readText()
        logText.shouldContain("2 + 2")
        logText.shouldNotContain("3 + 4")

        file.delete()
    }

    @Test
    fun testComms() {
        val targetName = "my_comm"
        val commId = "xyz"

        val registerCode =
            """
            import kotlinx.serialization.*
            import kotlinx.serialization.json.*
            
            notebook.commManager.registerCommTarget("$targetName") { comm, openData ->
                comm.send(
                    JsonObject(
                        mapOf(
                            "xo" to JsonPrimitive(comm.id)
                        )
                    )
                )
                
                comm.onMessage { d ->
                    comm.send(
                        JsonObject(
                            mapOf(
                                "y" to JsonPrimitive("received: " + d["x"]!!.jsonPrimitive.content)
                            )
                        )
                    )
                }
            }
            """.trimIndent()
        doExecute(registerCode, false)

        shellSocket.sendMessage(MessageType.COMM_OPEN, CommOpen(commId, targetName))

        ioPubSocket.receiveMessage().apply {
            val c = content.shouldBeTypeOf()
            c.commId shouldBe commId
            c.data["xo"]!!.jsonPrimitive.content shouldBe commId
        }

        // Thread.sleep(5000)

        shellSocket.sendMessage(
            MessageType.COMM_MSG,
            CommMsg(
                commId,
                JsonObject(
                    mapOf(
                        "x" to JsonPrimitive("4321"),
                    ),
                ),
            ),
        )

        ioPubSocket.wrapActionInBusyIdleStatusChange {
            ioPubSocket.receiveMessage().apply {
                val c = content.shouldBeTypeOf()
                c.commId shouldBe commId
                c.data["y"]!!.jsonPrimitive.content shouldBe "received: 4321"
            }
        }
    }

    @Test
    fun testDebugPortCommHandler() {
        val targetName = ProvidedCommMessages.OPEN_DEBUG_PORT_TARGET
        val commId = "some"
        val actualDebugPort = kernelConfig.debugPort

        shellSocket.sendMessage(
            MessageType.COMM_OPEN,
            CommOpen(
                commId,
                targetName,
            ),
        )

        shellSocket.sendMessage(
            MessageType.COMM_MSG,
            CommMsg(commId),
        )

        ioPubSocket.wrapActionInBusyIdleStatusChange {
            ioPubSocket.receiveMessage().apply {
                val c = content.shouldBeTypeOf()
                val data =
                    MessageFormat.decodeFromJsonElement(c.data).shouldBeTypeOf()
                c.commId shouldBe commId
                data.port shouldBe actualDebugPort
                data.status shouldBe MessageStatus.OK
            }
        }
    }

    @Test
    fun testCommand() {
        val res = doExecute(":help")
        res.shouldBeTypeOf()
        val text = res[MimeTypes.PLAIN_TEXT]!!.jsonPrimitive.content
        text.shouldContain(currentKotlinVersion)
        print(text)
    }

    @Test
    @EnabledForJreRange(max = JRE.JAVA_19, disabledReason = "Thread.stop() is not supported on JDK 20+")
    fun testInterrupt() {
        doExecute(
            "while(true);",
            hasResult = false,
            executeRequestSent = {
                Thread.sleep(15_000)
                interruptExecution()
            },
            ioPubChecker = { iopubSocket ->
                val msgText = iopubSocket.receiveErrorResponse()
                msgText shouldBe EXECUTION_INTERRUPTED_MESSAGE
            },
        ) shouldBe null

        doExecute("1") shouldBe jsonObject(MimeTypes.PLAIN_TEXT to "1")
        doExecute("2") shouldBe jsonObject(MimeTypes.PLAIN_TEXT to "2")
    }

    @Test
    @Disabled
    fun testBigDataFrame() {
        doExecute(
            """
            %use dataframe
            DataFrame.read("https://api.apis.guru/v2/list.json")
            """.trimIndent(),
            ioPubChecker = { iopubSocket ->
                iopubSocket.receiveDisplayDataResponse()
                iopubSocket.receiveDisplayDataResponse()
            },
        )
    }

    // Verify we do not crash when sending UPDATE_DISPLAY messages
    @Test
    fun testUpdateDisplay() {
        doExecute(
            """
            UPDATE_DISPLAY("b", "id1")
            "hello"
            """.trimIndent(),
            ioPubChecker = { iopubSocket ->
                // In case of an error, this would return STREAM
                iopubSocket.receiveUpdateDisplayDataResponse()
            },
        )
    }

    @Test
    fun testExecuteExceptionInSameCell() {
        val code =
            """
            val foo = "bar"
            TODO()
            """.trimIndent()
        doExecute(
            code,
            hasResult = false,
            ioPubChecker = { ioPubSocket ->
                // If execution throws an exception, we should send an ERROR
                // message type with the appropriate metadata.
                val message = ioPubSocket.receiveMessage()
                val content = message.content
                content.shouldBeTypeOf()
                content.name shouldBe "kotlin.NotImplementedError"
                content.value shouldBe "An operation is not implemented."

                // Stacktrace should be enhanced with cell information
                content.traceback shouldContain "\tat Line_0_jupyter.(Line_0.jupyter.kts:2) at Cell In[1], line 2"
                content.traceback[content.traceback.size - 2] shouldBe "at Cell In[1], line 2"
            },
        )
    }

    @Test
    fun testExecuteExceptionInOtherCell() {
        doExecute(
            """
            val callback: (Int) -> Int = { i: Int ->
                if (i == 42) TODO()
                else i
            }
            """.trimIndent(),
            hasResult = false,
        )
        val code =
            """
            callback(42)
            """.trimIndent()
        doExecute(
            code,
            hasResult = false,
            ioPubChecker = { ioPubSocket: JupyterSocketBase ->
                // If execution throws an exception, we should send an ERROR
                // message type with the appropriate metadata.
                val message = ioPubSocket.receiveMessage()
                message.type shouldBe MessageType.ERROR
                val content = message.content
                content.shouldBeTypeOf()
                content.name shouldBe "kotlin.NotImplementedError"
                content.value shouldBe "An operation is not implemented."

                // Stacktrace should be enhanced with cell information
                content.traceback shouldContain "\tat Line_0_jupyter\$callback\$1.invoke(Line_0.jupyter.kts:2) at Cell In[1], line 2"
                content.traceback shouldContain "\tat Line_0_jupyter\$callback\$1.invoke(Line_0.jupyter.kts:1) at Cell In[1], line 1"
                content.traceback shouldContain "\tat Line_1_jupyter.(Line_1.jupyter.kts:1) at Cell In[2], line 1"
                content.traceback[content.traceback.size - 2] shouldBe "at Cell In[1], line 2"
            },
        )
    }

    // In case of an exception happening in generated code. This code isn't visible to
    // the user. In that case, these things happen:
    // - Any stack trace line that references outside the visible code range only shows the request count.
    // - We find the first "visible" error and promote that at the end of error output instead of the
    //   first error. This means that the user can hopefully find the point in their code that triggers
    //   the behavior
    // - If no visible user code can be found, only the request count is displayed but no line number.
    @Test
    fun testExceptionInGeneratedCodeShouldNotReferenceLine() {
        val code =
            """
            %use ktor-client

            @Serializable
            class User(val id: Int)
            
            // Body cannot be serialized, and will throw exception in generated code
            http.get("https://github.com/Kotlin/kotlin-jupyter").body()
            """.trimIndent()

        doExecute(
            code,
            hasResult = false,
            ioPubChecker = { ioPubSocket ->
                val message = ioPubSocket.receiveMessage()
                message.type shouldBe MessageType.ERROR
                val content = message.content
                content.shouldBeTypeOf()
                content.name shouldBe "io.ktor.serialization.JsonConvertException"
                content.value shouldBe
                    "Illegal input: Field 'id' is required for type with serial name 'Line_6_jupyter.User', but it was missing at path: \$"

                // Stacktrace should only contain the cell reference if error is outside visible range
                content.traceback shouldContain "\tat Line_6_jupyter.(Line_6.jupyter.kts:12) at Cell In[1]"
                content.traceback shouldContain "\tat Line_6_jupyter\$User.(Line_6.jupyter.kts:3) at Cell In[1], line 3"
                content.traceback[content.traceback.size - 2] shouldBe "at Cell In[1]"
            },
        )
    }

    @Test
    fun testCompileErrorsAreEnhanced() {
        val illegalCode =
            """
            val var
            """.trimIndent()
        doExecute(
            illegalCode,
            hasResult = false,
            ioPubChecker = { ioPubSocket ->
                // If execution throws an exception, we should send an ERROR
                // message type with the appropriate metadata.
                val message = ioPubSocket.receiveMessage()
                message.type shouldBe MessageType.ERROR
                val content = message.content
                content.shouldBeTypeOf()
                content.name shouldBe "org.jetbrains.kotlinx.jupyter.exceptions.ReplCompilerException"
                content.value shouldBe
                    """
                    at Cell In[1], line 1, column 4: Expecting property name or receiver type
                    at Cell In[1], line 1, column 5: Property getter or setter expected
                    """.trimIndent()
                // Error should also contain the stack trace
                content.traceback.size shouldBeGreaterThan 0
            },
        )
    }

    @Test
    fun testLibraryExceptionShouldContainFullStackTrace() {
        val code =
            """
            USE {
                dependencies {
                    implementation("some:nonexistent:lib")
                }
            }
            """.trimIndent()
        doExecute(
            code,
            hasResult = false,
            ioPubChecker = { ioPubSocket ->
                // If execution throws an exception, we should send an ERROR
                // message type with the appropriate metadata.
                val message = ioPubSocket.receiveMessage()
                message.type shouldBe MessageType.ERROR
                val content = message.content
                content.shouldBeTypeOf()
                content.name shouldBe "org.jetbrains.kotlinx.jupyter.exceptions.ReplLibraryException"
                content.value shouldBe
                    "The problem is found in one of the loaded libraries: check library imports, dependencies and repositories"
                // Error should also contain the stack trace
                content.traceback.size shouldBeGreaterThan 0
            },
        )
    }

    @Test
    fun `kernel_info_reply should contain info about session state`() {
        doExecute(
            """
            SessionOptions.serializeScriptData = true
            """.trimIndent(),
            hasResult = false,
        )

        doExecute(
            """
            %use ktor-client
            """.trimIndent(),
            hasResult = false,
        )

        doExecute(
            """
            import java.util.concurrent.ConcurrentHashMap
            
            buildJsonObject {
                put("a", JsonPrimitive(1))
                put("b", JsonPrimitive(2))
            }
            """.trimIndent(),
        )

        val infoResponse = requestKernelInfo()
        val metadataObject = infoResponse.data.metadata
        metadataObject.shouldBeTypeOf()
        val metadata = MessageFormat.decodeFromJsonElement(metadataObject)

        with(metadata.state) {
            assertTrue { newClasspath.any { "kotlin-jupyter-ktor-client" in it } }
            newImports shouldContain "org.jetbrains.kotlinx.jupyter.serialization.UntypedAny"
            newImports shouldContain "java.util.concurrent.ConcurrentHashMap"

            compiledData.scripts.shouldNotBeEmpty()
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy