Files
Abduqodiri Qurbonzoda c9d8ecc599 Detect concurrent modifications in ArrayDeque #KT-63355
Remove the ArrayDeque.subList().subList() test for comodification detection.
For types that extend AbstractList and to not override subList function,
the behavior varies depending on JDK version. With JKD8, the test fails for ArrayDeque.
While with JDK9+, it passes.
2024-01-31 12:11:48 +00:00

511 lines
19 KiB
Kotlin

/*
* 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 <C, I : Iterator<*>> testIteratorThrowsCME(
withCollection: WithCollection<C>,
createIterator: C.() -> I,
collectionOp: CollectionOperation<C>,
iteratorOp: IteratorOperation<I>
) {
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<ConcurrentModificationException>(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 <C : MutableList<String>> testIteratorThrowsCME(
withMutableList: WithCollection<C>,
listOps: List<CollectionOperation<C>>
) {
for (listOp in listOps) {
for (iteratorOp in iteratorOperations<String>()) {
testIteratorThrowsCME(withMutableList, { iterator() }, listOp, iteratorOp)
}
for (iteratorOp in listIteratorOperations) {
testIteratorThrowsCME(withMutableList, { listIterator() }, listOp, iteratorOp)
testIteratorThrowsCME(withMutableList, { listIterator(2) }, listOp, iteratorOp)
}
}
}
private fun <C : MutableList<String>> testSubListThrowsCME(
withSubList: WithCollection<C>,
subListOps: List<CollectionOperation<C>>
) {
var invoked = false
withSubList { subList ->
invoked = true
for (subListOp in subListOps) {
val message = "subListOp: ${subListOp.description}"
if (subListOp.throwsCME) {
assertFailsWith<ConcurrentModificationException>(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<MutableList<String>>>(
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<MutableList<String>>) {
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<String>(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<String>(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<String>(10).apply {
addAll(listOf("a", "b", "c", "d"))
action(this)
}
}
}
@Test
fun arrayList() {
if (TestPlatform.current == TestPlatform.Js) return
val operations = listOf<CollectionOperation<ArrayList<String>>>(
CollectionOperation("trimToSize()") { trimToSize() },
CollectionOperation("ensureCapacity(minCapacity > capacity)") { ensureCapacity(100) },
)
fun testThrowsCME(withArrayList: WithCollection<ArrayList<String>>) {
testIteratorThrowsCME(withArrayList, operations)
}
// size == capacity
testThrowsCME { action ->
ArrayList<String>(4).apply {
addAll(listOf("a", "b", "c", "d"))
action(this)
}
}
testThrowsCME { action ->
arrayListOf("a", "b", "c", "d").also(action)
}
// size < capacity
testThrowsCME { action ->
ArrayList<String>(10).apply {
addAll(listOf("a", "b", "c", "d"))
action(this)
}
}
}
@Test
fun arrayDeque() {
if (TestPlatform.current == TestPlatform.Js) return
val operations = listOf<CollectionOperation<ArrayDeque<String>>>(
CollectionOperation("addFirst()") { addFirst("e") },
CollectionOperation("addLast()") { addLast("e") },
CollectionOperation("removeFirst()") { removeFirst() },
CollectionOperation("removeLast()") { removeLast() },
CollectionOperation("removeFirstOrNull()") { removeFirstOrNull() },
CollectionOperation("removeLastOrNull()") { removeLastOrNull() },
)
fun testThrowsCME(withArrayDeque: WithCollection<ArrayDeque<String>>) {
testIteratorThrowsCME(withArrayDeque, operations)
}
// size == capacity
testThrowsCME { action ->
ArrayDeque<String>(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<String>(10).apply {
addAll(listOf("a", "b", "c", "d"))
action(this)
}
}
}
@Test
fun subList() {
if (TestPlatform.current == TestPlatform.Js) return
val operations = listOf<CollectionOperation<MutableList<String>>>(
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<MutableList<String>>) {
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<ConcurrentModificationException> {
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<ConcurrentModificationException> {
buildList<String> {
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<ConcurrentModificationException> {
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<MutableSet<String>>>(
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<MutableSet<String>>) {
for (setOp in operations) {
for (iteratorOp in iteratorOperations<String>()) {
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<String>(10).apply {
addAll(elements)
action(this)
}
}
testThrowsCME { action ->
HashSet<String>(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<MutableMap<String, String>>>(
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<MutableMap<String, String>>) {
for (mapOp in operations) {
for (iteratorOp in iteratorOperations<String>()) {
testIteratorThrowsCME(withMutableMap, { keys.iterator() }, mapOp, iteratorOp)
testIteratorThrowsCME(withMutableMap, { values.iterator() }, mapOp, iteratorOp)
}
for (iteratorOp in iteratorOperations<MutableMap.MutableEntry<String, String>>()) {
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<String, String>(10).apply {
putAll(entries)
action(this)
}
}
testThrowsCME { action ->
HashMap<String, String>(10).apply {
putAll(entries)
action(this)
}
}
testThrowsCME { action ->
buildMap(10) {
putAll(entries)
action(this)
}
}
if (TestPlatform.current == TestPlatform.Js) {
testThrowsCME { action ->
stringMapOf<String>().apply {
putAll(entries)
action(this)
}
}
testThrowsCME { action ->
linkedStringMapOf<String>().apply {
putAll(entries)
action(this)
}
}
}
}
}
private typealias WithCollection<C> = (action: (C) -> Unit) -> Unit
private class CollectionOperation<C>(
val description: String,
val throwsCME: Boolean = true,
val function: C.() -> Unit
)
private class IteratorOperation<I>(
val description: String,
val precedingFunction: (I.() -> Unit)? = null,
val function: I.() -> Unit
)
private fun <E> iteratorOperations() = listOf<IteratorOperation<MutableIterator<E>>>(
IteratorOperation("next()") { next() },
IteratorOperation("remove()", { next() }) { remove() }
)
private val listIteratorOperations = listOf<IteratorOperation<MutableListIterator<String>>>(
IteratorOperation("next()") { next() },
IteratorOperation("remove()", { next() }) { remove() },
IteratorOperation("previous()", { next() }) { previous() },
IteratorOperation("add(\"e\")") { add("e") }
)