/* * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ package test.collections import test.TestPlatform import test.collections.js.linkedStringMapOf import test.collections.js.linkedStringSetOf import test.collections.js.stringMapOf import test.collections.js.stringSetOf import test.current import kotlin.test.Test import kotlin.test.assertFailsWith import kotlin.test.assertTrue import kotlin.test.fail class ConcurrentModificationTest { private fun > testIteratorThrowsCME( withCollection: WithCollection, createIterator: C.() -> I, collectionOp: CollectionOperation, iteratorOp: IteratorOperation ) { var invoked = false withCollection { collection -> invoked = true val iterator = collection.createIterator() iteratorOp.precedingFunction?.invoke(iterator) collectionOp.function.invoke(collection) val message = "listOp: ${collectionOp.description}, iteratorOp: ${iteratorOp.description}" if (collectionOp.throwsCME) { assertFailsWith(message) { iteratorOp.function.invoke(iterator) } } else { try { iteratorOp.function.invoke(iterator) } catch (e: Throwable) { fail("$message. Expected no exception, but was $e") } } } assertTrue(invoked) } private fun > testIteratorThrowsCME( withMutableList: WithCollection, listOps: List> ) { for (listOp in listOps) { for (iteratorOp in iteratorOperations()) { testIteratorThrowsCME(withMutableList, { iterator() }, listOp, iteratorOp) } for (iteratorOp in listIteratorOperations) { testIteratorThrowsCME(withMutableList, { listIterator() }, listOp, iteratorOp) testIteratorThrowsCME(withMutableList, { listIterator(2) }, listOp, iteratorOp) } } } private fun > testSubListThrowsCME( withSubList: WithCollection, subListOps: List> ) { var invoked = false withSubList { subList -> invoked = true for (subListOp in subListOps) { val message = "subListOp: ${subListOp.description}" if (subListOp.throwsCME) { assertFailsWith(message) { subListOp.function.invoke(subList) } } else { try { subListOp.function.invoke(subList) } catch (e: Throwable) { fail("$message. Expected no exception, but was $e") } } } } assertTrue(invoked) } @Test fun mutableList() { if (TestPlatform.current == TestPlatform.Js) return /** * Some operations should register a modification by contract, but java ArrayList, * whose implementation we can't change, do not register. * @param isJavaArrayListBehavior specifies whether to test java ArrayList behavior or the behavior by contract. */ fun operations(isJavaArrayListBehavior: Boolean) = listOf>>( CollectionOperation("set()", throwsCME = false) { set(2, "e") }, CollectionOperation("add()") { add("e") }, CollectionOperation("add(index)") { add(2, "e") }, CollectionOperation("remove(non-existing)", throwsCME = false) { remove("e") }, CollectionOperation("remove(existing)") { remove("d") }, CollectionOperation("removeAt()") { removeAt(2) }, CollectionOperation("addAll()") { addAll(listOf("e", "f")) }, CollectionOperation("addAll(emptyList())", throwsCME = isJavaArrayListBehavior) { addAll(emptyList()) }, CollectionOperation("addAll(index)") { addAll(2, listOf("e", "f")) }, CollectionOperation("addAll(index, emptyList())", throwsCME = isJavaArrayListBehavior) { addAll(2, emptyList()) }, CollectionOperation("removeAll(non-existing)", throwsCME = false) { removeAll(listOf("e", "f")) }, CollectionOperation("removeAll(some exist)") { removeAll(listOf("d", "e")) }, CollectionOperation("removeAll(emptyList())", throwsCME = false) { removeAll(emptyList()) }, CollectionOperation("retainAll(this.toMutableList())", throwsCME = false) { retainAll(this.toMutableList()) }, CollectionOperation("retainAll(non-existing)") { retainAll(listOf("e", "f")) }, CollectionOperation("retainAll(some exist)") { retainAll(listOf("d", "e")) }, CollectionOperation("retainAll(emptyList())") { retainAll(emptyList()) }, CollectionOperation("clear()") { clear() }, CollectionOperation("iterator.remove()") { iterator().apply { next(); remove() } }, ).also { ops -> ops + ops.map { CollectionOperation("subList(1, size)." + it.description, it.throwsCME) { it.function.invoke(subList(1, size)) } } } fun testThrowsCME(isJavaArrayListBehavior: Boolean = true, withMutableList: WithCollection>) { testIteratorThrowsCME(withMutableList, operations(isJavaArrayListBehavior)) } // size == capacity testThrowsCME { action -> MutableList(4) { ('a' + it).toString() }.also(action) } testThrowsCME { action -> mutableListOf("a", "b", "c", "d").also(action) } testThrowsCME { action -> buildList(4) { addAll(listOf("a", "b", "c", "d")) action(this) } } testThrowsCME(isJavaArrayListBehavior = false) { action -> ArrayDeque(4).apply { addAll(listOf("a", "b", "c", "d")) action(this) } } testThrowsCME(isJavaArrayListBehavior = false) { action -> ArrayDeque(listOf("a", "b", "c", "d")).also(action) } // size < capacity testThrowsCME { action -> ArrayList(10).apply { addAll(listOf("a", "b", "c", "d")) action(this) } } testThrowsCME { action -> buildList(10) { addAll(listOf("a", "b", "c", "d")) action(this) } } testThrowsCME(isJavaArrayListBehavior = false) { action -> ArrayDeque(10).apply { addAll(listOf("a", "b", "c", "d")) action(this) } } } @Test fun arrayList() { if (TestPlatform.current == TestPlatform.Js) return val operations = listOf>>( CollectionOperation("trimToSize()") { trimToSize() }, CollectionOperation("ensureCapacity(minCapacity > capacity)") { ensureCapacity(100) }, ) fun testThrowsCME(withArrayList: WithCollection>) { testIteratorThrowsCME(withArrayList, operations) } // size == capacity testThrowsCME { action -> ArrayList(4).apply { addAll(listOf("a", "b", "c", "d")) action(this) } } testThrowsCME { action -> arrayListOf("a", "b", "c", "d").also(action) } // size < capacity testThrowsCME { action -> ArrayList(10).apply { addAll(listOf("a", "b", "c", "d")) action(this) } } } @Test fun arrayDeque() { if (TestPlatform.current == TestPlatform.Js) return val operations = listOf>>( CollectionOperation("addFirst()") { addFirst("e") }, CollectionOperation("addLast()") { addLast("e") }, CollectionOperation("removeFirst()") { removeFirst() }, CollectionOperation("removeLast()") { removeLast() }, CollectionOperation("removeFirstOrNull()") { removeFirstOrNull() }, CollectionOperation("removeLastOrNull()") { removeLastOrNull() }, ) fun testThrowsCME(withArrayDeque: WithCollection>) { testIteratorThrowsCME(withArrayDeque, operations) } // size == capacity testThrowsCME { action -> ArrayDeque(4).apply { addAll(listOf("a", "b", "c", "d")) action(this) } } testThrowsCME { action -> ArrayDeque(listOf("a", "b", "c", "d")).also(action) } // size < capacity testThrowsCME { action -> ArrayDeque(10).apply { addAll(listOf("a", "b", "c", "d")) action(this) } } } @Test fun subList() { if (TestPlatform.current == TestPlatform.Js) return val operations = listOf>>( CollectionOperation("isEmpty()") { isEmpty() }, CollectionOperation("size") { size }, CollectionOperation("equals()") { equals(listOf("x")) }, CollectionOperation("hashCode()") { hashCode() }, CollectionOperation("toString()") { toString() }, CollectionOperation("indexOf") { indexOf("d") }, CollectionOperation("lastIndexOf") { lastIndexOf("d") }, CollectionOperation("contains") { contains("d") }, CollectionOperation("get()") { get(2) }, CollectionOperation("set()") { set(2, "e") }, CollectionOperation("add()") { add("e") }, CollectionOperation("add(index)") { add(2, "e") }, CollectionOperation("remove()") { remove("d") }, CollectionOperation("removeAt()") { removeAt(2) }, CollectionOperation("addAll()") { addAll(listOf("e", "f")) }, CollectionOperation("addAll(index)") { addAll(2, listOf("e", "f")) }, CollectionOperation("removeAll()") { removeAll(listOf("d", "e")) }, CollectionOperation("retainAll()") { retainAll(listOf("d", "e")) }, CollectionOperation("clear()") { clear() }, CollectionOperation("iterator()") { iterator() }, CollectionOperation("listIterator()") { listIterator() }, ) fun testThrowsCME(withMutableList: WithCollection>) { testSubListThrowsCME(withMutableList, operations) } testThrowsCME { action -> val arrayList = arrayListOf("a", "b", "c", "d") val subList = arrayList.subList(0, arrayList.size) arrayList.add("e") action(subList) } assertFailsWith { val arrayList = arrayListOf("a", "b", "c", "d") for (e in arrayList.subList(1, 3)) arrayList.remove(e) } testThrowsCME { action -> buildList { addAll(listOf("a", "b", "c", "d")) val subList = subList(0, size) add("e") action(subList) } } assertFailsWith { buildList { addAll(listOf("a", "b", "c")) for (e in subList(1, 3)) remove(e) } } testThrowsCME { action -> val arrayDeque = ArrayDeque(listOf("a", "b", "c", "d")) val subList = arrayDeque.subList(0, arrayDeque.size) arrayDeque.add("e") action(subList) } assertFailsWith { val arrayDeque = ArrayDeque(listOf("a", "b", "c", "d")) for (e in arrayDeque.subList(1, 3)) arrayDeque.remove(e) } } @Test fun mutableSet() { val operations = listOf>>( CollectionOperation("add(non-existing)") { add("e") }, CollectionOperation("add(existing)", throwsCME = false) { add("d") }, CollectionOperation("remove(non-existing)", throwsCME = false) { remove("e") }, CollectionOperation("remove(existing)") { remove("d") }, CollectionOperation("addAll(emptyList())", throwsCME = false) { addAll(emptyList()) }, CollectionOperation("addAll(existing)", throwsCME = false) { addAll(listOf("d", "b")) }, CollectionOperation("addAll(some exist)") { addAll(listOf("d", "e")) }, CollectionOperation("addAll(non-existing)") { addAll(listOf("e", "f")) }, CollectionOperation("removeAll(non-existing)", throwsCME = false) { removeAll(listOf("e", "f")) }, CollectionOperation("removeAll(some exist") { removeAll(listOf("d", "e")) }, CollectionOperation("removeAll(emptyList())", throwsCME = false) { removeAll(emptyList()) }, CollectionOperation("retainAll(this.toMutableSet())", throwsCME = false) { retainAll(this.toMutableSet()) }, CollectionOperation("retainAll(non-existing)") { retainAll(listOf("e", "f")) }, CollectionOperation("retainAll(some exist)") { retainAll(listOf("d", "e")) }, CollectionOperation("retainAll(emptyList())") { retainAll(emptyList()) }, CollectionOperation("clear()") { clear() }, CollectionOperation("iterator.remove()") { iterator().apply { next(); remove() } }, ) fun testThrowsCME(withMutableSet: WithCollection>) { for (setOp in operations) { for (iteratorOp in iteratorOperations()) { testIteratorThrowsCME(withMutableSet, { iterator() }, setOp, iteratorOp) } } } // Because platform implementations may have different load factor and rehash strategy, // make sure there is enough capacity to avoid rehash. val elements = listOf("a", "b", "c", "d") testThrowsCME { action -> LinkedHashSet(10).apply { addAll(elements) action(this) } } testThrowsCME { action -> HashSet(10).apply { addAll(elements) action(this) } } testThrowsCME { action -> buildSet(10) { addAll(elements) action(this) } } if (TestPlatform.current == TestPlatform.Js) { testThrowsCME { action -> stringSetOf().apply { addAll(elements) action(this) } } testThrowsCME { action -> linkedStringSetOf().apply { addAll(elements) action(this) } } } } @Test fun mutableMap() { val operations = listOf>>( CollectionOperation("put(non-existing)") { put("e", "e") }, CollectionOperation("put(existing)", throwsCME = false) { put("d", "d") }, CollectionOperation("put(update value)", throwsCME = false) { put("d", "D") }, CollectionOperation("remove(non-existing)", throwsCME = false) { remove("e") }, CollectionOperation("remove(existing)") { remove("d") }, CollectionOperation("putAll(emptyList())", throwsCME = false) { putAll(emptyMap()) }, CollectionOperation("putAll(existing)", throwsCME = false) { putAll(mapOf("d" to "d", "b" to "b")) }, CollectionOperation("putAll(update values)", throwsCME = false) { putAll(mapOf("d" to "D", "b" to "B")) }, CollectionOperation("putAll(some exist)") { putAll(mapOf("d" to "d", "e" to "e")) }, CollectionOperation("putAll(non-existing)") { putAll(mapOf("e" to "e", "f" to "f")) }, CollectionOperation("clear()") { clear() }, CollectionOperation("iterator.remove()") { iterator().apply { next(); remove() } }, ) fun testThrowsCME(withMutableMap: WithCollection>) { for (mapOp in operations) { for (iteratorOp in iteratorOperations()) { testIteratorThrowsCME(withMutableMap, { keys.iterator() }, mapOp, iteratorOp) testIteratorThrowsCME(withMutableMap, { values.iterator() }, mapOp, iteratorOp) } for (iteratorOp in iteratorOperations>()) { testIteratorThrowsCME(withMutableMap, { entries.iterator() }, mapOp, iteratorOp) } } } // Because platform implementations may have different load factor and rehash strategy, // make sure there is enough capacity to avoid rehash. val entries = mapOf("a" to "a", "b" to "b", "c" to "c", "d" to "d") testThrowsCME { action -> LinkedHashMap(10).apply { putAll(entries) action(this) } } testThrowsCME { action -> HashMap(10).apply { putAll(entries) action(this) } } testThrowsCME { action -> buildMap(10) { putAll(entries) action(this) } } if (TestPlatform.current == TestPlatform.Js) { testThrowsCME { action -> stringMapOf().apply { putAll(entries) action(this) } } testThrowsCME { action -> linkedStringMapOf().apply { putAll(entries) action(this) } } } } } private typealias WithCollection = (action: (C) -> Unit) -> Unit private class CollectionOperation( val description: String, val throwsCME: Boolean = true, val function: C.() -> Unit ) private class IteratorOperation( val description: String, val precedingFunction: (I.() -> Unit)? = null, val function: I.() -> Unit ) private fun iteratorOperations() = listOf>>( IteratorOperation("next()") { next() }, IteratorOperation("remove()", { next() }) { remove() } ) private val listIteratorOperations = listOf>>( IteratorOperation("next()") { next() }, IteratorOperation("remove()", { next() }) { remove() }, IteratorOperation("previous()", { next() }) { previous() }, IteratorOperation("add(\"e\")") { add("e") } )