2026-01-05 10:35:38 +01:00

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()
)
}
}