Improvements to several observables and more unit tests.

This commit is contained in:
Daan Vanden Bosch 2021-06-19 10:04:06 +02:00
parent 6d412b870d
commit 12e7d79863
16 changed files with 285 additions and 104 deletions

View File

@ -157,6 +157,16 @@ Features that are in ***bold italics*** are planned but not yet implemented.
- ***Support different sets of instructions (older versions had no stack)*** - ***Support different sets of instructions (older versions had no stack)***
## Verification/Warnings
- ***Entities with nonexistent event section***
- ***Entities with wave that's never triggered***
- ***Duplicate event IDs***
- ***Events with nonexistent event section***
- ***Event waves with no enemies***
- ***Events that trigger nonexistent events***
- ***Events that lock/unlock nonexistent doors***
## Bugs ## Bugs
- When a modal dialog is open, global keybindings should be disabled - When a modal dialog is open, global keybindings should be disabled

View File

@ -1,13 +1,13 @@
package world.phantasmal.observable.cell.list package world.phantasmal.observable.cell.list
import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.unsafe.unsafeAssertNotNull
import world.phantasmal.observable.CallbackObserver import world.phantasmal.observable.CallbackObserver
import world.phantasmal.observable.Dependent import world.phantasmal.observable.Dependent
import world.phantasmal.observable.Observer import world.phantasmal.observable.Observer
import world.phantasmal.observable.cell.AbstractDependentCell import world.phantasmal.observable.cell.AbstractDependentCell
import world.phantasmal.observable.cell.Cell import world.phantasmal.observable.cell.Cell
import world.phantasmal.observable.cell.DependentCell import world.phantasmal.observable.cell.DependentCell
import world.phantasmal.observable.cell.not
abstract class AbstractDependentListCell<E> : abstract class AbstractDependentListCell<E> :
AbstractDependentCell<List<E>>(), AbstractDependentCell<List<E>>(),
@ -25,12 +25,35 @@ abstract class AbstractDependentListCell<E> :
return elements return elements
} }
@Suppress("LeakingThis") private var _size: Cell<Int>? = null
final override val size: Cell<Int> = DependentCell(this) { elements.size } final override val size: Cell<Int>
get() {
if (_size == null) {
_size = DependentCell(this) { value.size }
}
final override val empty: Cell<Boolean> = size.map { it == 0 } return unsafeAssertNotNull(_size)
}
final override val notEmpty: Cell<Boolean> = !empty private var _empty: Cell<Boolean>? = null
final override val empty: Cell<Boolean>
get() {
if (_empty == null) {
_empty = DependentCell(this) { value.isEmpty() }
}
return unsafeAssertNotNull(_empty)
}
private var _notEmpty: Cell<Boolean>? = null
final override val notEmpty: Cell<Boolean>
get() {
if (_notEmpty == null) {
_notEmpty = DependentCell(this) { value.isNotEmpty() }
}
return unsafeAssertNotNull(_notEmpty)
}
final override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable = final override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable =
observeList(callNow, observer as ListObserver<E>) observeList(callNow, observer as ListObserver<E>)

View File

@ -14,7 +14,7 @@ class FilteredListCell<E>(
*/ */
private val indexMap = mutableListOf<Int>() private val indexMap = mutableListOf<Int>()
private var elements: ListWrapper<E> = ListWrapper(mutableListOf()) private val elements = mutableListOf<E>()
override val value: List<E> override val value: List<E>
get() { get() {
@ -125,7 +125,7 @@ class FilteredListCell<E>(
} }
} }
elements = elements.mutate { add(insertIndex, change.updated) } elements.add(insertIndex, change.updated)
indexMap[change.index] = insertIndex indexMap[change.index] = insertIndex
for (depIdx in (change.index + 1)..indexMap.lastIndex) { for (depIdx in (change.index + 1)..indexMap.lastIndex) {
@ -151,7 +151,7 @@ class FilteredListCell<E>(
if (index != -1) { if (index != -1) {
// If the element now doesn't pass the test and it previously did // If the element now doesn't pass the test and it previously did
// pass, remove it and emit a structural change. // pass, remove it and emit a structural change.
elements = elements.mutate { removeAt(index) } elements.removeAt(index)
indexMap[change.index] = -1 indexMap[change.index] = -1
for (depIdx in (change.index + 1)..indexMap.lastIndex) { for (depIdx in (change.index + 1)..indexMap.lastIndex) {
@ -195,18 +195,16 @@ class FilteredListCell<E>(
} }
private fun recompute() { private fun recompute() {
val newElements = mutableListOf<E>() elements.clear()
indexMap.clear() indexMap.clear()
dependency.value.forEach { element -> dependency.value.forEach { element ->
if (predicate(element)) { if (predicate(element)) {
newElements.add(element) elements.add(element)
indexMap.add(newElements.lastIndex) indexMap.add(elements.lastIndex)
} else { } else {
indexMap.add(-1) indexMap.add(-1)
} }
} }
elements = ListWrapper(newElements)
} }
} }

View File

@ -14,6 +14,11 @@ fun <E> mutableListCell(
): MutableListCell<E> = ): MutableListCell<E> =
SimpleListCell(mutableListOf(*elements), extractDependencies) SimpleListCell(mutableListOf(*elements), extractDependencies)
fun <T, R> Cell<T>.flatMapToList(
transform: (T) -> ListCell<R>,
): ListCell<R> =
FlatteningDependentListCell(this) { transform(value) }
fun <T1, T2, R> flatMapToList( fun <T1, T2, R> flatMapToList(
c1: Cell<T1>, c1: Cell<T1>,
c2: Cell<T2>, c2: Cell<T2>,

View File

@ -1,30 +0,0 @@
package world.phantasmal.observable.cell.list
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* ListWrapper is used to ensure that ListCell.value of some implementations references a new object
* after every change to the ListCell. This is done to honor the contract that emission of a
* ChangeEvent implies that Cell.value is no longer equal to the previous value.
* When a change is made to the ListCell, the underlying list of ListWrapper is usually mutated and
* then a new wrapper is created that points to the same underlying list.
*/
internal class ListWrapper<E>(private val mut: MutableList<E>) : List<E> by mut {
inline fun mutate(mutator: MutableList<E>.() -> Unit): ListWrapper<E> {
contract { callsInPlace(mutator, InvocationKind.EXACTLY_ONCE) }
mut.mutator()
return ListWrapper(mut)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
// If other is also a ListWrapper but it's not the exact same object then it's not equal.
if (other == null || this::class == other::class || other !is List<*>) return false
// If other is a list but not a ListWrapper, call its equals method for a structured
// comparison.
return other == this
}
override fun hashCode(): Int = mut.hashCode()
}

View File

@ -1,11 +1,12 @@
package world.phantasmal.observable.cell.list package world.phantasmal.observable.cell.list
import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.replaceAll
import world.phantasmal.core.unsafe.unsafeAssertNotNull import world.phantasmal.core.unsafe.unsafeAssertNotNull
import world.phantasmal.observable.ChangeEvent import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.ChangeManager
import world.phantasmal.observable.Dependency import world.phantasmal.observable.Dependency
import world.phantasmal.observable.Dependent import world.phantasmal.observable.Dependent
import world.phantasmal.observable.ChangeManager
typealias DependenciesExtractor<E> = (element: E) -> Array<Dependency> typealias DependenciesExtractor<E> = (element: E) -> Array<Dependency>
@ -16,12 +17,10 @@ typealias DependenciesExtractor<E> = (element: E) -> Array<Dependency>
* event. * event.
*/ */
class SimpleListCell<E>( class SimpleListCell<E>(
elements: MutableList<E>, private val elements: MutableList<E>,
private val extractDependencies: DependenciesExtractor<E>? = null, private val extractDependencies: DependenciesExtractor<E>? = null,
) : AbstractListCell<E>(), MutableListCell<E> { ) : AbstractListCell<E>(), MutableListCell<E> {
private var elements = ListWrapper(elements)
/** /**
* Dependents of dependencies related to this list's elements. Allows us to propagate changes to * Dependents of dependencies related to this list's elements. Allows us to propagate changes to
* elements via [ListChangeEvent]s. * elements via [ListChangeEvent]s.
@ -43,10 +42,9 @@ class SimpleListCell<E>(
checkIndex(index, elements.lastIndex) checkIndex(index, elements.lastIndex)
emitMightChange() emitMightChange()
val removed: E val removed = elements.set(index, element)
elements = elements.mutate { removed = set(index, element) }
if (extractDependencies != null) { if (dependents.isNotEmpty() && extractDependencies != null) {
elementDependents[index].dispose() elementDependents[index].dispose()
elementDependents[index] = ElementDependent(index, element) elementDependents[index] = ElementDependent(index, element)
} }
@ -61,7 +59,7 @@ class SimpleListCell<E>(
emitMightChange() emitMightChange()
val index = elements.size val index = elements.size
elements = elements.mutate { add(index, element) } elements.add(element)
finalizeStructuralChange(index, emptyList(), listOf(element)) finalizeStructuralChange(index, emptyList(), listOf(element))
} }
@ -70,7 +68,7 @@ class SimpleListCell<E>(
checkIndex(index, elements.size) checkIndex(index, elements.size)
emitMightChange() emitMightChange()
elements = elements.mutate { add(index, element) } elements.add(index, element)
finalizeStructuralChange(index, emptyList(), listOf(element)) finalizeStructuralChange(index, emptyList(), listOf(element))
} }
@ -90,8 +88,7 @@ class SimpleListCell<E>(
checkIndex(index, elements.lastIndex) checkIndex(index, elements.lastIndex)
emitMightChange() emitMightChange()
val removed: E val removed = elements.removeAt(index)
elements = elements.mutate { removed = removeAt(index) }
finalizeStructuralChange(index, listOf(removed), emptyList()) finalizeStructuralChange(index, listOf(removed), emptyList())
return removed return removed
@ -100,30 +97,32 @@ class SimpleListCell<E>(
override fun replaceAll(elements: Iterable<E>) { override fun replaceAll(elements: Iterable<E>) {
emitMightChange() emitMightChange()
val removed = this.elements val removed = ArrayList(this.elements)
this.elements = ListWrapper(elements.toMutableList()) this.elements.replaceAll(elements)
finalizeStructuralChange(0, removed, this.elements) finalizeStructuralChange(0, removed, ArrayList(this.elements))
} }
override fun replaceAll(elements: Sequence<E>) { override fun replaceAll(elements: Sequence<E>) {
emitMightChange() emitMightChange()
val removed = this.elements val removed = ArrayList(this.elements)
this.elements = ListWrapper(elements.toMutableList()) this.elements.replaceAll(elements)
finalizeStructuralChange(0, removed, this.elements) finalizeStructuralChange(0, removed, ArrayList(this.elements))
} }
override fun splice(fromIndex: Int, removeCount: Int, newElement: E) { override fun splice(fromIndex: Int, removeCount: Int, newElement: E) {
val removed = ArrayList(elements.subList(fromIndex, fromIndex + removeCount)) val removed = ArrayList<E>(removeCount)
for (i in fromIndex until (fromIndex + removeCount)) {
removed.add(elements[i])
}
emitMightChange() emitMightChange()
elements = elements.mutate { repeat(removeCount) { elements.removeAt(fromIndex) }
repeat(removeCount) { removeAt(fromIndex) } elements.add(fromIndex, newElement)
add(fromIndex, newElement)
}
finalizeStructuralChange(fromIndex, removed, listOf(newElement)) finalizeStructuralChange(fromIndex, removed, listOf(newElement))
} }
@ -131,8 +130,8 @@ class SimpleListCell<E>(
override fun clear() { override fun clear() {
emitMightChange() emitMightChange()
val removed = elements val removed = ArrayList(this.elements)
elements = ListWrapper(mutableListOf()) elements.clear()
finalizeStructuralChange(0, removed, emptyList()) finalizeStructuralChange(0, removed, emptyList())
} }
@ -140,15 +139,16 @@ class SimpleListCell<E>(
override fun sortWith(comparator: Comparator<E>) { override fun sortWith(comparator: Comparator<E>) {
emitMightChange() emitMightChange()
val removed = ArrayList(elements)
var throwable: Throwable? = null var throwable: Throwable? = null
try { try {
elements = elements.mutate { sortWith(comparator) } elements.sortWith(comparator)
} catch (e: Throwable) { } catch (e: Throwable) {
throwable = e throwable = e
} }
finalizeStructuralChange(0, elements, elements) finalizeStructuralChange(0, removed, ArrayList(elements))
if (throwable != null) { if (throwable != null) {
throw throwable throw throwable
@ -178,11 +178,9 @@ class SimpleListCell<E>(
} }
override fun emitDependencyChanged() { override fun emitDependencyChanged() {
try { val currentChanges = changes
emitDependencyChanged(ListChangeEvent(elements, changes)) changes = mutableListOf()
} finally { emitDependencyChanged(ListChangeEvent(elements, currentChanges))
changes = mutableListOf()
}
} }
private fun checkIndex(index: Int, maxIndex: Int) { private fun checkIndex(index: Int, maxIndex: Int) {
@ -194,7 +192,7 @@ class SimpleListCell<E>(
} }
private fun finalizeStructuralChange(index: Int, removed: List<E>, inserted: List<E>) { private fun finalizeStructuralChange(index: Int, removed: List<E>, inserted: List<E>) {
if (extractDependencies != null) { if (dependents.isNotEmpty() && extractDependencies != null) {
repeat(removed.size) { repeat(removed.size) {
elementDependents.removeAt(index).dispose() elementDependents.removeAt(index).dispose()
} }

View File

@ -0,0 +1,47 @@
package world.phantasmal.observable
import world.phantasmal.observable.test.ObservableTestSuite
import kotlin.test.*
interface DependencyTests : ObservableTestSuite {
fun createProvider(): Provider
@Test
fun correctly_emits_changes_to_its_dependents() = test {
val p = createProvider()
var dependencyMightChangeCalled = false
var dependencyChangedCalled = false
p.dependency.addDependent(object : Dependent {
override fun dependencyMightChange() {
assertFalse(dependencyMightChangeCalled)
assertFalse(dependencyChangedCalled)
dependencyMightChangeCalled = true
}
override fun dependencyChanged(dependency: Dependency, event: ChangeEvent<*>?) {
assertTrue(dependencyMightChangeCalled)
assertFalse(dependencyChangedCalled)
assertEquals(p.dependency, dependency)
assertNotNull(event)
dependencyChangedCalled = true
}
})
repeat(5) { index ->
dependencyMightChangeCalled = false
dependencyChangedCalled = false
p.emit()
assertTrue(dependencyMightChangeCalled, "repetition $index")
assertTrue(dependencyChangedCalled, "repetition $index")
}
}
interface Provider {
val dependency: Dependency
fun emit()
}
}

View File

@ -1,6 +1,5 @@
package world.phantasmal.observable package world.phantasmal.observable
import world.phantasmal.observable.test.ObservableTestSuite
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -8,8 +7,8 @@ import kotlin.test.assertEquals
* Test suite for all [Observable] implementations. There is a subclass of this suite for every * Test suite for all [Observable] implementations. There is a subclass of this suite for every
* [Observable] implementation. * [Observable] implementation.
*/ */
interface ObservableTests : ObservableTestSuite { interface ObservableTests : DependencyTests {
fun createProvider(): Provider override fun createProvider(): Provider
@Test @Test
fun calls_observers_when_events_are_emitted() = test { fun calls_observers_when_events_are_emitted() = test {
@ -55,9 +54,9 @@ interface ObservableTests : ObservableTestSuite {
assertEquals(1, changes) assertEquals(1, changes)
} }
interface Provider { interface Provider : DependencyTests.Provider {
val observable: Observable<*> val observable: Observable<*>
fun emit() override val dependency: Dependency get() = observable
} }
} }

View File

@ -9,12 +9,12 @@ class DependentCellTests : RegularCellTests, CellWithDependenciesTests {
} }
class Provider : CellTests.Provider, CellWithDependenciesTests.Provider { class Provider : CellTests.Provider, CellWithDependenciesTests.Provider {
private val dependency = SimpleCell(1) private val dependencyCell = SimpleCell(1)
override val observable = DependentCell(dependency) { 2 * dependency.value } override val observable = DependentCell(dependencyCell) { 2 * dependencyCell.value }
override fun emit() { override fun emit() {
dependency.value += 2 dependencyCell.value += 2
} }
override fun createWithDependencies(vararg dependencies: Cell<Int>) = override fun createWithDependencies(vararg dependencies: Cell<Int>) =

View File

@ -9,12 +9,14 @@ class DependentListCellTests : ListCellTests, CellWithDependenciesTests {
override fun createListProvider(empty: Boolean) = Provider(empty) override fun createListProvider(empty: Boolean) = Provider(empty)
class Provider(empty: Boolean) : ListCellTests.Provider, CellWithDependenciesTests.Provider { class Provider(empty: Boolean) : ListCellTests.Provider, CellWithDependenciesTests.Provider {
private val dependency = SimpleListCell(if (empty) mutableListOf() else mutableListOf(5)) private val dependencyCell =
SimpleListCell(if (empty) mutableListOf() else mutableListOf(5))
override val observable = DependentListCell(dependency) { dependency.value.map { 2 * it } } override val observable =
DependentListCell(dependencyCell) { dependencyCell.value.map { 2 * it } }
override fun addElement() { override fun addElement() {
dependency.add(4) dependencyCell.add(4)
} }
override fun createWithDependencies(vararg dependencies: Cell<Int>): Cell<Any> = override fun createWithDependencies(vararg dependencies: Cell<Int>): Cell<Any> =

View File

@ -5,13 +5,13 @@ import kotlin.test.*
class FilteredListCellTests : ListCellTests { class FilteredListCellTests : ListCellTests {
override fun createListProvider(empty: Boolean) = object : ListCellTests.Provider { override fun createListProvider(empty: Boolean) = object : ListCellTests.Provider {
private val dependency = private val dependencyCell =
SimpleListCell(if (empty) mutableListOf(5) else mutableListOf(5, 10)) SimpleListCell(if (empty) mutableListOf(5) else mutableListOf(5, 10))
override val observable = FilteredListCell(dependency, predicate = { it % 2 == 0 }) override val observable = FilteredListCell(dependencyCell, predicate = { it % 2 == 0 })
override fun addElement() { override fun addElement() {
dependency.add(4) dependencyCell.add(4)
} }
} }

View File

@ -11,15 +11,15 @@ class FlatteningDependentListCellDirectDependencyEmitsTests : ListCellTests {
private val transitiveDependency = StaticListCell(if (empty) emptyList() else listOf(7)) private val transitiveDependency = StaticListCell(if (empty) emptyList() else listOf(7))
// The direct dependency of the list under test can change. // The direct dependency of the list under test can change.
private val dependency = SimpleCell<ListCell<Int>>(transitiveDependency) private val directDependency = SimpleCell<ListCell<Int>>(transitiveDependency)
override val observable = override val observable =
FlatteningDependentListCell(dependency) { dependency.value } FlatteningDependentListCell(directDependency) { directDependency.value }
override fun addElement() { override fun addElement() {
// Update the direct dependency. // Update the direct dependency.
val oldTransitiveDependency: ListCell<Int> = dependency.value val oldTransitiveDependency: ListCell<Int> = directDependency.value
dependency.value = StaticListCell(oldTransitiveDependency.value + 4) directDependency.value = StaticListCell(oldTransitiveDependency.value + 4)
} }
} }
} }

View File

@ -21,10 +21,10 @@ class FlatteningDependentListCellTransitiveDependencyEmitsTests :
SimpleListCell(if (empty) mutableListOf() else mutableListOf(7)) SimpleListCell(if (empty) mutableListOf() else mutableListOf(7))
// The direct dependency of the list under test can't change. // The direct dependency of the list under test can't change.
private val dependency = StaticCell<ListCell<Int>>(transitiveDependency) private val directDependency = StaticCell<ListCell<Int>>(transitiveDependency)
override val observable = override val observable =
FlatteningDependentListCell(dependency) { dependency.value } FlatteningDependentListCell(directDependency) { directDependency.value }
override fun addElement() { override fun addElement() {
// Update the transitive dependency. // Update the transitive dependency.

View File

@ -0,0 +1,17 @@
package world.phantasmal.observable.cell.list
import world.phantasmal.observable.cell.SimpleCell
import world.phantasmal.observable.test.ObservableTestSuite
import world.phantasmal.observable.test.assertListCellEquals
import kotlin.test.Test
class ListCellCreationTests : ObservableTestSuite {
@Test
fun test_flatMapToList() = test {
val cell = SimpleCell(SimpleListCell(mutableListOf(1, 2, 3, 4, 5)))
val mapped = cell.flatMapToList { it }
assertListCellEquals(listOf(1, 2, 3, 4, 5), mapped)
}
}

View File

@ -1,6 +1,7 @@
package world.phantasmal.observable.cell.list package world.phantasmal.observable.cell.list
import world.phantasmal.observable.cell.SimpleCell import world.phantasmal.observable.cell.SimpleCell
import world.phantasmal.observable.test.assertListCellEquals
import world.phantasmal.testUtils.TestContext import world.phantasmal.testUtils.TestContext
import kotlin.test.* import kotlin.test.*
@ -25,11 +26,30 @@ class SimpleListCellTests : MutableListCellTests<Int> {
fun instantiates_correctly() = test { fun instantiates_correctly() = test {
val list = SimpleListCell(mutableListOf(1, 2, 3)) val list = SimpleListCell(mutableListOf(1, 2, 3))
assertEquals(3, list.size.value) assertListCellEquals(listOf(1, 2, 3), list)
assertEquals(3, list.value.size) }
assertEquals(1, list[0])
assertEquals(2, list[1]) @Test
assertEquals(3, list[2]) fun set() = test {
testSet(SimpleListCell(mutableListOf("a", "b", "c")))
}
@Test
fun set_with_extractDependencies() = test {
testSet(SimpleListCell(mutableListOf("a", "b", "c")) { arrayOf() })
}
private fun testSet(list: SimpleListCell<String>) {
list[1] = "test"
list[2] = "test2"
assertFailsWith<IndexOutOfBoundsException> {
list[-1] = "should not be in list"
}
assertFailsWith<IndexOutOfBoundsException> {
list[3] = "should not be in list"
}
assertListCellEquals(listOf("a", "test", "test2"), list)
} }
@Test @Test
@ -40,10 +60,92 @@ class SimpleListCellTests : MutableListCellTests<Int> {
list.add(1, "c") list.add(1, "c")
list.add(0, "a") list.add(0, "a")
assertEquals(3, list.size.value) assertListCellEquals(listOf("a", "b", "c"), list)
assertEquals("a", list[0]) }
assertEquals("b", list[1])
assertEquals("c", list[2]) @Test
fun remove() = test {
val list = SimpleListCell(mutableListOf("a", "b", "c", "d", "e"))
assertTrue(list.remove("c"))
assertListCellEquals(listOf("a", "b", "d", "e"), list)
assertTrue(list.remove("a"))
assertListCellEquals(listOf("b", "d", "e"), list)
assertTrue(list.remove("e"))
assertListCellEquals(listOf("b", "d"), list)
// The following values are not in the list (anymore).
assertFalse(list.remove("x"))
assertFalse(list.remove("a"))
assertFalse(list.remove("c"))
// List should remain unchanged after removal attempts of nonexistent elements.
assertListCellEquals(listOf("b", "d"), list)
}
@Test
fun removeAt() = test {
val list = SimpleListCell(mutableListOf("a", "b", "c", "d", "e"))
list.removeAt(2)
assertListCellEquals(listOf("a", "b", "d", "e"), list)
list.removeAt(0)
assertListCellEquals(listOf("b", "d", "e"), list)
list.removeAt(2)
assertListCellEquals(listOf("b", "d"), list)
assertFailsWith<IndexOutOfBoundsException> {
list.removeAt(-1)
}
assertFailsWith<IndexOutOfBoundsException> {
list.removeAt(list.size.value)
}
// List should remain unchanged after invalid calls.
assertListCellEquals(listOf("b", "d"), list)
}
@Test
fun splice() = test {
val list = SimpleListCell((0..9).toMutableList())
list.splice(fromIndex = 3, removeCount = 5, newElement = 100)
assertListCellEquals(listOf(0, 1, 2, 100, 8, 9), list)
list.splice(fromIndex = 0, removeCount = 0, newElement = 101)
assertListCellEquals(listOf(101, 0, 1, 2, 100, 8, 9), list)
list.splice(fromIndex = list.size.value, removeCount = 0, newElement = 102)
assertListCellEquals(listOf(101, 0, 1, 2, 100, 8, 9, 102), list)
// Negative fromIndex.
assertFailsWith<IndexOutOfBoundsException> {
list.splice(fromIndex = -1, removeCount = 0, newElement = 200)
}
// fromIndex too large.
assertFailsWith<IndexOutOfBoundsException> {
list.splice(fromIndex = list.size.value + 1, removeCount = 0, newElement = 201)
}
// removeCount too large.
assertFailsWith<IndexOutOfBoundsException> {
list.splice(fromIndex = 0, removeCount = 50, newElement = 202)
}
// List should remain unchanged after invalid calls.
assertListCellEquals(listOf(101, 0, 1, 2, 100, 8, 9, 102), list)
} }
@Test @Test

View File

@ -0,0 +1,10 @@
package world.phantasmal.observable.test
import world.phantasmal.observable.cell.list.ListCell
import kotlin.test.assertEquals
fun <E> assertListCellEquals(expected: List<E>, actual: ListCell<E>) {
assertEquals(expected.size, actual.size.value)
assertEquals(expected.size, actual.value.size)
assertEquals(expected, actual.value)
}