714 lines
21 KiB
Kotlin
714 lines
21 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.Script
|
|
import net.sergeych.lyng.eval
|
|
import net.sergeych.lyng.obj.ObjInstance
|
|
import net.sergeych.lyng.obj.ObjList
|
|
import kotlin.test.Test
|
|
import kotlin.test.assertEquals
|
|
import kotlin.test.assertFails
|
|
|
|
class OOTest {
|
|
@Test
|
|
fun testClassProps() = runTest {
|
|
eval(
|
|
"""
|
|
import lyng.time
|
|
|
|
class Point(x,y) {
|
|
static val origin = Point(0,0)
|
|
static var center = 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
|
|
|
|
class Point(x,y) {
|
|
private static var data = null
|
|
|
|
static fun getData() { data }
|
|
static fun setData(value) {
|
|
data = value
|
|
callFrom()
|
|
}
|
|
static fun callFrom() {
|
|
data = data + "!"
|
|
}
|
|
}
|
|
assertEquals(Point(0,0), Point(0,0) )
|
|
assertEquals(null, Point.getData() )
|
|
Point.setData("foo")
|
|
assertEquals( "foo!", Point.getData() )
|
|
""".trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun testDynamicGet() = runTest {
|
|
eval(
|
|
"""
|
|
val accessor = dynamic {
|
|
get { name ->
|
|
if( name == "foo" ) "bar" else null
|
|
}
|
|
}
|
|
|
|
println("-- " + accessor.foo)
|
|
assertEquals("bar", accessor.foo)
|
|
assertEquals(null, accessor.bar)
|
|
|
|
""".trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun testDelegateSet() = runTest {
|
|
eval(
|
|
"""
|
|
var setValueForBar = null
|
|
val accessor = 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 = 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) {
|
|
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) {
|
|
println("1")
|
|
dynamic {
|
|
get { name ->
|
|
println("innrer %s.%s"(contractName,name))
|
|
{ args... ->
|
|
if( name == "bar" ) args.sum() else null
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
val cc = 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(
|
|
id,
|
|
vaultId, userAddress, isDepositRequest, grossWeight, fineness, notes="",
|
|
createdAt = Instant.now().truncateToSecond(),
|
|
updatedAt = Instant.now().truncateToSecond()
|
|
) {
|
|
// unrelated for comparison
|
|
static val stateNames = [1, 2, 3]
|
|
|
|
val cell = cached { Cell[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(x) {
|
|
private val privateVal = 100
|
|
val p1 get() = x + 1
|
|
}
|
|
assertEquals(2, A(1).p1)
|
|
|
|
fun A.f() = x + 5
|
|
assertEquals(7, A(2).f())
|
|
|
|
// 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 = x + 3
|
|
|
|
assertEquals(5, A(2).simple)
|
|
|
|
// it should also work with properties:
|
|
val A.p10 get() = x * 10
|
|
assertEquals(20, A(2).p10)
|
|
""".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; A(1).exportPrivateVal")
|
|
}
|
|
assertFails {
|
|
scope.eval("val A.exportPrivateValProp get() = privateVal; A(1).exportPrivateValProp")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun testExtensionsAreScopeIsolated() = runTest {
|
|
val scope1 = Script.newScope()
|
|
scope1.eval(
|
|
"""
|
|
val String.totalDigits get() {
|
|
// notice using `this`:
|
|
this.characters.filter{ it.isDigit() }.size()
|
|
}
|
|
assertEquals(2, "answer is 42".totalDigits)
|
|
"""
|
|
)
|
|
val scope2 = Script.newScope()
|
|
scope2.eval(
|
|
"""
|
|
// in scope2 we didn't override `totalDigits` extension:
|
|
assertThrows { "answer is 42".totalDigits }
|
|
""".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(AccessException) { A().y = 200 }
|
|
val a = A()
|
|
a.setValue(200)
|
|
assertEquals(200, a.y)
|
|
|
|
class B(initial) {
|
|
var y = initial
|
|
protected set
|
|
}
|
|
class C(initial) : B(initial) {
|
|
fun setBValue(v) { y = v }
|
|
}
|
|
val c = C(10)
|
|
assertEquals(10, c.y)
|
|
assertThrows(AccessException) { 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(AccessException) { 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 var/var have no initializer:
|
|
abstract var bar
|
|
}
|
|
// 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 val bar = 11 }
|
|
assertEquals(10, F().foo())
|
|
assertEquals(11, F().bar)
|
|
""".trimIndent()
|
|
)
|
|
|
|
// Another possibility to override symbol is multiple inheritance: the parent that
|
|
// follows the abstract class in MI chain can override the abstract symbol:
|
|
scope.eval(
|
|
"""
|
|
// This implementor know nothing of A but still implements de-facto its needs:
|
|
class Implementor {
|
|
val bar = 3
|
|
fun foo() = 1
|
|
}
|
|
|
|
// now we can use MI to implement abstract class:
|
|
class F2 : A(42), Implementor
|
|
|
|
assertEquals(1, F2().foo())
|
|
assertEquals(3, F2().bar)
|
|
"""
|
|
)
|
|
}
|
|
|
|
@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() {
|
|
// This is NOT an override, but a new method
|
|
fun secret() = 2
|
|
}
|
|
val d = Derived()
|
|
assertEquals(2, d.secret())
|
|
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(
|
|
"""
|
|
interface 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(val id) {
|
|
var health
|
|
var mana
|
|
fun isAlive() = health > 0
|
|
fun status() = name + " (#" + 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
|
|
}
|
|
|
|
// Part 1: Provides health
|
|
class HealthPool(var health)
|
|
|
|
// Part 2: Provides mana and name
|
|
class ManaPool(var mana) {
|
|
val name = "Hero"
|
|
}
|
|
|
|
// Composite class implementing Character by parts
|
|
class Warrior(id, h, m) : HealthPool(h), ManaPool(m), Character(id)
|
|
|
|
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()
|
|
)
|
|
}
|
|
} |