diff options
10 files changed, 316 insertions, 95 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt b/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt index 25effec42f1a..db446c39767a 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt +++ b/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt @@ -19,13 +19,14 @@ package com.android.systemui.log import android.os.Trace import android.util.Log import com.android.systemui.log.dagger.LogModule +import com.android.systemui.util.collection.RingBuffer import java.io.PrintWriter import java.text.SimpleDateFormat -import java.util.ArrayDeque import java.util.Locale import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.BlockingQueue import kotlin.concurrent.thread +import kotlin.math.max /** * A simple ring buffer of recyclable log messages @@ -63,29 +64,22 @@ import kotlin.concurrent.thread * * Buffers are provided by [LogModule]. Instances should be created using a [LogBufferFactory]. * - * @param name The name of this buffer - * @param maxLogs The maximum number of messages to keep in memory at any one time, including the - * unused pool. Must be >= [poolSize]. - * @param poolSize The maximum amount that the size of the buffer is allowed to flex in response to - * sequential calls to [document] that aren't immediately followed by a matching call to [push]. + * @param name The name of this buffer, printed when the buffer is dumped and in some other + * situations. + * @param maxSize The maximum number of messages to keep in memory at any one time. Buffers start + * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches + * the maximum, it behaves like a ring buffer. */ class LogBuffer @JvmOverloads constructor( private val name: String, - private val maxLogs: Int, - private val poolSize: Int, + private val maxSize: Int, private val logcatEchoTracker: LogcatEchoTracker, private val systrace: Boolean = true ) { - init { - if (maxLogs < poolSize) { - throw IllegalArgumentException("maxLogs must be greater than or equal to poolSize, " + - "but maxLogs=$maxLogs < $poolSize=poolSize") - } - } + private val buffer = RingBuffer(maxSize) { LogMessageImpl.create() } - private val buffer: ArrayDeque<LogMessageImpl> = ArrayDeque() - private val echoMessageQueue: BlockingQueue<LogMessageImpl>? = - if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(poolSize) else null + private val echoMessageQueue: BlockingQueue<LogMessage>? = + if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null init { if (logcatEchoTracker.logInBackgroundThread && echoMessageQueue != null) { @@ -104,6 +98,9 @@ class LogBuffer @JvmOverloads constructor( var frozen = false private set + private val mutable + get() = !frozen && maxSize > 0 + /** * Logs a message to the log buffer * @@ -138,34 +135,19 @@ class LogBuffer @JvmOverloads constructor( initializer: LogMessage.() -> Unit, noinline printer: LogMessage.() -> String ) { - if (!frozen) { - val message = obtain(tag, level, printer) - initializer(message) - push(message) - } - } - - /** - * Same as [log], but doesn't push the message to the buffer. Useful if you need to supply a - * "reason" for doing something (the thing you supply the reason to will presumably call [push] - * on that message at some point). - */ - inline fun document( - tag: String, - level: LogLevel, - initializer: LogMessage.() -> Unit, - noinline printer: LogMessage.() -> String - ): LogMessage { val message = obtain(tag, level, printer) initializer(message) - return message + commit(message) } /** - * Obtains an instance of [LogMessageImpl], usually from the object pool. If the pool has been - * exhausted, creates a new instance. + * You should call [log] instead of this method. * - * In general, you should call [log] or [document] instead of this method. + * Obtains the next [LogMessage] from the ring buffer. If the buffer is not yet at max size, + * grows the buffer by one. + * + * After calling [obtain], the message will now be at the end of the buffer. The caller must + * store any relevant data on the message and then call [commit]. */ @Synchronized fun obtain( @@ -173,28 +155,26 @@ class LogBuffer @JvmOverloads constructor( level: LogLevel, printer: (LogMessage) -> String ): LogMessageImpl { - val message = when { - frozen -> LogMessageImpl.create() - buffer.size > maxLogs - poolSize -> buffer.removeFirst() - else -> LogMessageImpl.create() + if (!mutable) { + return FROZEN_MESSAGE } + val message = buffer.advance() message.reset(tag, level, System.currentTimeMillis(), printer) return message } /** - * Pushes a message into buffer, possibly evicting an older message if the buffer is full. + * You should call [log] instead of this method. + * + * After acquiring a message via [obtain], call this method to signal to the buffer that you + * have finished filling in its data fields. The message will be echoed to logcat if + * necessary. */ @Synchronized - fun push(message: LogMessage) { - if (frozen) { + fun commit(message: LogMessage) { + if (!mutable) { return } - if (buffer.size == maxLogs) { - Log.e(TAG, "LogBuffer $name has exceeded its pool size") - buffer.removeFirst() - } - buffer.add(message as LogMessageImpl) // Log in the background thread only if echoMessageQueue exists and has capacity (checking // capacity avoids the possibility of blocking this thread) if (echoMessageQueue != null && echoMessageQueue.remainingCapacity() > 0) { @@ -210,7 +190,7 @@ class LogBuffer @JvmOverloads constructor( } /** Sends message to echo after determining whether to use Logcat and/or systrace. */ - private fun echoToDesiredEndpoints(message: LogMessageImpl) { + private fun echoToDesiredEndpoints(message: LogMessage) { val includeInLogcat = logcatEchoTracker.isBufferLoggable(name, message.level) || logcatEchoTracker.isTagLoggable(message.tag, message.level) echo(message, toLogcat = includeInLogcat, toSystrace = systrace) @@ -219,19 +199,17 @@ class LogBuffer @JvmOverloads constructor( /** Converts the entire buffer to a newline-delimited string */ @Synchronized fun dump(pw: PrintWriter, tailLength: Int) { - val start = if (tailLength <= 0) { 0 } else { buffer.size - tailLength } + val iterationStart = if (tailLength <= 0) { 0 } else { max(0, buffer.size - tailLength) } - for ((i, message) in buffer.withIndex()) { - if (i >= start) { - dumpMessage(message, pw) - } + for (i in iterationStart until buffer.size) { + dumpMessage(buffer[i], pw) } } /** - * "Freezes" the contents of the buffer, making them immutable until [unfreeze] is called. - * Calls to [log], [document], [obtain], and [push] will not affect the buffer and will return - * dummy values if necessary. + * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called. + * Calls to [log], [obtain], and [commit] will not affect the buffer and will return dummy + * values if necessary. */ @Synchronized fun freeze() { @@ -293,3 +271,4 @@ class LogBuffer @JvmOverloads constructor( private const val TAG = "LogBuffer" private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) +private val FROZEN_MESSAGE = LogMessageImpl.create()
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt index cbfca25e0c60..5651399cb891 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt @@ -27,22 +27,21 @@ class LogBufferFactory @Inject constructor( private val logcatEchoTracker: LogcatEchoTracker ) { /* limit the size of maxPoolSize for low ram (Go) devices */ - private fun poolLimit(maxPoolSize_requested: Int): Int { - if (ActivityManager.isLowRamDeviceStatic()) { - return minOf(maxPoolSize_requested, 20) /* low ram max log size*/ + private fun adjustMaxSize(requestedMaxSize: Int): Int { + return if (ActivityManager.isLowRamDeviceStatic()) { + minOf(requestedMaxSize, 20) /* low ram max log size*/ } else { - return maxPoolSize_requested + requestedMaxSize } } @JvmOverloads fun create( name: String, - maxPoolSize: Int, - flexSize: Int = 10, + maxSize: Int, systrace: Boolean = true ): LogBuffer { - val buffer = LogBuffer(name, poolLimit(maxPoolSize), flexSize, logcatEchoTracker, systrace) + val buffer = LogBuffer(name, adjustMaxSize(maxSize), logcatEchoTracker, systrace) dumpManager.registerBuffer(name, buffer) return buffer } diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt b/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt index 91734cc361b0..40b0cdc173d8 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt +++ b/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt @@ -60,7 +60,7 @@ class LogcatEchoTrackerDebug private constructor( Settings.Global.getUriFor(BUFFER_PATH), true, object : ContentObserver(Handler(mainLooper)) { - override fun onChange(selfChange: Boolean, uri: Uri) { + override fun onChange(selfChange: Boolean, uri: Uri?) { super.onChange(selfChange, uri) cachedBufferLevels.clear() } @@ -70,7 +70,7 @@ class LogcatEchoTrackerDebug private constructor( Settings.Global.getUriFor(TAG_PATH), true, object : ContentObserver(Handler(mainLooper)) { - override fun onChange(selfChange: Boolean, uri: Uri) { + override fun onChange(selfChange: Boolean, uri: Uri?) { super.onChange(selfChange, uri) cachedTagLevels.clear() } @@ -110,7 +110,7 @@ class LogcatEchoTrackerDebug private constructor( } private fun parseProp(propValue: String?): LogLevel { - return when (propValue?.toLowerCase()) { + return when (propValue?.lowercase()) { "verbose" -> LogLevel.VERBOSE "v" -> LogLevel.VERBOSE "debug" -> LogLevel.DEBUG diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 1e7a292dadba..eff025f771a5 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -49,8 +49,7 @@ public class LogModule { @SysUISingleton @NotificationLog public static LogBuffer provideNotificationsLogBuffer(LogBufferFactory factory) { - return factory.create("NotifLog", 1000 /* maxPoolSize */, - 10 /* flexSize */, false /* systrace */); + return factory.create("NotifLog", 1000 /* maxSize */, false /* systrace */); } /** Provides a logging buffer for all logs related to the data layer of notifications. */ @@ -74,8 +73,7 @@ public class LogModule { @SysUISingleton @NotificationSectionLog public static LogBuffer provideNotificationSectionLogBuffer(LogBufferFactory factory) { - return factory.create("NotifSectionLog", 1000 /* maxPoolSize */, - 10 /* flexSize */, false /* systrace */); + return factory.create("NotifSectionLog", 1000 /* maxSize */, false /* systrace */); } /** Provides a logging buffer for all logs related to the data layer of notifications. */ @@ -91,8 +89,7 @@ public class LogModule { @SysUISingleton @QSLog public static LogBuffer provideQuickSettingsLogBuffer(LogBufferFactory factory) { - return factory.create("QSLog", 500 /* maxPoolSize */, - 10 /* flexSize */, false /* systrace */); + return factory.create("QSLog", 500 /* maxSize */, false /* systrace */); } /** Provides a logging buffer for {@link com.android.systemui.broadcast.BroadcastDispatcher} */ @@ -100,8 +97,8 @@ public class LogModule { @SysUISingleton @BroadcastDispatcherLog public static LogBuffer provideBroadcastDispatcherLogBuffer(LogBufferFactory factory) { - return factory.create("BroadcastDispatcherLog", 500 /* maxPoolSize */, - 10 /* flexSize */, false /* systrace */); + return factory.create("BroadcastDispatcherLog", 500 /* maxSize */, + false /* systrace */); } /** Provides a logging buffer for all logs related to Toasts shown by SystemUI. */ @@ -139,8 +136,8 @@ public class LogModule { @SysUISingleton @QSFragmentDisableLog public static LogBuffer provideQSFragmentDisableLogBuffer(LogBufferFactory factory) { - return factory.create("QSFragmentDisableFlagsLog", 10 /* maxPoolSize */, - 10 /* flexSize */, false /* systrace */); + return factory.create("QSFragmentDisableFlagsLog", 10 /* maxSize */, + false /* systrace */); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NPVCDownEventState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NPVCDownEventState.kt index 06235071734e..d44a56942065 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NPVCDownEventState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NPVCDownEventState.kt @@ -14,9 +14,9 @@ package com.android.systemui.statusbar.phone import android.view.MotionEvent -import com.android.internal.util.RingBuffer import com.android.systemui.dump.DumpsysTableLogger import com.android.systemui.dump.Row +import com.android.systemui.util.collection.RingBuffer import java.text.SimpleDateFormat import java.util.Locale @@ -67,16 +67,9 @@ class NPVCDownEventState private constructor( * Do not use [append] to add new elements. Instead use [insert], as it will recycle if * necessary. */ - class Buffer( - capacity: Int - ) : RingBuffer<NPVCDownEventState>(NPVCDownEventState::class.java, capacity) { - override fun append(t: NPVCDownEventState?) { - throw UnsupportedOperationException("Not supported, use insert instead") - } + class Buffer(capacity: Int) { - override fun createNewItem(): NPVCDownEventState { - return NPVCDownEventState() - } + private val buffer = RingBuffer(capacity) { NPVCDownEventState() } /** * Insert a new element in the buffer. @@ -94,7 +87,7 @@ class NPVCDownEventState private constructor( touchSlopExceededBeforeDown: Boolean, lastEventSynthesized: Boolean ) { - nextSlot.apply { + buffer.advance().apply { this.timeStamp = timeStamp this.x = x this.y = y @@ -115,7 +108,7 @@ class NPVCDownEventState private constructor( * @see NPVCDownEventState.asStringList */ fun toList(): List<Row> { - return toArray().map { it.asStringList } + return buffer.asSequence().map { it.asStringList }.toList() } } diff --git a/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt b/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt new file mode 100644 index 000000000000..97dc842ec699 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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. + */ + +package com.android.systemui.util.collection + +import kotlin.math.max + +/** + * A simple ring buffer implementation + * + * Use [advance] to get the least recent item in the buffer (and then presumably fill it with + * appropriate data). This will cause it to become the most recent item. + * + * As the buffer is used, it will grow, allocating new instances of T using [factory] until it + * reaches [maxSize]. After this point, no new instances will be created. Instead, the "oldest" + * instances will be recycled from the back of the buffer and placed at the front. + * + * @param maxSize The maximum size the buffer can grow to before it begins functioning as a ring. + * @param factory A function that creates a fresh instance of T. Used by the buffer while it's + * growing to [maxSize]. + */ +class RingBuffer<T>( + private val maxSize: Int, + private val factory: () -> T +) : Iterable<T> { + + private val buffer = MutableList<T?>(maxSize) { null } + + /** + * An abstract representation that points to the "end" of the buffer. Increments every time + * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into + * the backing array. Always points to the "next" available slot in the buffer. Before the + * buffer has completely filled, the value pointed to will be null. Afterward, it will be the + * value at the "beginning" of the buffer. + * + * This value is unlikely to overflow. Assuming [advance] is called at rate of 100 calls/ms, + * omega will overflow after a little under three million years of continuous operation. + */ + private var omega: Long = 0 + + /** + * The number of items currently stored in the buffer. Calls to [advance] will cause this value + * to increase by one until it reaches [maxSize]. + */ + val size: Int + get() = if (omega < maxSize) omega.toInt() else maxSize + + /** + * Advances the buffer's position by one and returns the value that is now present at the "end" + * of the buffer. If the buffer is not yet full, uses [factory] to create a new item. + * Otherwise, reuses the value that was previously at the "beginning" of the buffer. + * + * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that + * was previously stored on it. + */ + fun advance(): T { + val index = indexOf(omega) + omega += 1 + val entry = buffer[index] ?: factory().also { + buffer[index] = it + } + return entry + } + + /** + * Returns the value stored at [index], which can range from 0 (the "start", or oldest element + * of the buffer) to [size] - 1 (the "end", or newest element of the buffer). + */ + operator fun get(index: Int): T { + if (index < 0 || index >= size) { + throw IndexOutOfBoundsException("Index $index is out of bounds") + } + + // If omega is larger than the maxSize, then the buffer is full, and omega is equivalent + // to the "start" of the buffer. If omega is smaller than the maxSize, then the buffer is + // not yet full and our start should be 0. However, in modspace, maxSize and 0 are + // equivalent, so we can get away with using it as the start value instead. + val start = max(omega, maxSize.toLong()) + + return buffer[indexOf(start + index)]!! + } + + inline fun forEach(action: (T) -> Unit) { + for (i in 0 until size) { + action(get(i)) + } + } + + override fun iterator(): Iterator<T> { + return object : Iterator<T> { + private var position: Int = 0 + + override fun next(): T { + if (position >= size) { + throw NoSuchElementException() + } + return get(position).also { position += 1 } + } + + override fun hasNext(): Boolean { + return position < size + } + } + } + + private fun indexOf(position: Long): Int { + return (position % maxSize).toInt() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt index 0720bdb06556..bd029a727ee3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt @@ -24,7 +24,7 @@ import com.android.systemui.log.LogcatEchoTracker * Creates a LogBuffer that will echo everything to logcat, which is useful for debugging tests. */ fun logcatLogBuffer(name: String = "EchoToLogcatLogBuffer") = - LogBuffer(name, 50, 50, LogcatEchoTrackerAlways()) + LogBuffer(name, 50, LogcatEchoTrackerAlways()) /** * A [LogcatEchoTracker] that always allows echoing to the logcat. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt index 6971c63ed6d4..8cb530c355bd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt @@ -30,7 +30,7 @@ class LSShadeTransitionLoggerTest : SysuiTestCase() { @Before fun setup() { logger = LSShadeTransitionLogger( - LogBuffer("Test", 10, 10, mock()), + LogBuffer("Test", 10, mock()), gestureLogger, displayMetrics) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index 98397fb01b6b..6abc687f0ebb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -379,7 +379,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { mCommandQueue, mCarrierConfigTracker, new CollapsedStatusBarFragmentLogger( - new LogBuffer("TEST", 1, 1, mock(LogcatEchoTracker.class)), + new LogBuffer("TEST", 1, mock(LogcatEchoTracker.class)), new DisableFlagsLogger() ), mOperatorNameViewControllerFactory, diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt new file mode 100644 index 000000000000..5e09b81da4e8 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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. + */ + +package com.android.systemui.util.collection + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertSame +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class RingBufferTest : SysuiTestCase() { + + private val buffer = RingBuffer(5) { TestElement() } + + private val history = mutableListOf<TestElement>() + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun testBarelyFillBuffer() { + fillBuffer(5) + + assertEquals(0, buffer[0].id) + assertEquals(1, buffer[1].id) + assertEquals(2, buffer[2].id) + assertEquals(3, buffer[3].id) + assertEquals(4, buffer[4].id) + } + + @Test + fun testPartiallyFillBuffer() { + fillBuffer(3) + + assertEquals(3, buffer.size) + + assertEquals(0, buffer[0].id) + assertEquals(1, buffer[1].id) + assertEquals(2, buffer[2].id) + + assertThrows(IndexOutOfBoundsException::class.java) { buffer[3] } + assertThrows(IndexOutOfBoundsException::class.java) { buffer[4] } + } + + @Test + fun testSpinBuffer() { + fillBuffer(277) + + assertEquals(272, buffer[0].id) + assertEquals(273, buffer[1].id) + assertEquals(274, buffer[2].id) + assertEquals(275, buffer[3].id) + assertEquals(276, buffer[4].id) + assertThrows(IndexOutOfBoundsException::class.java) { buffer[5] } + + assertEquals(5, buffer.size) + } + + @Test + fun testElementsAreRecycled() { + fillBuffer(23) + + assertSame(history[4], buffer[1]) + assertSame(history[9], buffer[1]) + assertSame(history[14], buffer[1]) + assertSame(history[19], buffer[1]) + } + + @Test + fun testIterator() { + fillBuffer(13) + + val iterator = buffer.iterator() + + for (i in 0 until 5) { + assertEquals(history[8 + i], iterator.next()) + } + assertFalse(iterator.hasNext()) + assertThrows(NoSuchElementException::class.java) { iterator.next() } + } + + @Test + fun testForEach() { + fillBuffer(13) + var i = 8 + + buffer.forEach { + assertEquals(history[i], it) + i++ + } + assertEquals(13, i) + } + + private fun fillBuffer(count: Int) { + for (i in 0 until count) { + val elem = buffer.advance() + elem.id = history.size + history.add(elem) + } + } +} + +private class TestElement(var id: Int = 0) { + override fun toString(): String { + return "{TestElement $id}" + } +}
\ No newline at end of file |