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

org.jetbrains.kotlinx.jupyter.test.repl.ReplTests.kt Maven / Gradle / Ivy

There is a newer version: 0.12.0-356
Show newest version
package org.jetbrains.kotlinx.jupyter.test.repl

import io.kotest.matchers.booleans.shouldBeTrue
import io.kotest.matchers.collections.shouldBeEmpty
import io.kotest.matchers.collections.shouldHaveAtLeastSize
import io.kotest.matchers.maps.shouldContainKey
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.sequences.shouldBeEmpty
import io.kotest.matchers.sequences.shouldHaveSize
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldNotContain
import io.kotest.matchers.types.shouldBeInstanceOf
import jupyter.kotlin.JavaRuntime
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer
import org.jetbrains.kotlinx.jupyter.api.MimeTypedResult
import org.jetbrains.kotlinx.jupyter.api.MimeTypes
import org.jetbrains.kotlinx.jupyter.exceptions.ReplCompilerException
import org.jetbrains.kotlinx.jupyter.exceptions.ReplEvalRuntimeException
import org.jetbrains.kotlinx.jupyter.generateDiagnostic
import org.jetbrains.kotlinx.jupyter.generateDiagnosticFromAbsolute
import org.jetbrains.kotlinx.jupyter.repl.CompletionResult
import org.jetbrains.kotlinx.jupyter.repl.ListErrorsResult
import org.jetbrains.kotlinx.jupyter.repl.OutputConfig
import org.jetbrains.kotlinx.jupyter.repl.result.EvalResultEx
import org.jetbrains.kotlinx.jupyter.test.getOrFail
import org.jetbrains.kotlinx.jupyter.test.renderedValue
import org.jetbrains.kotlinx.jupyter.withPath
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.DisabledOnOs
import org.junit.jupiter.api.condition.EnabledForJreRange
import org.junit.jupiter.api.condition.JRE
import org.junit.jupiter.api.condition.OS
import java.io.File
import java.nio.file.Path
import kotlin.script.experimental.api.SourceCode

class ReplTests : AbstractSingleReplTest() {
    override val repl = makeSimpleRepl()

    @Test
    fun testRepl() {
        eval("val x = 3")
        val res = eval("x*2")
        res.renderedValue shouldBe 6
    }

    @Test
    fun testPropertiesGeneration() {
        // Note, this test should actually fail with ReplEvalRuntimeException, but 'cause of eval/compile
        // histories are out of sync, it fails with another exception. This test shows the wrong behavior and
        // should be fixed after fixing https://youtrack.jetbrains.com/issue/KT-36397

        // In fact, this shouldn't compile, but because of bug in compiler it fails in runtime
        evalError(
            """
            fun stack(vararg tup: Int): Int = tup.sum()
            val X = 1
            val x = stack(1, X)
            """.trimIndent(),
        )
    }

    @Test
    fun `compilation error`() {
        val ex =
            evalError(
                """
                val foobar = 78
                val foobaz = "dsdsda"
                val ddd = ppp
                val ooo = foobar
                """.trimIndent(),
            )

        val diag = ex.firstError
        val location = diag?.location
        val message = ex.message

        val expectedLocation = SourceCode.Location(SourceCode.Position(3, 11), SourceCode.Position(3, 14))
        val expectedMessage = "at Cell In[-1], line 3, column 11: Unresolved reference: ppp"

        location shouldBe expectedLocation
        message shouldBe expectedMessage
    }

    @Test
    fun `runtime execution error`() {
        val ex =
            evalError(
                """
                try {
                    (null as String)
                } catch(e: NullPointerException) {
                    throw RuntimeException("XYZ", e)
                }
                """.trimIndent(),
            )
        with(ex.render()) {
            shouldContain(NullPointerException::class.qualifiedName!!)
            shouldContain("XYZ")
            shouldContain("""at Line_\d+_jupyter.\(Line_\d+\.jupyter.kts:2\)""".toRegex())
            shouldNotContain(ReplEvalRuntimeException::class.simpleName!!)
        }
    }

    @Test
    fun testDependsOnAnnotation() {
        eval("@file:DependsOn(\"de.erichseifert.gral:gral-core:0.11\")")
    }

    @Test
    fun testImportResolutionAfterFailure() {
        val errorsRes = repl.listErrorsBlocking("import net.pearx.kasechange.*")
        errorsRes.errors shouldHaveSize 1

        val res =
            eval(
                """
                @file:DependsOn("net.pearx.kasechange:kasechange-jvm:1.3.0")
                import net.pearx.kasechange.*
                1
                """.trimIndent(),
            )

        res.renderedValue shouldBe 1
    }

    @Test
    fun testDependsOnAnnotationCompletion() {
        eval(
            """
            @file:Repository("https://repo1.maven.org/maven2/")
            @file:DependsOn("com.github.doyaaaaaken:kotlin-csv-jvm:0.7.3")
            """.trimIndent(),
        )

        val res = repl.completeBlocking("import com.github.", 18)
        res.shouldBeInstanceOf()
        res.sortedMatches().contains("doyaaaaaken")
    }

    @Test
    fun testDependencyConfigurationAnnotationCompletion() {
        eval(
            """
            USE {
                repositories {
                    mavenCentral()
                }
                dependencies {
                    implementation("io.github.config4k:config4k:0.4.2")
                }
            }
            """.trimIndent(),
        )

        val res = repl.completeBlocking("import io.github.", 17)
        res.shouldBeInstanceOf()
        res.sortedMatches().contains("config4k")
    }

    @Test
    fun testExternalStaticFunctions() {
        val res =
            eval(
                """
                @file:DependsOn("src/test/testData/kernelTestPackage-1.0.jar")
                import pack.*
                func()
                """.trimIndent(),
            )

        res.renderedValue shouldBe 42
    }

    @Test
    fun testScriptIsolation() {
        evalError("org.jetbrains.kotlinx.jupyter.ReplLineMagics.use")
    }

    @Test
    fun testDependsOnAnnotations() {
        val res =
            eval(
                """
                @file:DependsOn("de.erichseifert.gral:gral-core:0.11")
                @file:Repository("https://maven.pkg.jetbrains.space/public/p/kotlinx-html/maven")
                @file:DependsOn("org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.2")
                """.trimIndent(),
            )

        val newClasspath = res.metadata.newClasspath
        newClasspath shouldHaveAtLeastSize 2

        val htmlLibPath = "org/jetbrains/kotlinx/kotlinx-html-jvm/0.7.2/kotlinx-html-jvm".replace('/', File.separatorChar)

        newClasspath.any { htmlLibPath in it }.shouldBeTrue()
    }

    @Test
    fun testCompletionSimple() {
        eval("val foobar = 42")
        eval("var foobaz = 43")

        val result = repl.completeBlocking("val t = foo", 11)
        result.getOrFail().sortedMatches() shouldBe arrayListOf("foobar", "foobaz")
    }

    @Test
    fun testNoCompletionAfterNumbers() {
        val result = repl.completeBlocking("val t = 42", 10)
        result.getOrFail().sortedMatches().shouldBeEmpty()
    }

    @Test
    fun testCompletionForImplicitReceivers() {
        eval(
            """
            class AClass(val c_prop_x: Int) {
                fun filter(xxx: (AClass).() -> Boolean): AClass {
                    return this
                }
            }
            val AClass.c_prop_y: Int
                get() = c_prop_x * c_prop_x
            
            fun AClass.c_meth_z(v: Int) = v * c_prop_y
            val df = AClass(10)
            val c_zzz = "some string"
            """.trimIndent(),
        )

        val result = repl.completeBlocking("df.filter { c_ }", 14)
        result.getOrFail().sortedMatches() shouldBe arrayListOf("c_meth_z(", "c_prop_x", "c_prop_y", "c_zzz")
    }

    @Test
    fun testParametersCompletion() {
        eval("fun f(xyz: Int) = xyz * 2")

        val result = repl.completeBlocking("val t = f(x", 11)
        result.getOrFail().sortedMatches() shouldBe arrayListOf("xyz = ")
    }

    @Test
    fun testDeprecationCompletion() {
        eval(
            """
            @Deprecated("use id() function instead")
            fun id_deprecated(x: Int) = x
            """.trimIndent(),
        )

        runBlocking {
            val result = repl.completeBlocking("val t = id_d", 12)
            result.getOrFail().sortedRaw().any {
                it.text == "id_deprecated(" && it.deprecationLevel == DeprecationLevel.WARNING
            }.shouldBeTrue()
        }
    }

    @Test
    fun testErrorsList() {
        eval(
            """
            data class AClass(val memx: Int, val memy: String)
            data class BClass(val memz: String, val mema: AClass)
            val foobar = 42
            var foobaz = "string"
            val v = BClass("KKK", AClass(5, "25"))
            """.trimIndent(),
        )

        val result =
            repl.listErrorsBlocking(
                """
                val a = AClass("42", 3.14)
                val b: Int = "str"
                val c = foob
                """.trimIndent(),
            )
        val actualErrors = result.errors.toList()
        val path = actualErrors.first().sourcePath
        actualErrors shouldBe
            withPath(
                path,
                listOf(
                    generateDiagnostic(1, 16, 1, 20, "Type mismatch: inferred type is String but Int was expected", "ERROR"),
                    generateDiagnostic(1, 22, 1, 26, "The floating-point literal does not conform to the expected type String", "ERROR"),
                    generateDiagnostic(2, 14, 2, 19, "Type mismatch: inferred type is String but Int was expected", "ERROR"),
                    generateDiagnostic(3, 9, 3, 13, "Unresolved reference: foob", "ERROR"),
                ),
            )
    }

    @Test
    fun testFreeCompilerArg() {
        val res =
            eval(
                """
                @file:CompilerArgs("-opt-in=kotlin.RequiresOptIn")
                """.trimIndent(),
            )
        res.renderedValue shouldBe Unit

        val actualErrors =
            repl.listErrorsBlocking(
                """
                import kotlin.time.*
                @OptIn(ExperimentalTime::class)
                val mark = TimeSource.Monotonic.markNow()
                """.trimIndent(),
            ).errors.toList()

        actualErrors.shouldBeEmpty()
    }

    @Test
    fun testErrorsListWithMagic() {
        val result =
            repl.listErrorsBlocking(
                """
                %use dataframe
                
                val x = foobar
                3 * 14
                %trackClasspath
                """.trimIndent(),
            )
        val actualErrors = result.errors.toList()
        val path = actualErrors.first().sourcePath
        actualErrors shouldBe
            withPath(
                path,
                listOf(
                    generateDiagnostic(3, 9, 3, 15, "Unresolved reference: foobar", "ERROR"),
                ),
            )
    }

    @Test
    fun testCompletionWithMagic() {
        eval("val foobar = 42")
        val code =
            """
                
            %trackClasspath
            
            foo
            """.trimIndent()

        val result = repl.completeBlocking(code, code.indexOf("foo") + 3)
        result.shouldBeInstanceOf()
        result.sortedMatches() shouldBe arrayListOf("foobar")
    }

    @Test
    fun testCommands() {
        val code1 = ":help"
        val code2 = ":hex "

        runBlocking {
            repl.listErrors(code1) { result ->
                result.code shouldBe code1
                result.errors.shouldBeEmpty()
            }
            repl.listErrors(code2) { result ->
                result.code shouldBe code2
                val expectedList = listOf(generateDiagnosticFromAbsolute(code2, 0, 4, "Unknown command", "ERROR"))
                val actualList = result.errors.toList()
                actualList shouldBe expectedList
            }
            repl.complete(code2, 3) { result ->
                result.shouldBeInstanceOf()
                result.sortedMatches() shouldBe listOf("help")
            }
        }
    }

    @Test
    fun testEmptyErrorsListJson() {
        val res = ListErrorsResult("someCode")
        Json.encodeToString(serializer(), res.message) shouldBe """{"code":"someCode","errors":[]}"""
    }

    @Test
    fun testOut() {
        eval("1+1", 1)
        val res = eval("Out[1]")
        res.renderedValue shouldBe 2
        evalError("Out[3]")
    }

    @Test
    fun testNoHistory() {
        eval("1+1", storeHistory = false)
        evalError("Out[1]")
    }

    @Test
    fun testOutputMagic() {
        val options = repl.options

        eval("%output --max-cell-size=100500 --no-stdout")
        options.outputConfig shouldBe
            OutputConfig(
                cellOutputMaxSize = 100500,
                captureOutput = false,
            )

        eval("%output --max-buffer=42 --max-buffer-newline=33 --max-time=2000")
        options.outputConfig shouldBe
            OutputConfig(
                cellOutputMaxSize = 100500,
                captureOutput = false,
                captureBufferMaxSize = 42,
                captureNewlineBufferSize = 33,
                captureBufferTimeLimitMs = 2000,
            )

        eval("%output --reset-to-defaults")
        options.outputConfig shouldBe OutputConfig()
    }

    @Test
    fun testJavaRuntimeUtils() {
        val result = eval("JavaRuntimeUtils.version")
        val resultVersion = result.renderedValue
        val expectedVersion = JavaRuntime.version
        resultVersion shouldBe expectedVersion
    }

    @Test
    fun testKotlinMath() {
        val result = eval("2.0.pow(2.0)").renderedValue
        result shouldBe 4.0
    }

    @Test
    @DisabledOnOs(OS.MAC)
    @EnabledForJreRange(max = JRE.JAVA_11)
    fun testNativeLibrary() {
        val libName = "GraphMolWrap"
        val testDataPath = "src/test/testData/nativeTest"
        val jarPath = "$testDataPath/org.RDKit.jar"

        val res =
            eval(
                """
                @file:DependsOn("$jarPath")
                import org.RDKit.RWMol
                import org.RDKit.RWMol.MolFromSmiles
                Native.loadLibrary(RWMol::class, "$libName", "$testDataPath")
                MolFromSmiles("c1ccccc1")
                """.trimIndent(),
            ).renderedValue

        res.shouldNotBeNull()
        res::class.qualifiedName shouldBe "org.RDKit.RWMol"
    }

    @Test
    fun testLambdaRendering() {
        val res =
            eval(
                """
                val foo: (Int) -> Int = {it + 1}
                foo
                """.trimIndent(),
            ).renderedValue
        @Suppress("UNCHECKED_CAST")
        (res as (Int) -> Int)(1) shouldBe 2
    }

    @Test
    fun testAnonymousObjectRendering() {
        eval("42")
        eval("val sim = object : ArrayList() {}")
        val res = eval("sim").renderedValue
        res.toString() shouldBe "[]"
    }

    @Test
    fun testAnonymousObjectCustomRendering() {
        eval("USE { render> { it.size } }")
        eval(
            """
            val sim = object : ArrayList() {}
            sim.add("42")
            """.trimIndent(),
        )
        val res = eval("sim").renderedValue
        res shouldBe 1
    }

    @Test
    fun testValueClassRendering() {
        eval(
            """
            class Obj(val x: Int)

            @JvmInline
            value class Wrapper(val o: Obj)
            """.trimIndent(),
        )

        eval(
            """
            USE {
                addRenderer(
                    createRendererByCompileTimeType { (it.value as Obj).x * 2 }
                )
            }
            """.trimIndent(),
        )

        val res = eval("Wrapper(Obj(2))").renderedValue
        res shouldBe 2 * 2
    }

    @Test
    fun testParametrizedClassRendering() {
        eval(
            """
            USE {
                addRenderer(
                    createRendererByCompileTimeType> { (it.value as List).map { x -> x * 2 } }
                )
            }
            """.trimIndent(),
        )

        val res1 = eval("listOf(1, 2)").renderedValue
        res1 shouldBe listOf(2, 4)

        val res2 = eval("listOf('1', '2')").renderedValue
        res2 shouldBe listOf('1', '2')
    }

    @Test
    fun testStdlibJdkExtensionsUsage() {
        eval("USE_STDLIB_EXTENSIONS()")
        val res =
            eval(
                """
                import kotlin.io.path.*
                import java.nio.file.Paths
                
                Paths.get(".").absolute()
                """.trimIndent(),
            ).renderedValue
        res.shouldBeInstanceOf()
    }

    @Test
    fun testArraysRendering() {
        eval("intArrayOf(1, 2, 3)").renderedValue.toString() shouldBe "[1, 2, 3]"
        eval("arrayOf(1 to 2, 3 to 4)").renderedValue.toString() shouldBe "[(1, 2), (3, 4)]"
        eval("booleanArrayOf(true, false)").renderedValue.toString() shouldBe "[true, false]"
    }

    @Test
    fun testOutVarRendering() {
        eval("Out").renderedValue.shouldNotBeNull()
    }

    @Test
    fun testMagicsErrorsReporting() {
        "%us".let { code ->
            listErrors(code).errors.toList() shouldBe listOf(generateDiagnosticFromAbsolute(code, 0, 3, "Unknown magic", "ERROR"))
        }

        "%use kmath".let { code ->
            listErrors(code).errors.toList().shouldBeEmpty()
        }
    }

    @Test
    fun testIssue356() {
        eval(
            """
            sealed class BaseObjClass
            object Obj : BaseObjClass()
            val topLevelSequence = sequence {
               yield(Obj)
            }
            open class Base {
                val iter = topLevelSequence.iterator()
            }
            class Child: Base()
            
            Child::class.simpleName
            """.trimIndent(),
        ).renderedValue shouldBe "Child"
    }

    @Test
    fun testIssue360() {
        eval("val a = 1")
        eval("fun b() = a")
        eval("b()").renderedValue shouldBe 1
    }

    @Test
    fun testRegexBug413() {
        val code =
            """
            Regex("(?[0-9]*)").matchEntire("123456789")?.groups?.get("x")?.value
            """.trimIndent()

        eval(code)
        evalError(code)
    }

    @Test
    fun testRendererRecursion() {
        eval(
            """
            class A(val map: Map<*, *>)
            
            USE {
                render {
                    textResult(it.map.toString())
                }
                render> {
                    A(it)
                }
            }
            """.trimIndent(),
        )
        eval("mapOf()").renderedValue.shouldBeInstanceOf {
            it shouldContainKey MimeTypes.PLAIN_TEXT
            it[MimeTypes.PLAIN_TEXT] shouldBe "{}"
        }
    }

    @Test
    @Disabled("Reproduces KTNB-709")
    fun `check that root package is imported correctly`() {
        // Commenting this execution makes test pass
        eval("notebook")
        eval(
            """
            @file:DependsOn("com.sealwu:kscript-tools:1.0.22")
            """.trimIndent(),
        )
        val result =
            eval(
                """
                evalBash("foo")
                """.trimIndent(),
            )

        result.shouldBeInstanceOf()
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy