package structure/import for CLI
This commit is contained in:
parent
b6c6ef021a
commit
ef95ed4405
@ -4,6 +4,7 @@ The Lyng CLI is the reference command-line tool for the Lyng language. It lets y
|
|||||||
|
|
||||||
- Run Lyng scripts from files or inline strings (shebangs accepted)
|
- Run Lyng scripts from files or inline strings (shebangs accepted)
|
||||||
- Use standard argument passing (`ARGV`) to your scripts.
|
- Use standard argument passing (`ARGV`) to your scripts.
|
||||||
|
- Resolve local file imports from the executed script's directory tree.
|
||||||
- Format Lyng source files via the built-in `fmt` subcommand.
|
- Format Lyng source files via the built-in `fmt` subcommand.
|
||||||
|
|
||||||
|
|
||||||
@ -73,6 +74,7 @@ lyng -- -my-script.lyng arg1 arg2
|
|||||||
```
|
```
|
||||||
|
|
||||||
- Execute inline code with `-x/--execute` and pass positional args to `ARGV`:
|
- Execute inline code with `-x/--execute` and pass positional args to `ARGV`:
|
||||||
|
- Inline execution does not scan the filesystem for local modules; only file-based execution does.
|
||||||
|
|
||||||
```
|
```
|
||||||
lyng -x "println(\"Hello\")" more args
|
lyng -x "println(\"Hello\")" more args
|
||||||
@ -85,6 +87,63 @@ lyng --version
|
|||||||
lyng --help
|
lyng --help
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### Local imports for file execution
|
||||||
|
|
||||||
|
When you execute a script file, the CLI builds a temporary local import manager rooted at the directory that contains the entry script.
|
||||||
|
|
||||||
|
Formal structure:
|
||||||
|
|
||||||
|
- Root directory: the parent directory of the script passed to `lyng`.
|
||||||
|
- Scan scope: every `.lyng` file under that root directory, recursively.
|
||||||
|
- Entry script: the executed file itself is not registered as an importable module.
|
||||||
|
- Module name mapping: `relative/path/to/file.lyng` maps to import name `relative.path.to.file`.
|
||||||
|
- Package declaration: if a scanned file starts with `package ...` as its first non-blank line, that package name must exactly match the relative path mapping.
|
||||||
|
- Package omission: if there is no leading `package` declaration, the CLI uses the relative path mapping as the module name.
|
||||||
|
- Duplicates: if two files resolve to the same module name, CLI execution fails before script execution starts.
|
||||||
|
- Import visibility: only files inside the entry root subtree are considered. Parent directories and sibling projects are not searched.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```
|
||||||
|
project/
|
||||||
|
main.lyng
|
||||||
|
util/answer.lyng
|
||||||
|
math/add.lyng
|
||||||
|
```
|
||||||
|
|
||||||
|
`util/answer.lyng` is imported as `import util.answer`.
|
||||||
|
|
||||||
|
`math/add.lyng` is imported as `import math.add`.
|
||||||
|
|
||||||
|
Example contents:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
// util/answer.lyng
|
||||||
|
package util.answer
|
||||||
|
|
||||||
|
import math.add
|
||||||
|
|
||||||
|
fun answer() = plus(40, 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
// math/add.lyng
|
||||||
|
fun plus(a, b) = a + b
|
||||||
|
```
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
// main.lyng
|
||||||
|
import util.answer
|
||||||
|
|
||||||
|
println(answer())
|
||||||
|
```
|
||||||
|
|
||||||
|
Rationale:
|
||||||
|
|
||||||
|
- The module name is deterministic from the filesystem layout.
|
||||||
|
- Explicit `package` remains available as a consistency check instead of a second, conflicting naming system.
|
||||||
|
- The import search space stays local to the executed script, which avoids accidental cross-project resolution.
|
||||||
|
|
||||||
### Use in shell scripts
|
### Use in shell scripts
|
||||||
|
|
||||||
Standard unix shebangs (`#!`) are supported, so you can make Lyng scripts directly executable on Unix-like systems. For example:
|
Standard unix shebangs (`#!`) are supported, so you can make Lyng scripts directly executable on Unix-like systems. For example:
|
||||||
|
|||||||
@ -31,6 +31,8 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import net.sergeych.lyng.EvalSession
|
import net.sergeych.lyng.EvalSession
|
||||||
import net.sergeych.lyng.LyngVersion
|
import net.sergeych.lyng.LyngVersion
|
||||||
|
import net.sergeych.lyng.Pos
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.Script
|
import net.sergeych.lyng.Script
|
||||||
import net.sergeych.lyng.ScriptError
|
import net.sergeych.lyng.ScriptError
|
||||||
import net.sergeych.lyng.Source
|
import net.sergeych.lyng.Source
|
||||||
@ -40,17 +42,15 @@ import net.sergeych.lyng.io.http.createHttpModule
|
|||||||
import net.sergeych.lyng.io.net.createNetModule
|
import net.sergeych.lyng.io.net.createNetModule
|
||||||
import net.sergeych.lyng.io.ws.createWsModule
|
import net.sergeych.lyng.io.ws.createWsModule
|
||||||
import net.sergeych.lyng.obj.*
|
import net.sergeych.lyng.obj.*
|
||||||
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
|
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
|
||||||
import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy
|
import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy
|
||||||
import net.sergeych.lyngio.http.security.PermitAllHttpAccessPolicy
|
import net.sergeych.lyngio.http.security.PermitAllHttpAccessPolicy
|
||||||
import net.sergeych.lyngio.net.security.PermitAllNetAccessPolicy
|
import net.sergeych.lyngio.net.security.PermitAllNetAccessPolicy
|
||||||
import net.sergeych.lyngio.ws.security.PermitAllWsAccessPolicy
|
import net.sergeych.lyngio.ws.security.PermitAllWsAccessPolicy
|
||||||
import net.sergeych.mp_tools.globalDefer
|
import net.sergeych.mp_tools.globalDefer
|
||||||
import okio.FileSystem
|
import okio.*
|
||||||
import okio.Path.Companion.toPath
|
import okio.Path.Companion.toPath
|
||||||
import okio.SYSTEM
|
|
||||||
import okio.buffer
|
|
||||||
import okio.use
|
|
||||||
|
|
||||||
// common code
|
// common code
|
||||||
|
|
||||||
@ -72,21 +72,157 @@ data class CommandResult(
|
|||||||
|
|
||||||
val baseScopeDefer = globalDefer {
|
val baseScopeDefer = globalDefer {
|
||||||
Script.newScope().apply {
|
Script.newScope().apply {
|
||||||
addFn("exit") {
|
installCliBuiltins()
|
||||||
exit(requireOnlyArg<ObjInt>().toInt())
|
installCliModules(importManager)
|
||||||
ObjVoid
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Scope.installCliBuiltins() {
|
||||||
|
addFn("exit") {
|
||||||
|
exit(requireOnlyArg<ObjInt>().toInt())
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun installCliModules(manager: ImportManager) {
|
||||||
|
// Scripts still need to import the modules they use explicitly.
|
||||||
|
createFs(PermitAllAccessPolicy, manager)
|
||||||
|
createConsoleModule(PermitAllConsoleAccessPolicy, manager)
|
||||||
|
createHttpModule(PermitAllHttpAccessPolicy, manager)
|
||||||
|
createWsModule(PermitAllWsAccessPolicy, manager)
|
||||||
|
createNetModule(PermitAllNetAccessPolicy, manager)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class LocalCliModule(
|
||||||
|
val packageName: String,
|
||||||
|
val source: Source
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun readUtf8(path: Path): String =
|
||||||
|
FileSystem.SYSTEM.source(path).use { fileSource ->
|
||||||
|
fileSource.buffer().use { bs ->
|
||||||
|
bs.readUtf8()
|
||||||
}
|
}
|
||||||
// Install lyng.io.fs module with full access by default for the CLI tool's Scope.
|
}
|
||||||
// Scripts still need to `import lyng.io.fs` to use Path API.
|
|
||||||
createFs(PermitAllAccessPolicy, this)
|
private fun stripShebang(text: String): String {
|
||||||
// Install console access by default for interactive CLI scripts.
|
if (!text.startsWith("#!")) return text
|
||||||
// Scripts still need to `import lyng.io.console` to use it.
|
val pos = text.indexOf('\n')
|
||||||
createConsoleModule(PermitAllConsoleAccessPolicy, this)
|
return if (pos >= 0) text.substring(pos + 1) else ""
|
||||||
// Install network-oriented lyngio modules for CLI scripts.
|
}
|
||||||
// Scripts still need to import the modules they use explicitly.
|
|
||||||
createHttpModule(PermitAllHttpAccessPolicy, this)
|
private fun extractDeclaredPackageNameOrNull(source: Source): String? {
|
||||||
createWsModule(PermitAllWsAccessPolicy, this)
|
for (line in source.lines) {
|
||||||
createNetModule(PermitAllNetAccessPolicy, this)
|
if (line.isBlank()) continue
|
||||||
|
return if (line.startsWith("package ")) {
|
||||||
|
line.substring(8).trim()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun canonicalPath(path: Path): Path = FileSystem.SYSTEM.canonicalize(path)
|
||||||
|
|
||||||
|
private fun relativeModuleName(rootDir: Path, file: Path): String {
|
||||||
|
val rootText = rootDir.toString().trimEnd('/', '\\')
|
||||||
|
val fileText = file.toString()
|
||||||
|
val prefix = "$rootText/"
|
||||||
|
if (!fileText.startsWith(prefix)) {
|
||||||
|
throw ScriptError(Pos.builtIn, "local import root mismatch: $fileText is not under $rootText")
|
||||||
|
}
|
||||||
|
val relative = fileText.removePrefix(prefix)
|
||||||
|
val modulePath = relative.removeSuffix(".lyng")
|
||||||
|
return modulePath
|
||||||
|
.split('/', '\\')
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
|
.joinToString(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scanLyngFiles(rootDir: Path): List<Path> {
|
||||||
|
val system = FileSystem.SYSTEM
|
||||||
|
val pending = ArrayDeque<Path>()
|
||||||
|
val visited = linkedSetOf<String>()
|
||||||
|
val files = mutableListOf<Path>()
|
||||||
|
pending.add(rootDir)
|
||||||
|
while (pending.isNotEmpty()) {
|
||||||
|
val dir = pending.removeLast()
|
||||||
|
val canonicalDir = canonicalPath(dir)
|
||||||
|
if (!visited.add(canonicalDir.toString())) continue
|
||||||
|
val children = try {
|
||||||
|
system.list(canonicalDir)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (child in children) {
|
||||||
|
val meta = try {
|
||||||
|
system.metadata(child)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
when {
|
||||||
|
meta.isDirectory -> pending.add(child)
|
||||||
|
child.name.endsWith(".lyng") -> {
|
||||||
|
val canonicalFile = try {
|
||||||
|
canonicalPath(child)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
files += canonicalFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun discoverLocalCliModules(entryFile: Path): List<LocalCliModule> {
|
||||||
|
val rootDir = entryFile.parent ?: ".".toPath()
|
||||||
|
val seenPackages = linkedMapOf<String, Path>()
|
||||||
|
return scanLyngFiles(rootDir)
|
||||||
|
.asSequence()
|
||||||
|
.filter { it != entryFile }
|
||||||
|
.map { file ->
|
||||||
|
val text = stripShebang(readUtf8(file))
|
||||||
|
val source = Source(file.toString(), text)
|
||||||
|
val expectedPackage = relativeModuleName(rootDir, file)
|
||||||
|
val declaredPackage = extractDeclaredPackageNameOrNull(source)
|
||||||
|
if (declaredPackage != null && declaredPackage != expectedPackage) {
|
||||||
|
throw ScriptError(
|
||||||
|
source.startPos,
|
||||||
|
"local module package mismatch: expected '$expectedPackage' for ${file.toString()} but found '$declaredPackage'"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val packageName = declaredPackage ?: expectedPackage
|
||||||
|
val previous = seenPackages.putIfAbsent(packageName, file)
|
||||||
|
if (previous != null) {
|
||||||
|
throw ScriptError(
|
||||||
|
source.startPos,
|
||||||
|
"duplicate local module '$packageName': ${previous.toString()} and ${file.toString()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LocalCliModule(packageName, source)
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerLocalCliModules(manager: ImportManager, entryFile: Path) {
|
||||||
|
for (module in discoverLocalCliModules(entryFile)) {
|
||||||
|
manager.addPackage(module.packageName) { scope ->
|
||||||
|
scope.eval(module.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun newCliScope(argv: List<String>, entryFileName: String? = null): Scope {
|
||||||
|
val manager = baseScopeDefer.await().importManager.copy()
|
||||||
|
if (entryFileName != null) {
|
||||||
|
registerLocalCliModules(manager, canonicalPath(entryFileName.toPath()))
|
||||||
|
}
|
||||||
|
return manager.newStdScope().apply {
|
||||||
|
installCliBuiltins()
|
||||||
|
addConst("ARGV", ObjList(argv.map { ObjString(it) }.toMutableList()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,7 +336,6 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
|
|||||||
if (currentContext.invokedSubcommand != null) return
|
if (currentContext.invokedSubcommand != null) return
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val baseScope = baseScopeDefer.await()
|
|
||||||
when {
|
when {
|
||||||
version -> {
|
version -> {
|
||||||
println("Lyng language version ${LyngVersion}")
|
println("Lyng language version ${LyngVersion}")
|
||||||
@ -210,15 +345,13 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
|
|||||||
val objargs = mutableListOf<String>()
|
val objargs = mutableListOf<String>()
|
||||||
script?.let { objargs += it }
|
script?.let { objargs += it }
|
||||||
objargs += args
|
objargs += args
|
||||||
baseScope.addConst(
|
|
||||||
"ARGV", ObjList(
|
|
||||||
objargs.map { ObjString(it) }.toMutableList()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
launcher {
|
launcher {
|
||||||
// there is no script name, it is a first argument instead:
|
// there is no script name, it is a first argument instead:
|
||||||
processErrors {
|
processErrors {
|
||||||
executeSource(Source("<eval>", execute!!))
|
executeSource(
|
||||||
|
Source("<eval>", execute!!),
|
||||||
|
newCliScope(objargs)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,8 +361,7 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
|
|||||||
println("Error: no script specified.\n")
|
println("Error: no script specified.\n")
|
||||||
echoFormattedHelp()
|
echoFormattedHelp()
|
||||||
} else {
|
} else {
|
||||||
baseScope.addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList()))
|
launcher { executeFile(script!!, args) }
|
||||||
launcher { executeFile(script!!) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -239,13 +371,12 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
|
|||||||
|
|
||||||
fun executeFileWithArgs(fileName: String, args: List<String>) {
|
fun executeFileWithArgs(fileName: String, args: List<String>) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
baseScopeDefer.await().addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList()))
|
executeFile(fileName, args)
|
||||||
executeFile(fileName)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun executeSource(source: Source) {
|
suspend fun executeSource(source: Source, initialScope: Scope? = null) {
|
||||||
val session = EvalSession(baseScopeDefer.await())
|
val session = EvalSession(initialScope ?: baseScopeDefer.await())
|
||||||
try {
|
try {
|
||||||
evalOnCliDispatcher(session, source)
|
evalOnCliDispatcher(session, source)
|
||||||
} finally {
|
} finally {
|
||||||
@ -258,19 +389,14 @@ internal suspend fun evalOnCliDispatcher(session: EvalSession, source: Source):
|
|||||||
session.eval(source)
|
session.eval(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun executeFile(fileName: String) {
|
suspend fun executeFile(fileName: String, args: List<String> = emptyList()) {
|
||||||
var text = FileSystem.SYSTEM.source(fileName.toPath()).use { fileSource ->
|
val canonicalFile = canonicalPath(fileName.toPath())
|
||||||
fileSource.buffer().use { bs ->
|
val text = stripShebang(readUtf8(canonicalFile))
|
||||||
bs.readUtf8()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if( text.startsWith("#!") ) {
|
|
||||||
// skip shebang
|
|
||||||
val pos = text.indexOf('\n')
|
|
||||||
text = text.substring(pos + 1)
|
|
||||||
}
|
|
||||||
processErrors {
|
processErrors {
|
||||||
executeSource(Source(fileName, text))
|
executeSource(
|
||||||
|
Source(canonicalFile.toString(), text),
|
||||||
|
newCliScope(args, canonicalFile.toString())
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package net.sergeych.lyng_cli
|
||||||
|
|
||||||
|
import net.sergeych.jvmExitImpl
|
||||||
|
import net.sergeych.runMain
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.PrintStream
|
||||||
|
import java.nio.file.Files
|
||||||
|
|
||||||
|
class CliLocalImportsJvmTest {
|
||||||
|
private val originalOut: PrintStream = System.out
|
||||||
|
private val originalErr: PrintStream = System.err
|
||||||
|
|
||||||
|
private class TestExit(val code: Int) : RuntimeException()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
jvmExitImpl = { code -> throw TestExit(code) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
System.setOut(originalOut)
|
||||||
|
System.setErr(originalErr)
|
||||||
|
jvmExitImpl = { code -> kotlin.system.exitProcess(code) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class CliResult(val out: String, val err: String, val exitCode: Int?)
|
||||||
|
|
||||||
|
private fun runCli(vararg args: String): CliResult {
|
||||||
|
val outBuf = ByteArrayOutputStream()
|
||||||
|
val errBuf = ByteArrayOutputStream()
|
||||||
|
System.setOut(PrintStream(outBuf, true, Charsets.UTF_8))
|
||||||
|
System.setErr(PrintStream(errBuf, true, Charsets.UTF_8))
|
||||||
|
|
||||||
|
var exitCode: Int? = null
|
||||||
|
try {
|
||||||
|
runMain(arrayOf(*args))
|
||||||
|
} catch (e: TestExit) {
|
||||||
|
exitCode = e.code
|
||||||
|
} finally {
|
||||||
|
System.out.flush()
|
||||||
|
System.err.flush()
|
||||||
|
}
|
||||||
|
return CliResult(outBuf.toString("UTF-8"), errBuf.toString("UTF-8"), exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun cliDiscoversSiblingAndNestedLocalImportsFromEntryRoot() {
|
||||||
|
val dir = Files.createTempDirectory("lyng_cli_local_imports_")
|
||||||
|
try {
|
||||||
|
val mathDir = Files.createDirectories(dir.resolve("math"))
|
||||||
|
val utilDir = Files.createDirectories(dir.resolve("util"))
|
||||||
|
val mainFile = dir.resolve("main.lyng")
|
||||||
|
Files.writeString(
|
||||||
|
mathDir.resolve("add.lyng"),
|
||||||
|
"""
|
||||||
|
fun plus(a, b) = a + b
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
Files.writeString(
|
||||||
|
utilDir.resolve("answer.lyng"),
|
||||||
|
"""
|
||||||
|
package util.answer
|
||||||
|
|
||||||
|
import math.add
|
||||||
|
|
||||||
|
fun answer() = plus(40, 2)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
Files.writeString(
|
||||||
|
mainFile,
|
||||||
|
"""
|
||||||
|
import util.answer
|
||||||
|
|
||||||
|
println(answer())
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = runCli(mainFile.toString())
|
||||||
|
assertTrue(result.err, result.err.isBlank())
|
||||||
|
assertTrue(result.out, result.out.contains("42"))
|
||||||
|
} finally {
|
||||||
|
dir.toFile().deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun cliRejectsPackageThatDoesNotMatchRelativePath() {
|
||||||
|
val dir = Files.createTempDirectory("lyng_cli_local_imports_badpkg_")
|
||||||
|
try {
|
||||||
|
val utilDir = Files.createDirectories(dir.resolve("util"))
|
||||||
|
val mainFile = dir.resolve("main.lyng")
|
||||||
|
Files.writeString(
|
||||||
|
utilDir.resolve("answer.lyng"),
|
||||||
|
"""
|
||||||
|
package util.wrong
|
||||||
|
|
||||||
|
fun answer() = 42
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
Files.writeString(
|
||||||
|
mainFile,
|
||||||
|
"""
|
||||||
|
import util.answer
|
||||||
|
|
||||||
|
println(answer())
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = runCli(mainFile.toString())
|
||||||
|
assertTrue(result.out, result.out.contains("local module package mismatch"))
|
||||||
|
assertTrue(result.out, result.out.contains("expected 'util.answer'"))
|
||||||
|
} finally {
|
||||||
|
dir.toFile().deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user