- fixed bug in compiler (rare)
- added lyng.io.fs (multiplatform) - CLI tools now have access to the filesystem
This commit is contained in:
1 parent
41746f22e5
commit
438e48959e
28 files changed
+2137
-6
No files matched your search
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Copyright 2025 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.lyngio.fs
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.ExecutionError
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.io.fs.createFs
|
||||
import net.sergeych.lyng.obj.ObjIllegalOperationException
|
||||
import net.sergeych.lyngio.fs.security.*
|
||||
import kotlin.io.path.createTempDirectory
|
||||
import kotlin.io.path.writeText
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class FsModuleJvmScriptStyleTest {
|
||||
|
||||
private fun newScope(): Scope = Scope.new()
|
||||
|
||||
private suspend fun installModule(scope: Scope) {
|
||||
createFs(PermitAllAccessPolicy, scope)
|
||||
scope.eval("import lyng.io.fs")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun basicTextIoScript() {
|
||||
runBlocking {
|
||||
val scope = newScope()
|
||||
installModule(scope)
|
||||
|
||||
val dir = createTempDirectory("lyngio_script_basic_")
|
||||
try {
|
||||
val file = dir.resolve("hello.txt")
|
||||
scope.eval(
|
||||
"""
|
||||
import lyng.io.fs
|
||||
import lyng.stdlib
|
||||
|
||||
val p = Path("${file}")
|
||||
assertEquals(false, p.exists())
|
||||
p.writeUtf8("Hello from Lyng")
|
||||
assertEquals(true, p.exists())
|
||||
assertEquals("Hello from Lyng", p.readUtf8())
|
||||
println(p.name)
|
||||
assertEquals("hello.txt", p.name)
|
||||
assert(p.segments is List )
|
||||
assert(p.parent is Path)
|
||||
""".trimIndent()
|
||||
)
|
||||
} finally {
|
||||
dir.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun listGlobAndMetadataScript() {
|
||||
runBlocking {
|
||||
val scope = newScope()
|
||||
installModule(scope)
|
||||
|
||||
val dir = createTempDirectory("lyngio_script_glob_")
|
||||
try {
|
||||
val a = dir.resolve("a").toFile().apply { mkdirs() }
|
||||
dir.resolve("a/b").toFile().apply { mkdirs() }
|
||||
dir.resolve("a/one.txt").writeText("1")
|
||||
dir.resolve("a/b/two.txt").writeText("2")
|
||||
dir.resolve("a/b/three.md").writeText("3")
|
||||
|
||||
scope.eval(
|
||||
"""
|
||||
import lyng.io.fs
|
||||
val root = Path("${a}")
|
||||
val list = root.list().toList()
|
||||
val names = list.map { it.toString() }
|
||||
var hasOne = false
|
||||
var hasB = false
|
||||
val it1 = names.iterator()
|
||||
while (it1.hasNext()) {
|
||||
val n = it1.next()
|
||||
if (n.endsWith("one.txt")) hasOne = true
|
||||
if (n.endsWith("/b") || n.endsWith("\\b") || n.endsWith("b")) hasB = true
|
||||
}
|
||||
assertEquals(true, hasOne)
|
||||
assertEquals(true, hasB)
|
||||
|
||||
val matches = root.glob("**/*.txt").toList()
|
||||
val mnames = matches.map { it.toString() }
|
||||
var hasOneTxt = false
|
||||
var hasTwoTxt = false
|
||||
val it2 = mnames.iterator()
|
||||
while (it2.hasNext()) {
|
||||
val n = it2.next()
|
||||
if (n.endsWith("one.txt")) hasOneTxt = true
|
||||
if (n.endsWith("two.txt")) hasTwoTxt = true
|
||||
}
|
||||
assertEquals(true, hasOneTxt)
|
||||
assertEquals(true, hasTwoTxt)
|
||||
|
||||
val f = Path("${dir.resolve("a/one.txt")}")
|
||||
val m = f.metadata()
|
||||
assertEquals(true, m["isFile"])
|
||||
""".trimIndent()
|
||||
)
|
||||
} finally {
|
||||
dir.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun streamingUtf8ChunksAndLinesScript() {
|
||||
runBlocking {
|
||||
val scope = newScope()
|
||||
installModule(scope)
|
||||
|
||||
val dir = createTempDirectory("lyngio_script_stream_")
|
||||
try {
|
||||
val text = buildString {
|
||||
repeat(5000) { append("строка-").append(it).append('\n') }
|
||||
}
|
||||
val tf = dir.resolve("big.txt"); tf.writeText(text)
|
||||
|
||||
scope.eval(
|
||||
"""
|
||||
import lyng.io.fs
|
||||
val p = Path("${tf}")
|
||||
var total = 0
|
||||
val it = p.readUtf8Chunks(4096)
|
||||
val iter = it.iterator()
|
||||
while (iter.hasNext()) {
|
||||
val chunk = iter.next()
|
||||
total = total + chunk.size()
|
||||
}
|
||||
assertEquals(${text.length}, total)
|
||||
|
||||
var n = 0
|
||||
var first = ""
|
||||
var last = ""
|
||||
val lit = p.lines()
|
||||
val liter = lit.iterator()
|
||||
while (liter.hasNext()) {
|
||||
val ln = liter.next()
|
||||
if (n == 0) first = ln
|
||||
last = ln
|
||||
n = n + 1
|
||||
}
|
||||
assertEquals(5000, n)
|
||||
assertEquals("строка-0", first)
|
||||
assertEquals("строка-4999", last)
|
||||
""".trimIndent()
|
||||
)
|
||||
} finally {
|
||||
dir.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun denyingPolicyMappingScript() {
|
||||
runBlocking {
|
||||
val denyWrites = object : FsAccessPolicy {
|
||||
override suspend fun check(op: AccessOp, ctx: AccessContext): AccessDecision = when (op) {
|
||||
is AccessOp.OpenRead, is AccessOp.ListDir -> AccessDecision(Decision.Allow)
|
||||
else -> AccessDecision(Decision.Deny, reason = "denied by test policy")
|
||||
}
|
||||
}
|
||||
|
||||
val prep = newScope()
|
||||
installModule(prep)
|
||||
val dir = createTempDirectory("lyngio_script_deny_")
|
||||
try {
|
||||
val file = dir.resolve("ro.txt"); file.writeText("ro")
|
||||
|
||||
val scope = newScope()
|
||||
createFs(denyWrites, scope)
|
||||
scope.eval("import lyng.io.fs")
|
||||
|
||||
// reading should succeed
|
||||
scope.eval(
|
||||
"""
|
||||
import lyng.io.fs
|
||||
val p = Path("${file}")
|
||||
assertEquals("ro", p.readUtf8())
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
// writing should throw ExecutionError(ObjIllegalOperationException)
|
||||
val err = assertFailsWith<ExecutionError> {
|
||||
scope.eval(
|
||||
"""
|
||||
import lyng.io.fs
|
||||
// don't redeclare `p` to avoid same-scope conflicts across eval calls
|
||||
Path("${file}").writeUtf8("x")
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
assertTrue((err as ExecutionError).errorObject is ObjIllegalOperationException)
|
||||
} finally {
|
||||
dir.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* JVM tests for lyng.io.fs module bindings.
|
||||
*/
|
||||
package net.sergeych.lyngio.fs
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.ExecutionError
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.io.fs.createFs
|
||||
import net.sergeych.lyng.obj.*
|
||||
import net.sergeych.lyngio.fs.security.*
|
||||
import kotlin.io.path.*
|
||||
import kotlin.test.*
|
||||
|
||||
class FsModuleJvmTest {
|
||||
|
||||
private fun newScope(): Scope = Scope.new()
|
||||
|
||||
private suspend fun importFs(scope: Scope) {
|
||||
// Ensure module is installed and imported into the scope
|
||||
val installed = createFs(PermitAllAccessPolicy, scope)
|
||||
// ok to be false if already installed for this manager, but in tests we create a fresh scope
|
||||
scope.eval("import lyng.io.fs")
|
||||
}
|
||||
|
||||
private suspend fun pathObj(scope: Scope, path: String): Obj {
|
||||
val pathClass = scope.get("Path")!!.value as ObjClass
|
||||
return pathClass.callWithArgs(scope, ObjString(path))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun installerIdempotence() = runBlocking {
|
||||
val scope = newScope()
|
||||
val m = scope.importManager
|
||||
assertTrue(createFs(PermitAllAccessPolicy, m))
|
||||
assertFalse(createFs(PermitAllAccessPolicy, m))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun basicReadWriteUtf8AndExists() = runBlocking {
|
||||
val scope = newScope()
|
||||
importFs(scope)
|
||||
|
||||
val tmpDir = kotlin.io.path.createTempDirectory("lyngio_test_")
|
||||
try {
|
||||
val file = tmpDir.resolve("hello.txt")
|
||||
val p = pathObj(scope, file.toString())
|
||||
// Initially doesn't exist
|
||||
assertFalse(p.invokeInstanceMethod(scope, "exists").toBool())
|
||||
// Write and read
|
||||
p.invokeInstanceMethod(scope, "writeUtf8", ObjString("Hello Lyng!"))
|
||||
assertTrue(p.invokeInstanceMethod(scope, "exists").toBool())
|
||||
val read = p.invokeInstanceMethod(scope, "readUtf8") as ObjString
|
||||
assertEquals("Hello Lyng!", read.value)
|
||||
} finally {
|
||||
tmpDir.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bytesReadWriteAndMetadata() = runBlocking {
|
||||
val scope = newScope()
|
||||
importFs(scope)
|
||||
|
||||
val tmpDir = kotlin.io.path.createTempDirectory("lyngio_test_bytes_")
|
||||
try {
|
||||
val file = tmpDir.resolve("data.bin")
|
||||
val dirPath = pathObj(scope, tmpDir.toString())
|
||||
val p = pathObj(scope, file.toString())
|
||||
|
||||
// mkdirs on parent dir is no-op but should succeed
|
||||
dirPath.invokeInstanceMethod(scope, "mkdirs")
|
||||
|
||||
val bytes = (0 until 256).map { it.toByte() }.toByteArray().asUByteArray()
|
||||
p.invokeInstanceMethod(scope, "writeBytes", ObjBuffer(bytes))
|
||||
|
||||
val readBuf = p.invokeInstanceMethod(scope, "readBytes") as ObjBuffer
|
||||
assertContentEquals(bytes.asByteArray(), readBuf.byteArray.asByteArray())
|
||||
|
||||
val meta = p.invokeInstanceMethod(scope, "metadata") as ObjMap
|
||||
assertEquals(ObjBool(true), meta.map[ObjString("isFile")])
|
||||
assertEquals(ObjBool(false), meta.map[ObjString("isDirectory")])
|
||||
} finally {
|
||||
tmpDir.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun listAndGlob() = runBlocking {
|
||||
val scope = newScope()
|
||||
importFs(scope)
|
||||
|
||||
val tmpDir = kotlin.io.path.createTempDirectory("lyngio_test_glob_")
|
||||
try {
|
||||
// prepare dirs/files
|
||||
val sub1 = tmpDir.resolve("a"); sub1.createDirectories()
|
||||
val sub2 = tmpDir.resolve("a/b"); sub2.createDirectories()
|
||||
val f1 = tmpDir.resolve("a/one.txt"); f1.writeText("1")
|
||||
val f2 = tmpDir.resolve("a/b/two.txt"); f2.writeText("2")
|
||||
val f3 = tmpDir.resolve("a/b/three.md"); f3.writeText("3")
|
||||
|
||||
val root = pathObj(scope, sub1.toString())
|
||||
|
||||
// list should contain children: one.txt and b
|
||||
val list = root.invokeInstanceMethod(scope, "list") as ObjList
|
||||
val names = list.list.map { it.toString() }
|
||||
assertTrue(names.any { it.endsWith("one.txt") })
|
||||
assertTrue(names.any { it.endsWith("b") })
|
||||
|
||||
// glob **/*.txt
|
||||
val matches = root.invokeInstanceMethod(scope, "glob", ObjString("**/*.txt")) as ObjList
|
||||
val mnames = matches.list.map { it.toString() }
|
||||
assertTrue(mnames.any { it.endsWith("one.txt") })
|
||||
assertTrue(mnames.any { it.endsWith("two.txt") })
|
||||
assertFalse(mnames.any { it.endsWith("three.md") })
|
||||
} finally {
|
||||
tmpDir.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyMoveDelete() = runBlocking {
|
||||
val scope = newScope()
|
||||
importFs(scope)
|
||||
|
||||
val tmpDir = kotlin.io.path.createTempDirectory("lyngio_test_cmd_")
|
||||
try {
|
||||
val f1 = tmpDir.resolve("src.txt"); f1.writeText("abc")
|
||||
val f2 = tmpDir.resolve("dst.txt")
|
||||
val p1 = pathObj(scope, f1.toString())
|
||||
val p2 = pathObj(scope, f2.toString())
|
||||
|
||||
// copy
|
||||
p1.invokeInstanceMethod(scope, "copy", p2)
|
||||
assertTrue(f2.exists())
|
||||
assertEquals("abc", f2.readText())
|
||||
|
||||
// move with overwrite
|
||||
val f3 = tmpDir.resolve("moved.txt"); f3.writeText("x")
|
||||
val p3 = pathObj(scope, f3.toString())
|
||||
p2.invokeInstanceMethod(scope, "move", p3, ObjBool(true))
|
||||
assertFalse(f2.exists())
|
||||
assertEquals("abc", f3.readText())
|
||||
|
||||
// delete
|
||||
p3.invokeInstanceMethod(scope, "delete")
|
||||
assertFalse(f3.exists())
|
||||
} finally {
|
||||
tmpDir.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun streamingReaders() = runBlocking {
|
||||
val scope = newScope()
|
||||
importFs(scope)
|
||||
|
||||
val tmpDir = kotlin.io.path.createTempDirectory("lyngio_test_stream_")
|
||||
try {
|
||||
val large = ByteArray(2 * 1024 * 1024) { (it % 251).toByte() }
|
||||
val file = tmpDir.resolve("big.bin"); file.writeBytes(large)
|
||||
val p = pathObj(scope, file.toString())
|
||||
|
||||
// readChunks
|
||||
val it = p.invokeInstanceMethod(scope, "readChunks", ObjInt(131072)) // 128KB
|
||||
var total = 0
|
||||
val iter = it.invokeInstanceMethod(scope, "iterator")
|
||||
while (iter.invokeInstanceMethod(scope, "hasNext").toBool()) {
|
||||
val chunk = iter.invokeInstanceMethod(scope, "next") as ObjBuffer
|
||||
total += chunk.byteArray.size
|
||||
if (total >= 512 * 1024) {
|
||||
// test cancellation early
|
||||
iter.invokeInstanceMethod(scope, "cancelIteration")
|
||||
break
|
||||
}
|
||||
}
|
||||
assertTrue(total >= 512 * 1024)
|
||||
|
||||
// readUtf8Chunks + lines
|
||||
val text = buildString {
|
||||
repeat(10000) { append("line-").append(it).append('\n') }
|
||||
}
|
||||
val tf = tmpDir.resolve("big.txt"); tf.writeText(text)
|
||||
val tp = pathObj(scope, tf.toString())
|
||||
|
||||
val lit = tp.invokeInstanceMethod(scope, "lines")
|
||||
val lines = mutableListOf<String>()
|
||||
val lIter = lit.invokeInstanceMethod(scope, "iterator")
|
||||
while (lIter.invokeInstanceMethod(scope, "hasNext").toBool()) {
|
||||
val s = lIter.invokeInstanceMethod(scope, "next") as ObjString
|
||||
lines += s.value
|
||||
}
|
||||
assertEquals(10000, lines.size)
|
||||
assertEquals("line-0", lines.first())
|
||||
assertEquals("line-9999", lines.last())
|
||||
} finally {
|
||||
tmpDir.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun forbiddenOperationsRaiseLyngErrors() = runBlocking {
|
||||
// Policy that denies everything except read and list
|
||||
val denyWrites = object : FsAccessPolicy {
|
||||
override suspend fun check(op: AccessOp, ctx: AccessContext): AccessDecision = when (op) {
|
||||
is AccessOp.OpenRead, is AccessOp.ListDir -> AccessDecision(Decision.Allow)
|
||||
else -> AccessDecision(Decision.Deny, reason = "denied by test policy")
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare a file with PermitAll first (separate manager/scope)
|
||||
val prepScope = newScope()
|
||||
importFs(prepScope)
|
||||
val tmpDir = kotlin.io.path.createTempDirectory("lyngio_test_deny_")
|
||||
try {
|
||||
val file = tmpDir.resolve("ro.txt"); file.writeText("ro")
|
||||
|
||||
// New scope with denying policy
|
||||
val scope = newScope()
|
||||
assertTrue(net.sergeych.lyng.io.fs.createFs(denyWrites, scope))
|
||||
scope.eval("import lyng.io.fs")
|
||||
|
||||
val p = pathObj(scope, file.toString())
|
||||
// Read should work
|
||||
val text = p.invokeInstanceMethod(scope, "readUtf8") as ObjString
|
||||
assertEquals("ro", text.value)
|
||||
|
||||
// Write should throw ExecutionError with ObjIllegalOperationException
|
||||
val err = assertFailsWith<ExecutionError> {
|
||||
p.invokeInstanceMethod(scope, "writeUtf8", ObjString("x"))
|
||||
}
|
||||
assertTrue(err.errorObject is ObjIllegalOperationException)
|
||||
} finally {
|
||||
tmpDir.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user