diff --git a/lyng/src/jvmTest/kotlin/net/sergeych/CliLocalModuleImportRegressionJvmTest.kt b/lyng/src/jvmTest/kotlin/net/sergeych/CliLocalModuleImportRegressionJvmTest.kt index 831c13a..1f54e34 100644 --- a/lyng/src/jvmTest/kotlin/net/sergeych/CliLocalModuleImportRegressionJvmTest.kt +++ b/lyng/src/jvmTest/kotlin/net/sergeych/CliLocalModuleImportRegressionJvmTest.kt @@ -1,40 +1,92 @@ package net.sergeych +import net.sergeych.lyng.EvalSession +import net.sergeych.lyng.Source +import net.sergeych.lyng.obj.ObjString import kotlinx.coroutines.runBlocking import java.nio.file.Files import kotlin.io.path.writeText import kotlin.test.Test +import kotlin.test.assertEquals class CliLocalModuleImportRegressionJvmTest { + private fun writeTransitiveImportTree(root: java.nio.file.Path) { + val packageDir = Files.createDirectories(root.resolve("package1")) + val nestedDir = Files.createDirectories(packageDir.resolve("nested")) + + packageDir.resolve("alpha.lyng").writeText( + """ + package package1.alpha + + import lyng.stdlib + import lyng.io.net + + class Alpha { + val headers = Map() + + fun makeTask(port: Int, host: String): Deferred = launch { + host + ":" + port + } + + fun netModule() = Net + } + + fun alphaValue() = "alpha" + """.trimIndent() + ) + packageDir.resolve("beta.lyng").writeText( + """ + package package1.beta + + import lyng.stdlib + import package1.alpha + + fun betaValue() = alphaValue() + "|beta" + """.trimIndent() + ) + nestedDir.resolve("gamma.lyng").writeText( + """ + package package1.nested.gamma + + import lyng.io.net + import package1.alpha + import package1.beta + + val String.gammaTag get() = this + "|gamma" + + fun gammaValue() = betaValue().gammaTag + fun netModule() = Net + """.trimIndent() + ) + packageDir.resolve("entry.lyng").writeText( + """ + package package1.entry + + import lyng.stdlib + import lyng.io.net + import package1.alpha + import package1.beta + import package1.nested.gamma + + fun report() = gammaValue() + "|entry" + """.trimIndent() + ) + } + @Test fun localModuleUsingLaunchAndNetImportsWithoutStdlibRedefinition() = runBlocking { val root = Files.createTempDirectory("lyng-cli-import-regression") try { - val packageDir = Files.createDirectories(root.resolve("package1")) val mainFile = root.resolve("main.lyng") - val alphaFile = packageDir.resolve("alpha.lyng") - + writeTransitiveImportTree(root) mainFile.writeText( """ - import package1.alpha + import package1.entry + import package1.beta + import package1.nested.gamma - println("ok") - """.trimIndent() - ) - alphaFile.writeText( - """ - import lyng.io.net - - class Alpha { - val headers = Map() - - fn startListen(port, host) { - launch { - println(port, host) - } - } - } + println(report()) """.trimIndent() ) @@ -43,4 +95,43 @@ class CliLocalModuleImportRegressionJvmTest { root.toFile().deleteRecursively() } } + + @Test + fun localModuleImportsAreNoOpsWhenEvaldRepeatedlyOnSameCliContext() = runBlocking { + val root = Files.createTempDirectory("lyng-cli-import-regression-repeat") + try { + val mainFile = root.resolve("main.lyng") + writeTransitiveImportTree(root) + mainFile.writeText("println(\"bootstrap\")") + + val session = EvalSession(newCliScope(emptyList(), mainFile.toString())) + try { + repeat(5) { index -> + val result = evalOnCliDispatcher( + session, + Source( + "", + """ + import package1.entry + import package1.nested.gamma + import package1.beta + import package1.alpha + + report() + """.trimIndent() + ) + ) as ObjString + + assertEquals( + "alpha|beta|gamma|entry", + result.value + ) + } + } finally { + session.cancelAndJoin() + } + } finally { + root.toFile().deleteRecursively() + } + } } diff --git a/lyng/src/jvmTest/kotlin/net/sergeych/lyng_cli/CliLocalImportsJvmTest.kt b/lyng/src/jvmTest/kotlin/net/sergeych/lyng_cli/CliLocalImportsJvmTest.kt index 6f37af6..77afd02 100644 --- a/lyng/src/jvmTest/kotlin/net/sergeych/lyng_cli/CliLocalImportsJvmTest.kt +++ b/lyng/src/jvmTest/kotlin/net/sergeych/lyng_cli/CliLocalImportsJvmTest.kt @@ -64,6 +64,73 @@ class CliLocalImportsJvmTest { return CliResult(outBuf.toString("UTF-8"), errBuf.toString("UTF-8"), exitCode) } + private fun writeTransitiveImportTree(root: java.nio.file.Path) { + val packageDir = Files.createDirectories(root.resolve("package1")) + val nestedDir = Files.createDirectories(packageDir.resolve("nested")) + + Files.writeString( + packageDir.resolve("alpha.lyng"), + """ + package package1.alpha + + import lyng.stdlib + import lyng.io.net + + class Alpha { + val headers = Map() + + fun makeTask(port: Int, host: String): Deferred = launch { + host + ":" + port + } + + fun netModule() = Net + } + + fun alphaValue() = "alpha" + """.trimIndent() + ) + Files.writeString( + packageDir.resolve("beta.lyng"), + """ + package package1.beta + + import lyng.stdlib + import package1.alpha + + fun betaValue() = alphaValue() + "|beta" + """.trimIndent() + ) + Files.writeString( + nestedDir.resolve("gamma.lyng"), + """ + package package1.nested.gamma + + import lyng.io.net + import package1.alpha + import package1.beta + + val String.gammaTag get() = this + "|gamma" + + fun gammaValue() = betaValue().gammaTag + fun netModule() = Net + """.trimIndent() + ) + Files.writeString( + packageDir.resolve("entry.lyng"), + """ + package package1.entry + + import lyng.stdlib + import lyng.io.net + import package1.alpha + import package1.beta + import package1.nested.gamma + + fun report() = gammaValue() + "|entry" + """.trimIndent() + ) + } + @Test fun cliDiscoversSiblingAndNestedLocalImportsFromEntryRoot() { val dir = Files.createTempDirectory("lyng_cli_local_imports_") @@ -134,4 +201,37 @@ class CliLocalImportsJvmTest { dir.toFile().deleteRecursively() } } + + @Test + fun cliHandlesOverlappingDirectoryImportsWithTransitiveStdlibAndNetSymbols() { + val dir = Files.createTempDirectory("lyng_cli_local_imports_transitive_") + try { + val mainFile = dir.resolve("main.lyng") + writeTransitiveImportTree(dir) + Files.writeString( + mainFile, + """ + import package1.entry + import package1.beta + import package1.nested.gamma + + println(report()) + println(gammaValue()) + """.trimIndent() + ) + + val result = runCli(mainFile.toString()) + assertTrue(result.err, result.err.isBlank()) + assertTrue( + result.out, + result.out.contains("alpha|beta|gamma|entry") + ) + assertTrue( + result.out, + result.out.contains("alpha|beta|gamma") + ) + } finally { + dir.toFile().deleteRecursively() + } + } } diff --git a/lynglib/src/commonTest/kotlin/ScriptImportPreparationTest.kt b/lynglib/src/commonTest/kotlin/ScriptImportPreparationTest.kt index 6a82ff2..2444b1a 100644 --- a/lynglib/src/commonTest/kotlin/ScriptImportPreparationTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptImportPreparationTest.kt @@ -16,17 +16,60 @@ import kotlinx.coroutines.test.runTest import net.sergeych.lyng.Compiler +import net.sergeych.lyng.EvalSession import net.sergeych.lyng.Script import net.sergeych.lyng.Source +import net.sergeych.lyng.obj.ObjString import net.sergeych.lyng.obj.toInt import net.sergeych.lyng.pacman.ImportManager import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertSame class ScriptImportPreparationTest { + private fun nestedImportSources(prefix: String): Array = + arrayOf( + """ + package $prefix.alpha + + class Alpha { + val headers = Map() + + fun tagged(port: Int, host: String): String { + val task: Deferred = launch { + host + ":" + port + ":" + headers.size + } + return task.await() + } + } + + fun alphaValue() = Alpha().tagged(7, "alpha") + """.trimIndent(), + """ + package $prefix.beta + import $prefix.alpha + + fun betaValue() = alphaValue() + "|" + Alpha().tagged(8, "beta") + """.trimIndent(), + """ + package $prefix.gamma + import $prefix.alpha + import $prefix.beta + + val String.gammaTag get() = this + ":gamma" + + fun gammaValue() = betaValue() + "|" + "done".gammaTag + """.trimIndent() + ) + + private fun nestedImportManager(prefix: String = "tree"): ImportManager = + Script.defaultImportManager.copy().apply { + addTextPackages(*nestedImportSources(prefix)) + } + @Test fun scriptImportIntoExplicitlyPreparesExistingScope() = runTest { val manager = ImportManager() @@ -84,4 +127,67 @@ class ScriptImportPreparationTest { val record = assertNotNull(module["answer"]) assertEquals(42, module.resolve(record, "answer").toInt()) } + + @Test + fun repeatedImportIntoOnSameScopeIsIdempotentForNestedPackageGraph() = runTest { + val manager = nestedImportManager() + val script = Compiler.compile( + Source( + "", + """ + import tree.gamma + import tree.beta + import tree.alpha + + gammaValue() + """.trimIndent() + ), + manager + ) + val scope = manager.newStdScope() + + script.importInto(scope) + val importedGammaValue = assertNotNull(scope["gammaValue"]) + val importedAlpha = assertNotNull(scope["Alpha"]) + + repeat(5) { + script.importInto(scope) + } + + assertSame(importedGammaValue, scope["gammaValue"]) + assertSame(importedAlpha, scope["Alpha"]) + assertEquals( + "alpha:7:0|beta:8:0|done:gamma", + (script.execute(scope) as ObjString).value + ) + } + + @Test + fun repeatedEvalOnSameSessionCanReimportNestedPackageGraph() = runTest { + val prefix = "repeattree" + val manager = nestedImportManager(prefix) + val scope = manager.newModule() + val session = EvalSession(scope) + + try { + repeat(5) { index -> + val result = session.eval( + Source( + "", + """ + import $prefix.gamma + import $prefix.beta + import $prefix.alpha + + gammaValue() + """.trimIndent() + ) + ) as ObjString + + assertEquals("alpha:7:0|beta:8:0|done:gamma", result.value) + } + } finally { + session.cancelAndJoin() + } + } }