Merge "[Table Logging] Add tests for `logDiffsForTable`." into tm-qpr-dev
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/LogDiffsForTableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/LogDiffsForTableTest.kt
new file mode 100644
index 0000000..3b5e6b9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/LogDiffsForTableTest.kt
@@ -0,0 +1,1081 @@
+/*
+ * 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.log.table
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import java.io.StringWriter
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class LogDiffsForTableTest : SysuiTestCase() {
+
+ private val testScope = TestScope(UnconfinedTestDispatcher())
+
+ private lateinit var systemClock: FakeSystemClock
+ private lateinit var tableLogBuffer: TableLogBuffer
+
+ @Before
+ fun setUp() {
+ systemClock = FakeSystemClock()
+ tableLogBuffer = TableLogBuffer(MAX_SIZE, BUFFER_NAME, systemClock)
+ }
+
+ // ---- Flow<Boolean> tests ----
+
+ @Test
+ fun boolean_doesNotLogWhenNotCollected() {
+ val flow = flowOf(true, true, false)
+
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ COLUMN_NAME,
+ initialValue = false,
+ )
+
+ val logs = dumpLog()
+ assertThat(logs).doesNotContain(COLUMN_PREFIX)
+ assertThat(logs).doesNotContain(COLUMN_NAME)
+ assertThat(logs).doesNotContain("false")
+ }
+
+ @Test
+ fun boolean_logsInitialWhenCollected() =
+ testScope.runTest {
+ val flow = flowOf(true, true, false)
+
+ val flowWithLogging =
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ COLUMN_NAME,
+ initialValue = false,
+ )
+
+ systemClock.setCurrentTimeMillis(3000L)
+ val job = launch { flowWithLogging.collect() }
+
+ val logs = dumpLog()
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(3000L) +
+ SEPARATOR +
+ FULL_NAME +
+ SEPARATOR +
+ "false"
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun boolean_logsUpdates() =
+ testScope.runTest {
+ systemClock.setCurrentTimeMillis(100L)
+ val flow = flow {
+ for (bool in listOf(true, false, true)) {
+ systemClock.advanceTime(100L)
+ emit(bool)
+ }
+ }
+
+ val flowWithLogging =
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ COLUMN_NAME,
+ initialValue = false,
+ )
+
+ val job = launch { flowWithLogging.collect() }
+
+ val logs = dumpLog()
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(100L) + SEPARATOR + FULL_NAME + SEPARATOR + "false"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "true"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "false"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(400L) + SEPARATOR + FULL_NAME + SEPARATOR + "true"
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun boolean_doesNotLogIfSameValue() =
+ testScope.runTest {
+ systemClock.setCurrentTimeMillis(100L)
+ val flow = flow {
+ for (bool in listOf(true, true, false, false, true)) {
+ systemClock.advanceTime(100L)
+ emit(bool)
+ }
+ }
+
+ val flowWithLogging =
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ COLUMN_NAME,
+ initialValue = true,
+ )
+
+ val job = launch { flowWithLogging.collect() }
+
+ val logs = dumpLog()
+ // Input flow: true@100, true@200, true@300, false@400, false@500, true@600
+ // Output log: true@100, --------, --------, false@400, ---------, true@600
+ val expected1 =
+ TABLE_LOG_DATE_FORMAT.format(100L) + SEPARATOR + FULL_NAME + SEPARATOR + "true"
+ val expected4 =
+ TABLE_LOG_DATE_FORMAT.format(400L) + SEPARATOR + FULL_NAME + SEPARATOR + "false"
+ val expected6 =
+ TABLE_LOG_DATE_FORMAT.format(600L) + SEPARATOR + FULL_NAME + SEPARATOR + "true"
+ assertThat(logs).contains(expected1)
+ assertThat(logs).contains(expected4)
+ assertThat(logs).contains(expected6)
+
+ val unexpected2 =
+ TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "true"
+ val unexpected3 =
+ TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "true"
+ val unexpected5 =
+ TABLE_LOG_DATE_FORMAT.format(500L) + SEPARATOR + FULL_NAME + SEPARATOR + "false"
+ assertThat(logs).doesNotContain(unexpected2)
+ assertThat(logs).doesNotContain(unexpected3)
+ assertThat(logs).doesNotContain(unexpected5)
+
+ job.cancel()
+ }
+
+ @Test
+ fun boolean_worksForStateFlows() =
+ testScope.runTest {
+ val flow = MutableStateFlow(false)
+
+ val flowWithLogging =
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ COLUMN_NAME,
+ initialValue = false,
+ )
+
+ systemClock.setCurrentTimeMillis(50L)
+ val job = launch { flowWithLogging.collect() }
+ assertThat(dumpLog())
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(50L) + SEPARATOR + FULL_NAME + SEPARATOR + "false"
+ )
+
+ systemClock.setCurrentTimeMillis(100L)
+ flow.emit(true)
+ assertThat(dumpLog())
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(100L) + SEPARATOR + FULL_NAME + SEPARATOR + "true"
+ )
+
+ systemClock.setCurrentTimeMillis(200L)
+ flow.emit(false)
+ assertThat(dumpLog())
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "false"
+ )
+
+ // Doesn't log duplicates
+ systemClock.setCurrentTimeMillis(300L)
+ flow.emit(false)
+ assertThat(dumpLog())
+ .doesNotContain(
+ TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "false"
+ )
+
+ job.cancel()
+ }
+
+ // ---- Flow<Int> tests ----
+
+ @Test
+ fun int_doesNotLogWhenNotCollected() {
+ val flow = flowOf(5, 6, 7)
+
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ COLUMN_NAME,
+ initialValue = 1234,
+ )
+
+ val logs = dumpLog()
+ assertThat(logs).doesNotContain(COLUMN_PREFIX)
+ assertThat(logs).doesNotContain(COLUMN_NAME)
+ assertThat(logs).doesNotContain("1234")
+ }
+
+ @Test
+ fun int_logsInitialWhenCollected() =
+ testScope.runTest {
+ val flow = flowOf(5, 6, 7)
+
+ val flowWithLogging =
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ COLUMN_NAME,
+ initialValue = 1234,
+ )
+
+ systemClock.setCurrentTimeMillis(3000L)
+ val job = launch { flowWithLogging.collect() }
+
+ val logs = dumpLog()
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(3000L) + SEPARATOR + FULL_NAME + SEPARATOR + "1234"
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun int_logsUpdates() =
+ testScope.runTest {
+ systemClock.setCurrentTimeMillis(100L)
+ val flow = flow {
+ for (int in listOf(2, 3, 4)) {
+ systemClock.advanceTime(100L)
+ emit(int)
+ }
+ }
+
+ val flowWithLogging =
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ COLUMN_NAME,
+ initialValue = 1,
+ )
+
+ val job = launch { flowWithLogging.collect() }
+
+ val logs = dumpLog()
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(100L) + SEPARATOR + FULL_NAME + SEPARATOR + "1"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "2"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "3"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(400L) + SEPARATOR + FULL_NAME + SEPARATOR + "4"
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun int_doesNotLogIfSameValue() =
+ testScope.runTest {
+ systemClock.setCurrentTimeMillis(100L)
+ val flow = flow {
+ for (bool in listOf(2, 3, 3, 3, 2, 6, 6)) {
+ systemClock.advanceTime(100L)
+ emit(bool)
+ }
+ }
+
+ val flowWithLogging =
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ COLUMN_NAME,
+ initialValue = 1,
+ )
+
+ val job = launch { flowWithLogging.collect() }
+
+ val logs = dumpLog()
+ // Input flow: 1@100, 2@200, 3@300, 3@400, 3@500, 2@600, 6@700, 6@800
+ // Output log: 1@100, 2@200, 3@300, -----, -----, 2@600, 6@700, -----
+ val expected1 =
+ TABLE_LOG_DATE_FORMAT.format(100L) + SEPARATOR + FULL_NAME + SEPARATOR + "1"
+ val expected2 =
+ TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "2"
+ val expected3 =
+ TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "3"
+ val expected6 =
+ TABLE_LOG_DATE_FORMAT.format(600L) + SEPARATOR + FULL_NAME + SEPARATOR + "2"
+ val expected7 =
+ TABLE_LOG_DATE_FORMAT.format(700L) + SEPARATOR + FULL_NAME + SEPARATOR + "6"
+ assertThat(logs).contains(expected1)
+ assertThat(logs).contains(expected2)
+ assertThat(logs).contains(expected3)
+ assertThat(logs).contains(expected6)
+ assertThat(logs).contains(expected7)
+
+ val unexpected4 =
+ TABLE_LOG_DATE_FORMAT.format(400L) + SEPARATOR + FULL_NAME + SEPARATOR + "3"
+ val unexpected5 =
+ TABLE_LOG_DATE_FORMAT.format(500L) + SEPARATOR + FULL_NAME + SEPARATOR + "3"
+ val unexpected8 =
+ TABLE_LOG_DATE_FORMAT.format(800L) + SEPARATOR + FULL_NAME + SEPARATOR + "6"
+ assertThat(logs).doesNotContain(unexpected4)
+ assertThat(logs).doesNotContain(unexpected5)
+ assertThat(logs).doesNotContain(unexpected8)
+ job.cancel()
+ }
+
+ @Test
+ fun int_worksForStateFlows() =
+ testScope.runTest {
+ val flow = MutableStateFlow(1111)
+
+ val flowWithLogging =
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ COLUMN_NAME,
+ initialValue = 1111,
+ )
+
+ systemClock.setCurrentTimeMillis(50L)
+ val job = launch { flowWithLogging.collect() }
+ assertThat(dumpLog())
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(50L) + SEPARATOR + FULL_NAME + SEPARATOR + "1111"
+ )
+
+ systemClock.setCurrentTimeMillis(100L)
+ flow.emit(2222)
+ assertThat(dumpLog())
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(100L) + SEPARATOR + FULL_NAME + SEPARATOR + "2222"
+ )
+
+ systemClock.setCurrentTimeMillis(200L)
+ flow.emit(3333)
+ assertThat(dumpLog())
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "3333"
+ )
+
+ // Doesn't log duplicates
+ systemClock.setCurrentTimeMillis(300L)
+ flow.emit(3333)
+ assertThat(dumpLog())
+ .doesNotContain(
+ TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "3333"
+ )
+
+ job.cancel()
+ }
+
+ // ---- Flow<String> tests ----
+
+ @Test
+ fun string_doesNotLogWhenNotCollected() {
+ val flow = flowOf("val5", "val6", "val7")
+
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ COLUMN_NAME,
+ initialValue = "val1234",
+ )
+
+ val logs = dumpLog()
+ assertThat(logs).doesNotContain(COLUMN_PREFIX)
+ assertThat(logs).doesNotContain(COLUMN_NAME)
+ assertThat(logs).doesNotContain("val1234")
+ }
+
+ @Test
+ fun string_logsInitialWhenCollected() =
+ testScope.runTest {
+ val flow = flowOf("val5", "val6", "val7")
+
+ val flowWithLogging =
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ COLUMN_NAME,
+ initialValue = "val1234",
+ )
+
+ systemClock.setCurrentTimeMillis(3000L)
+ val job = launch { flowWithLogging.collect() }
+
+ val logs = dumpLog()
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(3000L) +
+ SEPARATOR +
+ FULL_NAME +
+ SEPARATOR +
+ "val1234"
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun string_logsUpdates() =
+ testScope.runTest {
+ systemClock.setCurrentTimeMillis(100L)
+ val flow = flow {
+ for (int in listOf("val2", "val3", "val4")) {
+ systemClock.advanceTime(100L)
+ emit(int)
+ }
+ }
+
+ val flowWithLogging =
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ COLUMN_NAME,
+ initialValue = "val1",
+ )
+
+ val job = launch { flowWithLogging.collect() }
+
+ val logs = dumpLog()
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(100L) + SEPARATOR + FULL_NAME + SEPARATOR + "val1"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "val2"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "val3"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(400L) + SEPARATOR + FULL_NAME + SEPARATOR + "val4"
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun string_logsNull() =
+ testScope.runTest {
+ systemClock.setCurrentTimeMillis(100L)
+ val flow = flow {
+ for (int in listOf(null, "something", null)) {
+ systemClock.advanceTime(100L)
+ emit(int)
+ }
+ }
+
+ val flowWithLogging =
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ COLUMN_NAME,
+ initialValue = "start",
+ )
+
+ val job = launch { flowWithLogging.collect() }
+
+ val logs = dumpLog()
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(100L) + SEPARATOR + FULL_NAME + SEPARATOR + "start"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "null"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(300L) +
+ SEPARATOR +
+ FULL_NAME +
+ SEPARATOR +
+ "something"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(400L) + SEPARATOR + FULL_NAME + SEPARATOR + "null"
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun string_doesNotLogIfSameValue() =
+ testScope.runTest {
+ systemClock.setCurrentTimeMillis(100L)
+ val flow = flow {
+ for (bool in listOf("start", "new", "new", "newer", "newest", "newest")) {
+ systemClock.advanceTime(100L)
+ emit(bool)
+ }
+ }
+
+ val flowWithLogging =
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ COLUMN_NAME,
+ initialValue = "start",
+ )
+
+ val job = launch { flowWithLogging.collect() }
+
+ val logs = dumpLog()
+ // Input flow: start@100, start@200, new@300, new@400, newer@500, newest@600, newest@700
+ // Output log: start@100, ---------, new@300, -------, newer@500, newest@600, ----------
+ val expected1 =
+ TABLE_LOG_DATE_FORMAT.format(100L) + SEPARATOR + FULL_NAME + SEPARATOR + "start"
+ val expected3 =
+ TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "new"
+ val expected5 =
+ TABLE_LOG_DATE_FORMAT.format(500L) + SEPARATOR + FULL_NAME + SEPARATOR + "newer"
+ val expected6 =
+ TABLE_LOG_DATE_FORMAT.format(600L) + SEPARATOR + FULL_NAME + SEPARATOR + "newest"
+ assertThat(logs).contains(expected1)
+ assertThat(logs).contains(expected3)
+ assertThat(logs).contains(expected5)
+ assertThat(logs).contains(expected6)
+
+ val unexpected2 =
+ TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "start"
+ val unexpected4 =
+ TABLE_LOG_DATE_FORMAT.format(400L) + SEPARATOR + FULL_NAME + SEPARATOR + "new"
+ val unexpected7 =
+ TABLE_LOG_DATE_FORMAT.format(700L) + SEPARATOR + FULL_NAME + SEPARATOR + "newest"
+ assertThat(logs).doesNotContain(unexpected2)
+ assertThat(logs).doesNotContain(unexpected4)
+ assertThat(logs).doesNotContain(unexpected7)
+
+ job.cancel()
+ }
+
+ @Test
+ fun string_worksForStateFlows() =
+ testScope.runTest {
+ val flow = MutableStateFlow("initial")
+
+ val flowWithLogging =
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ COLUMN_NAME,
+ initialValue = "initial",
+ )
+
+ systemClock.setCurrentTimeMillis(50L)
+ val job = launch { flowWithLogging.collect() }
+ assertThat(dumpLog())
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(50L) +
+ SEPARATOR +
+ FULL_NAME +
+ SEPARATOR +
+ "initial"
+ )
+
+ systemClock.setCurrentTimeMillis(100L)
+ flow.emit("nextVal")
+ assertThat(dumpLog())
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(100L) +
+ SEPARATOR +
+ FULL_NAME +
+ SEPARATOR +
+ "nextVal"
+ )
+
+ systemClock.setCurrentTimeMillis(200L)
+ flow.emit("nextNextVal")
+ assertThat(dumpLog())
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(200L) +
+ SEPARATOR +
+ FULL_NAME +
+ SEPARATOR +
+ "nextNextVal"
+ )
+
+ // Doesn't log duplicates
+ systemClock.setCurrentTimeMillis(300L)
+ flow.emit("nextNextVal")
+ assertThat(dumpLog())
+ .doesNotContain(
+ TABLE_LOG_DATE_FORMAT.format(300L) +
+ SEPARATOR +
+ FULL_NAME +
+ SEPARATOR +
+ "nextNextVal"
+ )
+
+ job.cancel()
+ }
+
+ // ---- Flow<Diffable> tests ----
+
+ @Test
+ fun diffable_doesNotLogWhenNotCollected() {
+ val flow =
+ flowOf(
+ TestDiffable(1, "1", true),
+ TestDiffable(2, "2", false),
+ )
+
+ val initial = TestDiffable(0, "0", false)
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ initial,
+ )
+
+ val logs = dumpLog()
+ assertThat(logs).doesNotContain(COLUMN_PREFIX)
+ assertThat(logs).doesNotContain(TestDiffable.COL_FULL)
+ assertThat(logs).doesNotContain(TestDiffable.COL_INT)
+ assertThat(logs).doesNotContain(TestDiffable.COL_STRING)
+ assertThat(logs).doesNotContain(TestDiffable.COL_BOOLEAN)
+ }
+
+ @Test
+ fun diffable_logsInitialWhenCollected_usingLogFull() =
+ testScope.runTest {
+ val flow =
+ flowOf(
+ TestDiffable(1, "1", true),
+ TestDiffable(2, "2", false),
+ )
+
+ val initial = TestDiffable(1234, "string1234", false)
+ val flowWithLogging =
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ initial,
+ )
+
+ systemClock.setCurrentTimeMillis(3000L)
+ val job = launch { flowWithLogging.collect() }
+
+ val logs = dumpLog()
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(3000L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_FULL +
+ SEPARATOR +
+ "true"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(3000L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_INT +
+ SEPARATOR +
+ "1234"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(3000L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_STRING +
+ SEPARATOR +
+ "string1234"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(3000L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_BOOLEAN +
+ SEPARATOR +
+ "false"
+ )
+ job.cancel()
+ }
+
+ @Test
+ fun diffable_logsUpdates_usingLogDiffs() =
+ testScope.runTest {
+ val initialValue = TestDiffable(0, "string0", false)
+ val diffables =
+ listOf(
+ TestDiffable(1, "string1", true),
+ TestDiffable(2, "string1", true),
+ TestDiffable(2, "string2", false),
+ )
+
+ systemClock.setCurrentTimeMillis(100L)
+ val flow = flow {
+ for (diffable in diffables) {
+ systemClock.advanceTime(100L)
+ emit(diffable)
+ }
+ }
+
+ val flowWithLogging =
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ initialValue,
+ )
+
+ val job = launch { flowWithLogging.collect() }
+
+ val logs = dumpLog()
+
+ // Initial -> first: everything different
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(200L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_FULL +
+ SEPARATOR +
+ "false"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(200L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_INT +
+ SEPARATOR +
+ "1"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(200L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_STRING +
+ SEPARATOR +
+ "string1"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(200L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_BOOLEAN +
+ SEPARATOR +
+ "true"
+ )
+
+ // First -> second: int different
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(300L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_FULL +
+ SEPARATOR +
+ "false"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(300L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_INT +
+ SEPARATOR +
+ "2"
+ )
+ assertThat(logs)
+ .doesNotContain(
+ TABLE_LOG_DATE_FORMAT.format(300L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_STRING +
+ SEPARATOR +
+ "string1"
+ )
+ assertThat(logs)
+ .doesNotContain(
+ TABLE_LOG_DATE_FORMAT.format(300L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_BOOLEAN +
+ SEPARATOR +
+ "true"
+ )
+
+ // Second -> third: string & boolean different
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(400L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_FULL +
+ SEPARATOR +
+ "false"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(400L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_STRING +
+ SEPARATOR +
+ "string2"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(400L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_BOOLEAN +
+ SEPARATOR +
+ "false"
+ )
+ assertThat(logs)
+ .doesNotContain(
+ TABLE_LOG_DATE_FORMAT.format(400L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_INT +
+ SEPARATOR +
+ "2"
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun diffable_worksForStateFlows() =
+ testScope.runTest {
+ val initialValue = TestDiffable(0, "string0", false)
+ val flow = MutableStateFlow(initialValue)
+ val flowWithLogging =
+ flow.logDiffsForTable(
+ tableLogBuffer,
+ COLUMN_PREFIX,
+ initialValue,
+ )
+
+ systemClock.setCurrentTimeMillis(50L)
+ val job = launch { flowWithLogging.collect() }
+
+ var logs = dumpLog()
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(50L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_INT +
+ SEPARATOR +
+ "0"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(50L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_STRING +
+ SEPARATOR +
+ "string0"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(50L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_BOOLEAN +
+ SEPARATOR +
+ "false"
+ )
+
+ systemClock.setCurrentTimeMillis(100L)
+ flow.emit(TestDiffable(1, "string1", true))
+
+ logs = dumpLog()
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(100L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_INT +
+ SEPARATOR +
+ "1"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(100L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_STRING +
+ SEPARATOR +
+ "string1"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(100L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_BOOLEAN +
+ SEPARATOR +
+ "true"
+ )
+
+ // Doesn't log duplicates
+ systemClock.setCurrentTimeMillis(200L)
+ flow.emit(TestDiffable(1, "newString", true))
+
+ logs = dumpLog()
+ assertThat(logs)
+ .doesNotContain(
+ TABLE_LOG_DATE_FORMAT.format(200L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_INT +
+ SEPARATOR +
+ "1"
+ )
+ assertThat(logs)
+ .contains(
+ TABLE_LOG_DATE_FORMAT.format(200L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_STRING +
+ SEPARATOR +
+ "newString"
+ )
+ assertThat(logs)
+ .doesNotContain(
+ TABLE_LOG_DATE_FORMAT.format(200L) +
+ SEPARATOR +
+ COLUMN_PREFIX +
+ "." +
+ TestDiffable.COL_BOOLEAN +
+ SEPARATOR +
+ "true"
+ )
+
+ job.cancel()
+ }
+
+ private fun dumpLog(): String {
+ val outputWriter = StringWriter()
+ tableLogBuffer.dump(PrintWriter(outputWriter), arrayOf())
+ return outputWriter.toString()
+ }
+
+ class TestDiffable(
+ private val testInt: Int,
+ private val testString: String,
+ private val testBoolean: Boolean,
+ ) : Diffable<TestDiffable> {
+ override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+ row.logChange(COL_FULL, false)
+
+ if (testInt != prevVal.testInt) {
+ row.logChange(COL_INT, testInt)
+ }
+ if (testString != prevVal.testString) {
+ row.logChange(COL_STRING, testString)
+ }
+ if (testBoolean != prevVal.testBoolean) {
+ row.logChange(COL_BOOLEAN, testBoolean)
+ }
+ }
+
+ override fun logFull(row: TableRowLogger) {
+ row.logChange(COL_FULL, true)
+ row.logChange(COL_INT, testInt)
+ row.logChange(COL_STRING, testString)
+ row.logChange(COL_BOOLEAN, testBoolean)
+ }
+
+ companion object {
+ const val COL_INT = "intColumn"
+ const val COL_STRING = "stringColumn"
+ const val COL_BOOLEAN = "booleanColumn"
+ const val COL_FULL = "loggedFullColumn"
+ }
+ }
+
+ private companion object {
+ const val MAX_SIZE = 50
+ const val BUFFER_NAME = "LogDiffsForTableTest"
+ const val COLUMN_PREFIX = "columnPrefix"
+ const val COLUMN_NAME = "columnName"
+ const val FULL_NAME = "$COLUMN_PREFIX.$COLUMN_NAME"
+ private const val SEPARATOR = "|"
+ }
+}