Skip to content

KotlinJsSourcePreparationSmokeTest.kt

src/jvmMain/kotlin/afield/examples/KotlinJsSourcePreparationSmokeTest.kt · 121 lines

package afield.examples

import afield.lang.remotejs.KotlinJsHeadProfiles
import afield.lang.remotejs.KotlinToJsCompiler
import java.io.File
import java.nio.file.Files
import afield.media.image.util.NodeTool

object KotlinJsSourcePreparationSmokeTest

FIELD_TEST id: remotejs.kotlinjs-source-preparation runner: jvm-main entrypoint: afield.examples.KotlinJsSourcePreparationSmokeTest tags: [smoke, remotejs, kotlinjs] timeoutSec: 180 expect: exitCode: 0 stdoutContains: - "KotlinJsSourcePreparationSmokeTest ok"

fun main(args: Array<String>)

body — 99 lines, from line 22
    @JvmStatic
    fun main(args: Array<String>) {
        val source = """
            // {{ remoteAR }}
            data class V(val x: Double, val y: Double) {
                operator fun plus(o: V) = V(x + o.x, y + o.y)
            }

            val a = V(1.0, 2.0)
            val b = a + V(3.0, 4.0)
            var counter = 1
            counter += 1
            print("unrelated initializer")
            print("vectors: " + a + b + " counter=" + counter)
        """.trimIndent()
        val selection = KotlinJsHeadProfiles.resolve(source)
        val printLine = source.lineSequence().indexOfFirst { it.contains("vectors:") } + 1
        val stateKey = "source_prep_state"

        // Whole execution: script declarations are file scoped, both initializers run, and Kotlin's
        // ordinary print resolves to Field's global print hook.
        val wholePrepared = selection.profile.prepareSourceMapped(selection.source, stateKey = stateKey)
        val whole = KotlinToJsCompiler.compile(
            wholePrepared.text,
            moduleName = "source_prep_whole",
            wrapImplicitMain = false,
        ).remapDiagnostics(wholePrepared)
        require(whole.success && whole.js != null) { whole.rawLog.takeLast(2000) }

        // Fragment execution: compile against every declaration in the current box, but run only
        // the selected initializer. This is the regression from the editor screenshot: a, b and
        // the mutated value of counter remain in scope, while unrelated side effects are not replayed.
        val fragmentPrepared = selection.profile.prepareSourceMapped(
            selection.source,
            activeUserLines = printLine..printLine,
            stateKey = stateKey,
        )
        val fragment = KotlinToJsCompiler.compile(
            fragmentPrepared.text,
            moduleName = "source_prep_fragment",
            wrapImplicitMain = false,
        ).remapDiagnostics(fragmentPrepared)
        require(fragment.success && fragment.js != null) { fragment.rawLog.takeLast(2000) }
        val output = runNode(whole.js, fragment.js)
        require(output.lineSequence().count { it == "FIELD_PRINT:${printLine - 1}:unrelated initializer" } == 1) { output }
        require(output.lineSequence().count {
            it == "FIELD_PRINT:$printLine:vectors: V(x=1, y=2)V(x=4, y=6) counter=2"
        } == 2) { output }

        // A compiler error in the selected initializer maps back through synthesized main to the
        // exact one-based editor line.
        val brokenSource = source.replace(
            """print("vectors: " + a + b + " counter=" + counter)""",
            """print("vectors: " + missingName)""",
        )
        val brokenSelection = KotlinJsHeadProfiles.resolve(brokenSource)
        val brokenPrepared = brokenSelection.profile.prepareSourceMapped(
            brokenSelection.source,
            activeUserLines = printLine..printLine,
            stateKey = stateKey,
        )
        val broken = KotlinToJsCompiler.compile(
            brokenPrepared.text,
            moduleName = "source_prep_broken",
            wrapImplicitMain = false,
        ).remapDiagnostics(brokenPrepared)
        require(!broken.success) { "broken fragment unexpectedly compiled" }
        require(broken.diagnostics.any { it.severity == "error" && it.line == printLine }) {
            "expected diagnostic on editor line $printLine, got ${broken.diagnostics}"
        }

        println("KotlinJsSourcePreparationSmokeTest ok")
    }

    private fun runNode(vararg javascript: String): String {
        val node = requireNotNull(NodeTool.resolveNode()) {
            "node is required to verify generated Kotlin/JS execution and retained state: " +
                NodeTool.unavailableMessage()
        }
        val script = Files.createTempFile("field-kotlinjs-source-prep", ".js").toFile()
        return try {
            script.writeText(
                """
                global.print = function (value, line) {
                    console.log("FIELD_PRINT:" + String(line) + ":" + String(value));
                };
                ${javascript.joinToString("\n")}
                """.trimIndent(),
            )
            val process = ProcessBuilder(node, script.absolutePath).redirectErrorStream(true).start()
            val output = process.inputStream.bufferedReader().readText().trim()
            process.waitFor()
            require(process.exitValue() == 0) { "node failed: $output" }
            output
        } finally {
            script.delete()
        }
    }
}