1203 lines
38 KiB
Kotlin

/*
* 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.
*
*/
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.EvalSession
import net.sergeych.lyng.ModuleScope
import net.sergeych.lyng.Script
import net.sergeych.lyng.bridge.bindObject
import net.sergeych.lyng.bridge.data
import net.sergeych.lyng.eval
import net.sergeych.lyng.obj.ObjBool
import net.sergeych.lyng.obj.ObjBuffer
import net.sergeych.lyng.obj.ObjInt
import net.sergeych.lyng.obj.ObjInstance
import net.sergeych.lyng.obj.ObjList
import net.sergeych.lyng.obj.ObjNull
import net.sergeych.lyng.obj.ObjString
import net.sergeych.lyng.obj.ObjVoid
import net.sergeych.lyng.toSource
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
class OOTest {
@Test
fun testClassProps() = runTest {
eval(
"""
import lyng.time
class Point(val x, val y) {
static val origin = Point(0,0)
static var center = null
}
Point.center = Point.origin
assertEquals(Point(0,0), Point.origin)
assertEquals(Point(0,0), Point.center)
Point.center = Point(1,2)
assertEquals(Point(0,0), Point.origin)
assertEquals(Point(1,2), Point.center)
""".trimIndent()
)
}
@Test
fun testClassMethods() = runTest {
eval(
"""
import lyng.time
var pointData = null
class Point(val x, val y) {
static fun getData() = pointData
static fun setData(value) {
pointData = value + "!"
}
}
assertEquals(Point(0,0), Point(0,0) )
assertEquals(null, Point.getData() )
Point.setData("foo")
assertEquals( "foo!", Point.getData() )
""".trimIndent()
)
}
@Test
fun testDynamicGet() = runTest {
val ms = Script.newScope()
ms.eval(
"""
val accessor: String = dynamic {
get { name ->
if( name == "foo" ) "bar" else null
}
}
println("-- " + accessor.foo)
assertEquals("bar", accessor.foo)
assertEquals(null, accessor.bar)
""".trimIndent()
)
ms.eval("""
assertEquals("bar", accessor.foo)
assertEquals(null, accessor.bad)
""".trimIndent())
}
@Test
fun testDelegateSet() = runTest {
eval(
"""
var setValueForBar = null
val accessor: Delegate = dynamic {
get { name ->
when(name) {
"foo" -> "bar"
"bar" -> setValueForBar
else -> null
}
}
set { name, value ->
if( name == "bar" )
setValueForBar = value
else throw IllegalAssignmentException("Can't assign "+name)
}
}
assertEquals("bar", accessor.foo)
assertEquals(null, accessor.bar)
accessor.bar = "buzz"
assertEquals("buzz", accessor.bar)
assertThrows {
accessor.bad = "!23"
}
""".trimIndent()
)
}
@Test
fun testDynamicIndexAccess() = runTest {
eval(
"""
val store = Map()
val accessor: Delegate = dynamic {
get { name ->
store[name]
}
set { name, value ->
store[name] = value
}
}
assertEquals(null, accessor["foo"])
assertEquals(null, accessor.foo)
accessor["foo"] = "bar"
assertEquals("bar", accessor["foo"])
assertEquals("bar", accessor.foo)
""".trimIndent()
)
}
@Test
fun testMultilineConstructor() = runTest {
eval(
"""
class Point(
x,
y
)
assertEquals(Point(1,2), Point(1,2) )
""".trimIndent()
)
}
@Test
fun testDynamicClass() = runTest {
eval(
"""
fun getContract(contractName): Delegate {
dynamic {
get { name ->
println("Call: %s.%s"(contractName,name))
}
}
}
getContract("foo").bar
"""
)
}
@Test
fun testDynamicClassReturn2() = runTest {
// todo: should work without extra parenthesis
// see below
eval(
"""
fun getContract(contractName): Delegate {
println("1")
dynamic {
get { name ->
println("innrer %s.%s"(contractName,name))
{ args... ->
if( name == "bar" ) (args as List).sum() else null
}
}
}
}
val cc: Delegate = dynamic {
get { name ->
println("Call cc %s"(name))
getContract(name)
}
}
val x = cc.foo.bar
println(x)
x(1,2,3)
assertEquals(6, x(1,2,3))
// v HERE v
assertEquals(15, cc.foo.bar(10,2,3))
"""
)
}
@Test
fun testClassInitialization() = runTest {
eval(
"""
var countInstances = 0
class Point(val x: Int, val y: Int) {
println("Class initializer is called 1")
var magnitude
/*
init {} section optionally provide initialization code that is called on each instance creation.
it should have the same access to this.* and constructor parameters as any other member
function.
*/
init {
countInstances++
magnitude = Math.sqrt(x*x + y*y)
}
}
val p = Point(1, 2)
assertEquals(1, countInstances)
assertEquals(p, Point(1,2) )
assertEquals(2, countInstances)
""".trimIndent()
)
}
@Test
fun testMIInitialization() = runTest {
eval(
"""
var order = []
class A {
init { order.add("A") }
}
class B : A {
init { order.add("B") }
}
class C {
init { order.add("C") }
}
class D : B, C {
init { order.add("D") }
}
D()
assertEquals(["A", "B", "C", "D"], order)
"""
)
}
@Test
fun testMIDiamondInitialization() = runTest {
eval(
"""
var order = []
class A {
init { order.add("A") }
}
class B : A {
init { order.add("B") }
}
class C : A {
init { order.add("C") }
}
class D : B, C {
init { order.add("D") }
}
D()
assertEquals(["A", "B", "C", "D"], order)
"""
)
}
@Test
fun testInitBlockInDeserialization() = runTest {
eval(
"""
import lyng.serialization
var count = 0
class A {
init { count++ }
}
val a1 = A()
val coded = Lynon.encode(a1)
val a2 = Lynon.decode(coded)
assertEquals(2, count)
"""
)
}
@Test
fun testDefaultCompare() = runTest {
eval(
"""
class Point(val x: Int, val y: Int)
assertEquals(Point(1,2), Point(1,2) )
assert( Point(1,2) != Point(2,1) )
assert( Point(1,2) == Point(1,2) )
""".trimIndent()
)
}
@Test
fun testConstructorCallsWithNamedParams() = runTest {
val scope = Script.newScope()
val list = scope.eval(
"""
import lyng.time
class BarRequest(
val id,
val vaultId, val userAddress, val isDepositRequest, val grossWeight, val fineness, val notes="",
val createdAt = Instant.now().truncateToSecond(),
val updatedAt = Instant.now().truncateToSecond()
) {
// unrelated for comparison
static val stateNames = [1, 2, 3]
val cell = cached { id }
}
assertEquals( 5,5.toInt())
val b1 = BarRequest(1, "v1", "u1", true, 1000, 999)
val b2 = BarRequest(1, "v1", "u1", true, 1000, 999, createdAt: b1.createdAt, updatedAt: b1.updatedAt)
assertEquals(b1, b2)
assertEquals( 0, b1 <=> b2)
[b1, b2]
""".trimIndent()
) as ObjList
val b1 = list.list[0] as ObjInstance
val b2 = list.list[1] as ObjInstance
assertEquals(0, b1.compareTo(scope, b2))
}
@Test
fun testPropAsExtension() = runTest {
val scope = Script.newScope()
scope.eval(
"""
class A(val x) {
private val privateVal = 100
val p1 get() = this.x + 1
}
assertEquals(2, A(1).p1)
fun A.f() = this.x + 5
assertEquals(7, __ext__A__f(A(2)))
// The same, we should be able to add member values to a class;
// notice it should access to the class public instance members,
// somewhat like it is declared in the class body
val A.simple get() = this.x + 3
assertEquals(5, __ext_get__A__simple(A(2)))
// it should also work with properties:
val A.p10 get() = this.x * 10
assertEquals(20, __ext_get__A__p10(A(2)))
""".trimIndent()
)
// important is that such extensions should not be able to access private members
// and thus remove privateness:
assertFails {
scope.eval("val A.exportPrivateVal = privateVal; __ext_get__A__exportPrivateVal(A(1))")
}
assertFails {
scope.eval("val A.exportPrivateValProp get() = privateVal; __ext_get__A__exportPrivateValProp(A(1))")
}
}
@Test
fun testObjectSingletonSupportsExtensions() = runTest {
val scope = Script.newScope()
scope.eval(
"""
object X {
fun base() = "base"
}
fun X.decorate<T>(value: T): String {
this.base() + ":" + value.toString()
}
val X.tag get() = this.base() + ":tag"
assertEquals("base", X.base())
assertEquals("base:42", X.decorate(42))
assertEquals("base:ok", X.decorate("ok"))
assertEquals("base:tag", X.tag)
// Wrapper names should be generated for singleton-object receivers too.
assertEquals("base:17", __ext__X__decorate(X, 17))
assertEquals("base:tag", __ext_get__X__tag(X))
""".trimIndent()
)
}
@Test
fun testExtensionsAreScopeIsolated() = runTest {
val scope1 = Script.newScope()
scope1.eval(
"""
fun String.totalDigits() =
// notice using `this`:
(this.characters as List).filter{ (it as Char).isDigit() }.size()
assertEquals(2, __ext__String__totalDigits("answer is 42"))
"""
)
val scope2 = Script.newScope()
assertFails {
// in scope2 we didn't override `totalDigits` extension:
scope2.eval("""__ext__String__totalDigits("answer is 42")""".trimIndent())
}
}
@Test
fun testCacheInClass() = runTest {
eval(
"""
class T(salt) {
private var c
init {
println("create cached with "+salt)
c = cached { salt + "." }
}
fun getResult() = c()
}
val t1 = T("foo")
val t2 = T("bar")
assertEquals("bar.", t2.getResult())
assertEquals("foo.", t1.getResult())
""".trimIndent()
)
}
@Test
fun testLateInitValsInClasses() = runTest {
assertFails {
eval(
"""
class T {
val x
}
"""
)
}
assertFails {
eval("val String.late")
}
eval(
"""
// but we can "late-init" them in init block:
class OK {
val x
init {
x = "foo"
}
}
val ok = OK()
assertEquals("foo", ok.x)
// they can't be reassigned:
assertThrows(IllegalAssignmentException) {
ok.x = "bar"
}
// To test access before init, we need a trick:
class AccessBefore {
val x
fun readX() = x
init {
assertEquals(x, Unset)
// if we call readX() here, x is Unset.
// Just reading it is fine, but using it should throw:
assertThrows(UnsetException) { readX() + 1 }
x = 42
}
}
AccessBefore()
""".trimIndent()
)
}
@Test
fun testPrivateSet() = runTest {
eval(
"""
class A {
var y = 100
private set
fun setValue(newValue) { y = newValue }
}
assertEquals(100, A().y)
assertThrows(IllegalAccessException) { A().y = 200 }
val a = A()
a.setValue(200)
assertEquals(200, a.y)
class B {
var y = 10
protected set
}
class C : B {
fun setBValue(v) { y = v }
}
val c = C()
assertEquals(10, c.y)
assertThrows(IllegalAccessException) { c.y = 20 }
c.setBValue(30)
assertEquals(30, c.y)
class D {
private var _y = 0
var y
get() = _y
private set(v) { _y = v }
fun setY(v) { y = v }
}
val d = D()
assertEquals(0, d.y)
assertThrows(IllegalAccessException) { d.y = 10 }
d.setY(20)
assertEquals(20, d.y)
"""
)
}
@Test
fun testValPrivateSetError() = runTest {
assertFails {
eval("class E { val x = 1 private set }")
}
}
@Test
fun testAbstractClassesAndOverridingProposal() = runTest {
val scope = Script.newScope()
/*
Abstract class is a sort of interface on steroidsL it is a class some members/methods of which
are required to be implemented by heirs. Still it is a regular class in all other respects.
Just can't be instantiated
*/
scope.eval(
"""
// abstract modifier is required. It can have a constructor, or be without it:
abstract class A(someParam=1) {
// if the method is marked as abstract, it has no body:
abstract fun foo(): Int
// abstract members have no initializer:
abstract fun getBar(): Int
}
// can't create instance of the abstract class:
assertThrows { A() }
""".trimIndent()
)
// create abstract method with body or val/var with initializer is an error:
assertFails { scope.eval("abstract class B { abstract fun foo() = 1 }") }
assertFails { scope.eval("abstract class C { abstract val bar = 1 }") }
// inheriting an abstract class without implementing all of it abstract members and methods
// is not allowed:
assertFails { scope.eval("class D : A(1) { override fun foo() = 10 }") }
// but it is allowed to inherit in another abstract class:
scope.eval("abstract class E : A(1) { override fun foo() = 10 }")
// implementing all abstracts let us have regular class:
scope.eval(
"""
class F : E() { override fun getBar() = 11 }
assertEquals(10, F().foo())
assertEquals(11, F().getBar())
""".trimIndent()
)
// MI-based abstract implementation is deferred.
}
@Test
fun testAbstractAndOverrideEdgeCases() = runTest {
val scope = Script.newScope()
// 1. abstract private is an error:
assertFails { scope.eval("abstract class Err { abstract private fun foo() }") }
assertFails { scope.eval("abstract class Err { abstract private val x }") }
// 2. private member in parent is not visible for overriding:
scope.eval(
"""
class Base {
private fun secret() = 1
fun callSecret() = secret()
}
class Derived : Base() {
// New method name avoids private override ambiguity
fun secret2() = 2
}
val d = Derived()
assertEquals(2, d.secret2())
assertEquals(1, d.callSecret())
""".trimIndent()
)
// Using override keyword when there is only a private member in parent is an error:
assertFails { scope.eval("class D2 : Base() { override fun secret() = 3 }") }
// 3. interface can have state (constructor, fields, init):
scope.eval(
"""
class I(val x) {
var y = x * 2
val z
init {
z = y + 1
}
fun foo() = x + y + z
}
class Impl : I(10)
val impl = Impl()
assertEquals(10, impl.x)
assertEquals(20, impl.y)
assertEquals(21, impl.z)
assertEquals(51, impl.foo())
""".trimIndent()
)
// 4. closed members cannot be overridden:
scope.eval(
"""
class G {
closed fun locked() = "locked"
closed val permanent = 42
}
""".trimIndent()
)
assertFails { scope.eval("class H : G() { override fun locked() = \"free\" }") }
assertFails { scope.eval("class H : G() { override val permanent = 0 }") }
// Even without override keyword, it should fail if it's closed:
assertFails { scope.eval("class H : G() { fun locked() = \"free\" }") }
// 5. Visibility widening is allowed, narrowing is forbidden:
scope.eval(
"""
class BaseVis {
protected fun prot() = 1
}
class Widened : BaseVis() {
override fun prot() = 2 // Widened to public (default)
}
assertEquals(2, Widened().prot())
class BasePub {
fun pub() = 1
}
""".trimIndent()
)
// Narrowing:
assertFails { scope.eval("class Narrowed : BasePub() { override protected fun pub() = 2 }") }
assertFails { scope.eval("class Narrowed : BasePub() { override private fun pub() = 2 }") }
}
@Test
fun testInterfaceImplementationByParts() = runTest {
val scope = Script.newScope()
scope.eval(
"""
// Interface with state (id) and abstract requirements
interface Character {
abstract val id
var health
var mana
abstract fun getName()
fun isAlive() = health > 0
fun status() = getName() + " (#" + id + "): " + health + " HP, " + mana + " MP"
// name is also abstractly required by the status method,
// even if not explicitly marked 'abstract val' here,
// it will be looked up in MRO
}
class Warrior(id0, health0, mana0) : Character {
override val id = id0
override var health = health0
override var mana = mana0
override fun getName() = "Hero"
}
val w = Warrior(1, 100, 50)
assertEquals(100, w.health)
assertEquals(50, w.mana)
assertEquals(1, w.id)
assert(w.isAlive())
assertEquals("Hero (#1): 100 HP, 50 MP", w.status())
w.health = 0
assert(!w.isAlive())
""".trimIndent()
)
}
@Test
fun testBasicObjectExpression() = runTest {
eval("""
val x = object { val y = 1 }
assertEquals(1, x.y)
class Base(v) {
val value = v
fun squares() = value * value
}
val y = object : Base(2) {
override val value = 5
}
assertEquals(25, y.squares())
""".trimIndent())
}
@Test
fun testArgsPriority() = runTest {
eval("""
class A(id) {
var stored = null
// Arguments should have priority on
// instance fields
fun setStored(id) { stored = id }
}
val a = A(1)
assertEquals(1, a.id)
assertEquals(null, a.stored)
// Check that arguments of the call have the priority:
a.setStored(2)
assertEquals(1, a.id)
assertEquals(2, a.stored)
""".trimIndent())
}
/**
* Demonstrates that function parameters are shadowed by class methods of the same name
* when accessed within a block, but not in a single expression.
*/
@Test
fun testParameterShadowingConflict() = runTest {
val scope = Script.newScope()
val result = scope.eval("""
class Tester() {
fun id() { "method" }
// This correctly returns "success"
fun checkOk(id) = id
// This incorrectly returns the 'id' method (a Callable) instead of "success"
fun checkFail(id) {
id
}
}
val t = Tester()
if (t.checkOk("success") != "success") throw "checkOk failed"
t.checkFail("success")
""".trimIndent().toSource("repro"))
assertEquals("success", result.toString(), "Parameter 'id' should shadow method 'id' in block")
}
@Test
fun testOverrideVisibilityRules1() = runTest {
val scope = Script.newScope()
scope.eval("""
interface Base {
abstract protected fun foo()
fun bar() {
// it must see foo() as it is protected and
// is declared here (even as abstract):
foo()
}
}
class Derived : Base {
protected val suffix = "!"
private fun fooPrivateImpl() = "bar"
override protected fun foo() {
// it should access own private and all protected memberes here:
fooPrivateImpl() + suffix
}
}
class Derived2: Base {
private var value = 42
private fun fooPrivateImpl() = value
override protected fun foo() {
fooPrivateImpl()
value++
}
}
val d: Derived = Derived()
assertEquals("bar!", d.bar())
val d2: Derived2 = Derived2()
assertEquals(42, d2.bar())
assertEquals(43, d2.bar())
""".trimIndent())
scope.createChildScope().eval("""
val d: Derived = Derived()
assertEquals("bar!", d.bar())
val d2: Derived2 = Derived2()
assertEquals(42, d2.bar())
""".trimIndent())
}
@Test
fun testOverrideVisibilityRules2() = runTest {
val scope = Script.newScope()
scope.eval("""
interface Base {
abstract fun foo()
fun bar() {
// it must see foo() as it is protected and
// is declared here (even as abstract):
foo()
}
}
class Derived : Base {
protected val suffix = "!"
private fun fooPrivateImpl() = "bar"
override fun foo() {
// it should access own private and all protected memberes here:
fooPrivateImpl() + suffix
}
}
class Derived2: Base {
private var value = 42
private fun fooPrivateImpl() = value
override fun foo() {
fooPrivateImpl()
value++
}
}
val d: Derived = Derived()
assertEquals("bar!", (d as Derived).bar())
class Holder {
val d2: Derived2 = Derived2()
fun callBar() = (d2 as Derived2).bar()
}
val holder: Holder = Holder()
assertEquals(42, (holder as Holder).callBar())
assertEquals(43, (holder as Holder).callBar())
""".trimIndent())
}
@Test
fun testToStringWithTransients() = runTest {
eval("""
class C(val amount,@Transient var transient=0) {
val l by lazy { transient + amount }
fun lock(): C {
if( transient < 10 )
return C(amount).also { it.transient = transient + 10 }
else
return this
}
}
println(C(1))
val c1: C = C(1).lock() as C
val c1b: C = c1.lock() as C
val c2: C = c1b.lock() as C
println(c1.amount)
println(c2.amount)
""".trimIndent())
}
@Test
fun testToStringWithTransient() = runTest {
eval("""
class C(val amount,@Transient var transient=0) {
val l by lazy { transient + amount }
fun lock(): C {
if( transient < 10 )
return C(amount).also { it.transient = transient + 10 }
else
return this
}
}
println(C(1))
val c1: C = C(1).lock() as C
val c1b: C = c1.lock() as C
val c2: C = c1b.lock() as C
println(c1.amount)
println(c2.amount)
""".trimIndent())
}
@Test
fun testToJsonString() = runTest {
eval("""
class T(a,b,@Transient c=0)
assertEquals("{\"a\":\"foo\",\"b\":\"bar\"}",T("foo", "bar").toJsonString())
""".trimIndent())
}
@Test
fun testAssignToUnqualifiedParams() = runTest {
eval("""
class T(x) {
fun setx(v) { x = v }
fun incr(v) { x += v }
}
val t = T(1)
t.setx(2)
assertEquals(2, t.x)
t.incr(3)
assertEquals(5, t.x)
""".trimIndent())
}
@Test
fun testDynamicToDynamic() = runTest {
val ms = Script.newScope()
ms.eval("""
class A(prefix) {
val da = dynamic {
get { name -> "a:"+prefix+":"+name }
}
}
val B: A = dynamic {
get { p -> A(p) }
}
assertEquals(A("bar").da.foo, "a:bar:foo")
assertEquals( B.buzz.da.foo, "a:buzz:foo" )
val C = dynamic {
get { p -> A(p).da }
}
assertEquals(C.buzz.foo, "a:buzz:foo")
""".trimIndent())
ms.eval("""
""")
}
@Test
fun testDynamicToDynamicFun() = runTest {
val ms = Script.newScope()
ms.eval("""
class A(prefix) {
val da = dynamic {
get { name -> { x -> "a:"+prefix+":"+name+"/"+x } }
}
}
val B: A = dynamic {
get { p -> A(p) }
}
assertEquals(A("bar").da.foo("buzz"), "a:bar:foo/buzz")
assertEquals( B.buzz.da.foo("42"), "a:buzz:foo/42" )
val C = dynamic {
get { p -> A(p).da }
}
assertEquals(C.buzz.foo("one"), "a:buzz:foo/one")
""".trimIndent())
ms.eval("""
""")
}
@Test
fun testExtendingObjectWithExternals() = runTest {
val s = EvalSession()
s.eval("""
extern object Storage {
extern val spaceUsed: Int
extern val spaceAvailable: Int
/*
Return packed binary data or null
*/
extern fun getPacked(key: String): Buffer?
/*
Upsert packed binary data
*/
extern fun putPacked(key: String,value: Buffer)
/*
Delete data.
@return true if data were actually deleted, false means
there were no data for the key.
*/
extern fun delete(key: String): Bool
}
""".trimIndent()
)
val scope = s.getScope() as ModuleScope
scope.bindObject("Storage") {
init { _ ->
data = mutableMapOf<String, ObjBuffer>()
}
addVal("spaceUsed") {
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
ObjInt(storage.values.sumOf { it.size }.toLong())
}
addVal("spaceAvailable") {
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
val capacity = 1_024
ObjInt((capacity - storage.values.sumOf { it.size }).toLong())
}
addFun("getPacked") {
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
val key = (args.list[0] as ObjString).value
storage[key] ?: ObjNull
}
addFun("putPacked") {
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
val key = (args.list[0] as ObjString).value
val value = args.list[1] as ObjBuffer
storage[key] = value
ObjVoid
}
addFun("delete") {
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
val key = (args.list[0] as ObjString).value
ObjBool(storage.remove(key) != null)
}
}
s.eval("""
import lyng.serialization
// Use names that do not collide with Obj built-ins so extension dispatch is exercised.
override fun Storage.getAt(key: String): Object? {
Storage.getPacked(key)?.let {
Lynon.decode(it.toBitInput())
}
}
override fun Storage.putAt(key: String, value: Object) {
Storage.putPacked(key, Lynon.encode(value).toBuffer())
}
assertEquals(0, Storage.spaceUsed)
assertEquals(1024, Storage.spaceAvailable)
val missing: String? = Storage["missing"]
assertEquals(null, missing)
Storage["name"] = "alice"
Storage["count"] = 42
val name: String? = Storage["name"]
val count: Int? = Storage["count"]
assertEquals("alice", name)
assertEquals(42, count)
assert(Storage.spaceUsed > 0)
assert(Storage.spaceAvailable < 1024)
val wrappedName: String? = __ext__Storage__getAt(Storage, "name")
assertEquals("alice", wrappedName)
__ext__Storage__putAt(Storage, "flag", true)
val flag: Bool? = Storage["flag"]
assertEquals(true, flag)
assert(Storage.delete("name"))
val deletedName: String? = Storage["name"]
assertEquals(null, deletedName)
assert(!Storage.delete("name"))
""".trimIndent())
}
@Test
fun testExtendingObjectWithExternals2() = runTest {
val s = EvalSession()
s.eval("""
import lyng.serialization
object Storage {
extern val spaceUsed: Int
extern val spaceAvailable: Int
/*
Return packed binary data or null
*/
extern fun getPacked(key: String): Buffer?
/*
Upsert packed binary data
*/
extern fun putPacked(key: String,value: Buffer)
/*
Delete data.
@return true if data were actually deleted, false means
there were no data for the key.
*/
extern fun delete(key: String): Bool
override fun putAt(key: String,value: Object) {
putPacked(key, Lynon.encode(value).toBuffer())
}
override fun getAt(key: String): Object? =
getPacked(key)?.let { Lynon.decode(it.toBitInput()) }
}
""".trimIndent()
)
val scope = s.getScope() as ModuleScope
scope.bindObject("Storage") {
init { _ ->
data = mutableMapOf<String, ObjBuffer>()
}
addVal("spaceUsed") {
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
ObjInt(storage.values.sumOf { it.size }.toLong())
}
addVal("spaceAvailable") {
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
val capacity = 1_024
ObjInt((capacity - storage.values.sumOf { it.size }).toLong())
}
addFun("getPacked") {
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
val key = (args.list[0] as ObjString).value
storage[key] ?: ObjNull
}
addFun("putPacked") {
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
val key = (args.list[0] as ObjString).value
val value = args.list[1] as ObjBuffer
storage[key] = value
ObjVoid
}
addFun("delete") {
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
val key = (args.list[0] as ObjString).value
ObjBool(storage.remove(key) != null)
}
}
s.eval("""
assertEquals(0, Storage.spaceUsed)
assertEquals(1024, Storage.spaceAvailable)
val missing: String? = Storage["missing"]
assertEquals(null, missing)
Storage["name"] = "alice"
Storage["count"] = 42
val name: String? = Storage["name"]
val count: Int? = Storage["count"]
assertEquals("alice", name)
assertEquals(42, count)
assert(Storage.spaceUsed > 0)
assert(Storage.spaceAvailable < 1024)
val wrappedName: String? = Storage.getAt("name")
assertEquals("alice", wrappedName)
Storage.putAt("flag", true)
val flag: Bool? = Storage["flag"]
assertEquals(true, flag)
assert(Storage.delete("name"))
val deletedName: String? = Storage["name"]
assertEquals(null, deletedName)
assert(!Storage.delete("name"))
""".trimIndent())
}
}