From 42824a4eb9cedc43d8d452bb8a47a90b21d48b3d Mon Sep 17 00:00:00 2001 From: NthPortal Date: Wed, 2 Sep 2020 14:52:29 -0400 Subject: [PATCH] [bug#12009] Make ArrayBuffer's iterator fail-fast Make `ArrayBuffer`'s iterator fail-fast when the buffer is mutated after the iterator's creation. --- project/MimaFilters.scala | 20 +++ src/library/scala/collection/IndexedSeq.scala | 10 +- .../scala/collection/IndexedSeqView.scala | 26 ++-- .../collection/mutable/ArrayBuffer.scala | 90 ++++++++++---- .../mutable/CheckedIndexedSeqView.scala | 117 ++++++++++++++++++ .../mutable/MutationTrackingTest.scala | 66 ++++++++-- 6 files changed, 272 insertions(+), 57 deletions(-) create mode 100644 src/library/scala/collection/mutable/CheckedIndexedSeqView.scala diff --git a/project/MimaFilters.scala b/project/MimaFilters.scala index 0d41bbe2ed16..c5cb1fdd0649 100644 --- a/project/MimaFilters.scala +++ b/project/MimaFilters.scala @@ -24,6 +24,26 @@ object MimaFilters extends AutoPlugin { // with JDK 11 and run MiMa it'll complain IteratorWrapper isn't forwards compatible with 2.13.0 - but we // don't publish the artifact built with JDK 11 anyways ProblemFilters.exclude[DirectMissingMethodProblem]("scala.collection.convert.JavaCollectionWrappers#IteratorWrapper.asIterator"), + + // Fixes for scala/bug#12009 + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.collection.mutable.ArrayBufferView.this"), + ProblemFilters.exclude[FinalClassProblem]("scala.collection.IndexedSeqView$IndexedSeqViewIterator"), + ProblemFilters.exclude[FinalClassProblem]("scala.collection.IndexedSeqView$IndexedSeqViewReverseIterator"), + ProblemFilters.exclude[MissingClassProblem]("scala.collection.mutable.CheckedIndexedSeqView"), + ProblemFilters.exclude[MissingClassProblem]("scala.collection.mutable.CheckedIndexedSeqView$"), + ProblemFilters.exclude[MissingClassProblem]("scala.collection.mutable.CheckedIndexedSeqView$CheckedIterator"), + ProblemFilters.exclude[MissingClassProblem]("scala.collection.mutable.CheckedIndexedSeqView$CheckedReverseIterator"), + ProblemFilters.exclude[MissingClassProblem]("scala.collection.mutable.CheckedIndexedSeqView$Id"), + ProblemFilters.exclude[MissingClassProblem]("scala.collection.mutable.CheckedIndexedSeqView$Appended"), + ProblemFilters.exclude[MissingClassProblem]("scala.collection.mutable.CheckedIndexedSeqView$Prepended"), + ProblemFilters.exclude[MissingClassProblem]("scala.collection.mutable.CheckedIndexedSeqView$Concat"), + ProblemFilters.exclude[MissingClassProblem]("scala.collection.mutable.CheckedIndexedSeqView$Take"), + ProblemFilters.exclude[MissingClassProblem]("scala.collection.mutable.CheckedIndexedSeqView$TakeRight"), + ProblemFilters.exclude[MissingClassProblem]("scala.collection.mutable.CheckedIndexedSeqView$Drop"), + ProblemFilters.exclude[MissingClassProblem]("scala.collection.mutable.CheckedIndexedSeqView$DropRight"), + ProblemFilters.exclude[MissingClassProblem](s"scala.collection.mutable.CheckedIndexedSeqView$$Map"), + ProblemFilters.exclude[MissingClassProblem]("scala.collection.mutable.CheckedIndexedSeqView$Reverse"), + ProblemFilters.exclude[MissingClassProblem]("scala.collection.mutable.CheckedIndexedSeqView$Slice"), ) override val buildSettings = Seq( diff --git a/src/library/scala/collection/IndexedSeq.scala b/src/library/scala/collection/IndexedSeq.scala index 18b66b710b07..65a30efe4030 100644 --- a/src/library/scala/collection/IndexedSeq.scala +++ b/src/library/scala/collection/IndexedSeq.scala @@ -47,15 +47,7 @@ trait IndexedSeqOps[+A, +CC[_], +C] extends Any with SeqOps[A, CC, C] { self => s.asInstanceOf[S with EfficientSplit] } - override def reverseIterator: Iterator[A] = new AbstractIterator[A] { - private[this] var i = self.length - def hasNext: Boolean = 0 < i - def next(): A = - if (0 < i) { - i -= 1 - self(i) - } else Iterator.empty.next() - } + override def reverseIterator: Iterator[A] = view.reverseIterator override def foldRight[B](z: B)(op: (A, B) => B): B = { val it = reverseIterator diff --git a/src/library/scala/collection/IndexedSeqView.scala b/src/library/scala/collection/IndexedSeqView.scala index a1b3d4d5e32b..692486b1e088 100644 --- a/src/library/scala/collection/IndexedSeqView.scala +++ b/src/library/scala/collection/IndexedSeqView.scala @@ -49,14 +49,15 @@ trait IndexedSeqView[+A] extends IndexedSeqOps[A, View, View[A]] with SeqView[A] object IndexedSeqView { @SerialVersionUID(3L) - private final class IndexedSeqViewIterator[A](self: IndexedSeqView[A]) extends AbstractIterator[A] with Serializable { + private[collection] class IndexedSeqViewIterator[A](self: IndexedSeqView[A]) extends AbstractIterator[A] with Serializable { private[this] var current = 0 - private[this] var remainder = self.size + private[this] var remainder = self.length override def knownSize: Int = remainder - def hasNext = remainder > 0 + @inline private[this] def _hasNext: Boolean = remainder > 0 + def hasNext: Boolean = _hasNext def next(): A = - if (hasNext) { - val r = self.apply(current) + if (_hasNext) { + val r = self(current) current += 1 remainder -= 1 r @@ -82,18 +83,18 @@ object IndexedSeqView { } } @SerialVersionUID(3L) - private final class IndexedSeqViewReverseIterator[A](self: IndexedSeqView[A]) extends AbstractIterator[A] with Serializable { - private[this] var pos = self.size - 1 - private[this] var remainder = self.size - def hasNext: Boolean = remainder > 0 + private[collection] class IndexedSeqViewReverseIterator[A](self: IndexedSeqView[A]) extends AbstractIterator[A] with Serializable { + private[this] var pos = self.length - 1 + private[this] var remainder = self.length + @inline private[this] def _hasNext: Boolean = remainder > 0 + def hasNext: Boolean = _hasNext def next(): A = - if (pos < 0) throw new NoSuchElementException - else { + if (_hasNext) { val r = self(pos) pos -= 1 remainder -= 1 r - } + } else Iterator.empty.next() override def drop(n: Int): Iterator[A] = { if (n > 0) { @@ -103,7 +104,6 @@ object IndexedSeqView { this } - override def sliceIterator(from: Int, until: Int): Iterator[A] = { val startCutoff = pos val untilCutoff = startCutoff - remainder + 1 diff --git a/src/library/scala/collection/mutable/ArrayBuffer.scala b/src/library/scala/collection/mutable/ArrayBuffer.scala index ad7fe215d7a8..f67755db9e6f 100644 --- a/src/library/scala/collection/mutable/ArrayBuffer.scala +++ b/src/library/scala/collection/mutable/ArrayBuffer.scala @@ -39,6 +39,7 @@ import scala.util.chaining._ * @define mayNotTerminateInf * @define willNotTerminateInf */ +@SerialVersionUID(-1582447879429021880L) class ArrayBuffer[A] private (initialElements: Array[AnyRef], initialSize: Int) extends AbstractBuffer[A] with IndexedBuffer[A] @@ -51,6 +52,8 @@ class ArrayBuffer[A] private (initialElements: Array[AnyRef], initialSize: Int) def this(initialSize: Int) = this(new Array[AnyRef](initialSize max 1), 0) + @transient private[this] var mutationCount: Int = 0 + protected[collection] var array: Array[AnyRef] = initialElements protected var size0 = initialSize @@ -62,14 +65,17 @@ class ArrayBuffer[A] private (initialElements: Array[AnyRef], initialSize: Int) override def knownSize: Int = super[IndexedSeqOps].knownSize /** Ensure that the internal array has at least `n` cells. */ - protected def ensureSize(n: Int): Unit = + protected def ensureSize(n: Int): Unit = { + mutationCount += 1 array = ArrayBuffer.ensureSize(array, size0, n) + } def sizeHint(size: Int): Unit = if(size > length && size >= 1) ensureSize(size) /** Reduce length to `n`, nulling out all dropped elements */ private def reduceToSize(n: Int): Unit = { + mutationCount += 1 Arrays.fill(array, n, size0, null) size0 = n } @@ -79,7 +85,10 @@ class ArrayBuffer[A] private (initialElements: Array[AnyRef], initialSize: Int) * which may replace the array by a shorter one. * This allows releasing some unused memory. */ - def trimToSize(): Unit = resize(length) + def trimToSize(): Unit = { + mutationCount += 1 + resize(length) + } /** Trims the `array` buffer size down to either a power of 2 * or Int.MaxValue while keeping first `requiredLength` elements. @@ -99,12 +108,13 @@ class ArrayBuffer[A] private (initialElements: Array[AnyRef], initialSize: Int) def update(@deprecatedName("n", "2.13.0") index: Int, elem: A): Unit = { checkWithinBounds(index, index + 1) + mutationCount += 1 array(index) = elem.asInstanceOf[AnyRef] } def length = size0 - override def view: ArrayBufferView[A] = new ArrayBufferView(array, size0) + override def view: ArrayBufferView[A] = new ArrayBufferView(array, size0, () => mutationCount) override def iterableFactory: SeqFactory[ArrayBuffer] = ArrayBuffer @@ -136,9 +146,12 @@ class ArrayBuffer[A] private (initialElements: Array[AnyRef], initialSize: Int) override def addAll(elems: IterableOnce[A]): this.type = { elems match { case elems: ArrayBuffer[_] => - ensureSize(length + elems.length) - Array.copy(elems.array, 0, array, length, elems.length) - size0 = length + elems.length + val elemsLength = elems.size0 + if (elemsLength > 0) { + ensureSize(length + elemsLength) + Array.copy(elems.array, 0, array, length, elemsLength) + size0 = length + elemsLength + } case _ => super.addAll(elems) } this @@ -164,19 +177,21 @@ class ArrayBuffer[A] private (initialElements: Array[AnyRef], initialSize: Int) insertAll(index, ArrayBuffer.from(this)) case elems: collection.Iterable[A] => val elemsLength = elems.size - ensureSize(length + elemsLength) - Array.copy(array, index, array, index + elemsLength, size0 - index) - size0 = size0 + elemsLength - elems match { - case elems: ArrayBuffer[_] => - Array.copy(elems.array, 0, array, index, elemsLength) - case _ => - var i = 0 - val it = elems.iterator - while (i < elemsLength) { - this(index + i) = it.next() - i += 1 - } + if (elemsLength > 0) { + ensureSize(length + elemsLength) + Array.copy(array, index, array, index + elemsLength, size0 - index) + size0 = size0 + elemsLength + elems match { + case elems: ArrayBuffer[_] => + Array.copy(elems.array, 0, array, index, elemsLength) + case _ => + var i = 0 + val it = elems.iterator + while (i < elemsLength) { + this(index + i) = it.next() + i += 1 + } + } } case _ => insertAll(index, ArrayBuffer.from(elems)) @@ -232,7 +247,10 @@ class ArrayBuffer[A] private (initialElements: Array[AnyRef], initialSize: Int) * @return modified input $coll sorted according to the ordering `ord`. */ override def sortInPlace[B >: A]()(implicit ord: Ordering[B]): this.type = { - if (length > 1) scala.util.Sorting.stableSort(array.asInstanceOf[Array[B]], 0, length) + if (length > 1) { + mutationCount += 1 + scala.util.Sorting.stableSort(array.asInstanceOf[Array[B]], 0, length) + } this } } @@ -297,8 +315,36 @@ object ArrayBuffer extends StrictOptimizedSeqFactory[ArrayBuffer] { } } -final class ArrayBufferView[A](val array: Array[AnyRef], val length: Int) extends AbstractIndexedSeqView[A] { +final class ArrayBufferView[A] private[mutable](val array: Array[AnyRef], val length: Int, mutationCount: () => Int) + extends AbstractIndexedSeqView[A] { + @deprecated("never intended to be public; call ArrayBuffer#view instead", since = "2.13.4") + def this(array: Array[AnyRef], length: Int) = { + // this won't actually track mutation, but it would be a pain to have the implementation + // check if we have a method to get the current mutation count or not on every method and + // change what it does based on that. hopefully no one ever calls this. + this(array, length, () => 0) + } + @throws[ArrayIndexOutOfBoundsException] - def apply(n: Int) = if (n < length) array(n).asInstanceOf[A] else throw new IndexOutOfBoundsException(s"$n is out of bounds (min 0, max ${length - 1})") + def apply(n: Int): A = if (n < length) array(n).asInstanceOf[A] else throw new IndexOutOfBoundsException(s"$n is out of bounds (min 0, max ${length - 1})") override protected[this] def className = "ArrayBufferView" + + // we could inherit all these from `CheckedIndexedSeqView`, except this class is public + override def iterator: Iterator[A] = new CheckedIndexedSeqView.CheckedIterator(this, mutationCount()) + override def reverseIterator: Iterator[A] = new CheckedIndexedSeqView.CheckedReverseIterator(this, mutationCount()) + + override def appended[B >: A](elem: B): IndexedSeqView[B] = new CheckedIndexedSeqView.Appended(this, elem)(mutationCount) + override def prepended[B >: A](elem: B): IndexedSeqView[B] = new CheckedIndexedSeqView.Prepended(elem, this)(mutationCount) + override def take(n: Int): IndexedSeqView[A] = new CheckedIndexedSeqView.Take(this, n)(mutationCount) + override def takeRight(n: Int): IndexedSeqView[A] = new CheckedIndexedSeqView.TakeRight(this, n)(mutationCount) + override def drop(n: Int): IndexedSeqView[A] = new CheckedIndexedSeqView.Drop(this, n)(mutationCount) + override def dropRight(n: Int): IndexedSeqView[A] = new CheckedIndexedSeqView.DropRight(this, n)(mutationCount) + override def map[B](f: A => B): IndexedSeqView[B] = new CheckedIndexedSeqView.Map(this, f)(mutationCount) + override def reverse: IndexedSeqView[A] = new CheckedIndexedSeqView.Reverse(this)(mutationCount) + override def slice(from: Int, until: Int): IndexedSeqView[A] = new CheckedIndexedSeqView.Slice(this, from, until)(mutationCount) + override def tapEach[U](f: A => U): IndexedSeqView[A] = new CheckedIndexedSeqView.Map(this, { (a: A) => f(a); a})(mutationCount) + + override def concat[B >: A](suffix: IndexedSeqView.SomeIndexedSeqOps[B]): IndexedSeqView[B] = new CheckedIndexedSeqView.Concat(this, suffix)(mutationCount) + override def appendedAll[B >: A](suffix: IndexedSeqView.SomeIndexedSeqOps[B]): IndexedSeqView[B] = new CheckedIndexedSeqView.Concat(this, suffix)(mutationCount) + override def prependedAll[B >: A](prefix: IndexedSeqView.SomeIndexedSeqOps[B]): IndexedSeqView[B] = new CheckedIndexedSeqView.Concat(prefix, this)(mutationCount) } diff --git a/src/library/scala/collection/mutable/CheckedIndexedSeqView.scala b/src/library/scala/collection/mutable/CheckedIndexedSeqView.scala new file mode 100644 index 000000000000..ea26dcc05323 --- /dev/null +++ b/src/library/scala/collection/mutable/CheckedIndexedSeqView.scala @@ -0,0 +1,117 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala +package collection +package mutable + +private trait CheckedIndexedSeqView[+A] extends IndexedSeqView[A] { + protected val mutationCount: () => Int + + override def iterator: Iterator[A] = new CheckedIndexedSeqView.CheckedIterator(this, mutationCount()) + override def reverseIterator: Iterator[A] = new CheckedIndexedSeqView.CheckedReverseIterator(this, mutationCount()) + + override def appended[B >: A](elem: B): IndexedSeqView[B] = new CheckedIndexedSeqView.Appended(this, elem)(mutationCount) + override def prepended[B >: A](elem: B): IndexedSeqView[B] = new CheckedIndexedSeqView.Prepended(elem, this)(mutationCount) + override def take(n: Int): IndexedSeqView[A] = new CheckedIndexedSeqView.Take(this, n)(mutationCount) + override def takeRight(n: Int): IndexedSeqView[A] = new CheckedIndexedSeqView.TakeRight(this, n)(mutationCount) + override def drop(n: Int): IndexedSeqView[A] = new CheckedIndexedSeqView.Drop(this, n)(mutationCount) + override def dropRight(n: Int): IndexedSeqView[A] = new CheckedIndexedSeqView.DropRight(this, n)(mutationCount) + override def map[B](f: A => B): IndexedSeqView[B] = new CheckedIndexedSeqView.Map(this, f)(mutationCount) + override def reverse: IndexedSeqView[A] = new CheckedIndexedSeqView.Reverse(this)(mutationCount) + override def slice(from: Int, until: Int): IndexedSeqView[A] = new CheckedIndexedSeqView.Slice(this, from, until)(mutationCount) + override def tapEach[U](f: A => U): IndexedSeqView[A] = new CheckedIndexedSeqView.Map(this, { (a: A) => f(a); a})(mutationCount) + + override def concat[B >: A](suffix: IndexedSeqView.SomeIndexedSeqOps[B]): IndexedSeqView[B] = new CheckedIndexedSeqView.Concat(this, suffix)(mutationCount) + override def appendedAll[B >: A](suffix: IndexedSeqView.SomeIndexedSeqOps[B]): IndexedSeqView[B] = new CheckedIndexedSeqView.Concat(this, suffix)(mutationCount) + override def prependedAll[B >: A](prefix: IndexedSeqView.SomeIndexedSeqOps[B]): IndexedSeqView[B] = new CheckedIndexedSeqView.Concat(prefix, this)(mutationCount) +} + +private object CheckedIndexedSeqView { + import IndexedSeqView.SomeIndexedSeqOps + + @SerialVersionUID(3L) + private[mutable] class CheckedIterator[A](self: IndexedSeqView[A], mutationCount: => Int) + extends IndexedSeqView.IndexedSeqViewIterator[A](self) { + private[this] val expectedCount = mutationCount + override def hasNext: Boolean = { + MutationTracker.checkMutationsForIteration(expectedCount, mutationCount) + super.hasNext + } + } + + @SerialVersionUID(3L) + private[mutable] class CheckedReverseIterator[A](self: IndexedSeqView[A], mutationCount: => Int) + extends IndexedSeqView.IndexedSeqViewReverseIterator[A](self) { + private[this] val expectedCount = mutationCount + override def hasNext: Boolean = { + MutationTracker.checkMutationsForIteration(expectedCount, mutationCount) + super.hasNext + } + } + + @SerialVersionUID(3L) + class Id[+A](underlying: SomeIndexedSeqOps[A])(protected val mutationCount: () => Int) + extends IndexedSeqView.Id(underlying) with CheckedIndexedSeqView[A] + + @SerialVersionUID(3L) + class Appended[+A](underlying: SomeIndexedSeqOps[A], elem: A)(protected val mutationCount: () => Int) + extends IndexedSeqView.Appended(underlying, elem) with CheckedIndexedSeqView[A] + + @SerialVersionUID(3L) + class Prepended[+A](elem: A, underlying: SomeIndexedSeqOps[A])(protected val mutationCount: () => Int) + extends IndexedSeqView.Prepended(elem, underlying) with CheckedIndexedSeqView[A] + + @SerialVersionUID(3L) + class Concat[A](prefix: SomeIndexedSeqOps[A], suffix: SomeIndexedSeqOps[A])(protected val mutationCount: () => Int) + extends IndexedSeqView.Concat[A](prefix, suffix) with CheckedIndexedSeqView[A] + + @SerialVersionUID(3L) + class Take[A](underlying: SomeIndexedSeqOps[A], n: Int)(protected val mutationCount: () => Int) + extends IndexedSeqView.Take(underlying, n) with CheckedIndexedSeqView[A] + + @SerialVersionUID(3L) + class TakeRight[A](underlying: SomeIndexedSeqOps[A], n: Int)(protected val mutationCount: () => Int) + extends IndexedSeqView.TakeRight(underlying, n) with CheckedIndexedSeqView[A] + + @SerialVersionUID(3L) + class Drop[A](underlying: SomeIndexedSeqOps[A], n: Int)(protected val mutationCount: () => Int) + extends IndexedSeqView.Drop[A](underlying, n) with CheckedIndexedSeqView[A] + + @SerialVersionUID(3L) + class DropRight[A](underlying: SomeIndexedSeqOps[A], n: Int)(protected val mutationCount: () => Int) + extends IndexedSeqView.DropRight[A](underlying, n) with CheckedIndexedSeqView[A] + + @SerialVersionUID(3L) + class Map[A, B](underlying: SomeIndexedSeqOps[A], f: A => B)(protected val mutationCount: () => Int) + extends IndexedSeqView.Map(underlying, f) with CheckedIndexedSeqView[B] + + @SerialVersionUID(3L) + class Reverse[A](underlying: SomeIndexedSeqOps[A])(protected val mutationCount: () => Int) + extends IndexedSeqView.Reverse[A](underlying) with CheckedIndexedSeqView[A] { + override def reverse: IndexedSeqView[A] = underlying match { + case x: IndexedSeqView[A] => x + case _ => super.reverse + } + } + + @SerialVersionUID(3L) + class Slice[A](underlying: SomeIndexedSeqOps[A], from: Int, until: Int)(protected val mutationCount: () => Int) + extends AbstractIndexedSeqView[A] with CheckedIndexedSeqView[A] { + protected val lo = from max 0 + protected val hi = (until max 0) min underlying.length + protected val len = (hi - lo) max 0 + @throws[IndexOutOfBoundsException] + def apply(i: Int): A = underlying(lo + i) + def length: Int = len + } +} diff --git a/test/junit/scala/collection/mutable/MutationTrackingTest.scala b/test/junit/scala/collection/mutable/MutationTrackingTest.scala index 9ff9511320e3..c5a03270f01a 100644 --- a/test/junit/scala/collection/mutable/MutationTrackingTest.scala +++ b/test/junit/scala/collection/mutable/MutationTrackingTest.scala @@ -18,34 +18,40 @@ import java.util.ConcurrentModificationException import org.junit.Test import scala.annotation.nowarn +import scala.annotation.unchecked.{uncheckedVariance => uV} import scala.tools.testkit.AssertUtil.assertThrows abstract class MutationTrackingTest[+C <: Iterable[_]](factory: Factory[Int, C]) { - private def runOp(op: C => Any, viewOrIterator: C => IterableOnceOps[_, AnyConstr, _]): Unit = { - val coll = (factory.newBuilder += 1 += 2 += 3 += 4).result() + private[this] type VoI = C => IterableOnceOps[_, AnyConstr, _] + // if you do bad things with this by returning a different builder, it WILL bite you + protected[this] type BuildSequence = Builder[Int, C @uV] => Builder[Int, C @uV] + protected[this] val defaultBuildSequence: BuildSequence = _ += 1 += 2 += 3 += 4 + + private[this] def runOp(op: C => Any, bs: BuildSequence, viewOrIterator: VoI): Unit = { + val coll = bs(factory.newBuilder).result() val it = viewOrIterator(coll) op(coll) it.foreach(_ => ()) } - private def runOpMaybeThrowing(op: C => Any, - throws: Boolean, - viewOrIterator: C => IterableOnceOps[_, AnyConstr, _]): Unit = { - if (throws) assertThrows[ConcurrentModificationException](runOp(op, viewOrIterator), _ contains "iteration") - else runOp(op, viewOrIterator) + private[this] def runOpMaybeThrowing(op: C => Any, bs: BuildSequence, throws: Boolean, viewOrIterator: VoI): Unit = { + if (throws) assertThrows[ConcurrentModificationException](runOp(op, bs, viewOrIterator), _ contains "iteration") + else runOp(op, bs, viewOrIterator) } - private def runOpForViewAndIterator(op: C => Any, throws: Boolean): Unit = { - runOp(op, _.view) // never throws - runOpMaybeThrowing(op, throws, _.iterator) - runOpMaybeThrowing(op, throws, _.view.iterator) + private[this] def runOpForViewAndIterator(op: C => Any, bs: BuildSequence, throws: Boolean): Unit = { + runOp(op, bs, _.view) // never throws + runOpMaybeThrowing(op, bs, throws, _.iterator) + runOpMaybeThrowing(op, bs, throws, _.view.iterator) } /** Checks that no exception is thrown by an operation. */ - def checkFine(op: C => Any): Unit = runOpForViewAndIterator(op, throws = false) + protected[this] def checkFine(op: C => Any, buildSequence: BuildSequence = defaultBuildSequence): Unit = + runOpForViewAndIterator(op, buildSequence, throws = false) /** Checks that an exception is thrown by an operation. */ - def checkThrows(op: C => Any): Unit = runOpForViewAndIterator(op, throws = true) + protected[this] def checkThrows(op: C => Any, buildSequence: BuildSequence = defaultBuildSequence): Unit = + runOpForViewAndIterator(op, buildSequence, throws = true) @Test def nop(): Unit = checkFine { _ => () } @@ -94,6 +100,29 @@ object MutationTrackingTest { def transform(): Unit = checkThrows { _.transform(_ + 1) } } + trait IndexedSeqTest { self: MutationTrackingTest[IndexedSeq[Int]] => + @Test + def mapInPlace(): Unit = checkThrows { _.mapInPlace(_ + 1) } + + @Test + def sortInPlace(): Unit = { + checkThrows { _.sortInPlace() } + checkFine (_.sortInPlace(), _ += 1) + } + + @Test + def sortInPlaceWith(): Unit = { + checkThrows { _.sortInPlaceWith(_ > _) } + checkFine (_.sortInPlaceWith(_ > _), _ += 1) + } + + @Test + def sortInPlaceBy(): Unit = { + checkThrows { _.sortInPlaceBy(_ * -1) } + checkFine (_.sortInPlaceBy(_ * -1), _ += 1) + } + } + trait BufferTest extends GrowableTest with ShrinkableTest with SeqTest { self: MutationTrackingTest[Buffer[Int]] => @Test def insert(): Unit = checkThrows { _.insert(0, 5) } @@ -210,4 +239,15 @@ package MutationTrackingTestImpl { @Test def filterInPlace(): Unit = checkThrows { _.filterInPlace(_ => true) } } + + class ArrayBufferTest extends MutationTrackingTest(ArrayBuffer) with BufferTest with IndexedSeqTest { + @Test + def clearAndShrink(): Unit = checkThrows { _ clearAndShrink 2 } + + @Test + def trimToSize(): Unit = checkThrows { _.trimToSize() } + + @Test + def sizeHint(): Unit = checkThrows { _ sizeHint 16 } + } }