1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.log.table
18 
19 import androidx.test.filters.SmallTest
20 import com.android.systemui.SysuiTestCase
21 import com.android.systemui.log.table.TableChange.Companion.IS_INITIAL_PREFIX
22 import com.android.systemui.util.mockito.mock
23 import com.android.systemui.util.time.FakeSystemClock
24 import com.google.common.truth.Truth.assertThat
25 import java.io.PrintWriter
26 import java.io.StringWriter
27 import kotlinx.coroutines.ExperimentalCoroutinesApi
28 import kotlinx.coroutines.flow.MutableStateFlow
29 import kotlinx.coroutines.flow.collect
30 import kotlinx.coroutines.flow.flow
31 import kotlinx.coroutines.flow.flowOf
32 import kotlinx.coroutines.launch
33 import kotlinx.coroutines.test.TestScope
34 import kotlinx.coroutines.test.UnconfinedTestDispatcher
35 import kotlinx.coroutines.test.runTest
36 import org.junit.Before
37 import org.junit.Test
38 
39 @SmallTest
40 @OptIn(ExperimentalCoroutinesApi::class)
41 class LogDiffsForTableTest : SysuiTestCase() {
42 
43     private val testDispatcher = UnconfinedTestDispatcher()
44     private val testScope = TestScope(testDispatcher)
45 
46     private lateinit var systemClock: FakeSystemClock
47     private lateinit var tableLogBuffer: TableLogBuffer
48 
49     @Before
50     fun setUp() {
51         systemClock = FakeSystemClock()
52         tableLogBuffer =
53             TableLogBuffer(
54                 MAX_SIZE,
55                 BUFFER_NAME,
56                 systemClock,
57                 mock(),
58                 testDispatcher,
59                 testScope.backgroundScope,
60             )
61     }
62 
63     // ---- Flow<Boolean> tests ----
64 
65     @Test
66     fun boolean_doesNotLogWhenNotCollected() {
67         val flow = flowOf(true, true, false)
68 
69         flow.logDiffsForTable(
70             tableLogBuffer,
71             COLUMN_PREFIX,
72             COLUMN_NAME,
73             initialValue = false,
74         )
75 
76         val logs = dumpLog()
77         assertThat(logs).doesNotContain(COLUMN_PREFIX)
78         assertThat(logs).doesNotContain(COLUMN_NAME)
79         assertThat(logs).doesNotContain("false")
80     }
81 
82     @Test
83     fun boolean_logsInitialWhenCollected() =
84         testScope.runTest {
85             val flow = flowOf(true, true, false)
86 
87             val flowWithLogging =
88                 flow.logDiffsForTable(
89                     tableLogBuffer,
90                     COLUMN_PREFIX,
91                     COLUMN_NAME,
92                     initialValue = false,
93                 )
94 
95             systemClock.setCurrentTimeMillis(3000L)
96             val job = launch { flowWithLogging.collect() }
97 
98             val logs = dumpLog()
99             assertThat(logs)
100                 .contains(
101                     TABLE_LOG_DATE_FORMAT.format(3000L) +
102                         SEPARATOR +
103                         FULL_NAME +
104                         SEPARATOR +
105                         IS_INITIAL_PREFIX +
106                         "false"
107                 )
108 
109             job.cancel()
110         }
111 
112     @Test
113     fun boolean_logsUpdates() =
114         testScope.runTest {
115             systemClock.setCurrentTimeMillis(100L)
116             val flow = flow {
117                 for (bool in listOf(true, false, true)) {
118                     systemClock.advanceTime(100L)
119                     emit(bool)
120                 }
121             }
122 
123             val flowWithLogging =
124                 flow.logDiffsForTable(
125                     tableLogBuffer,
126                     COLUMN_PREFIX,
127                     COLUMN_NAME,
128                     initialValue = false,
129                 )
130 
131             val job = launch { flowWithLogging.collect() }
132 
133             val logs = dumpLog()
134             assertThat(logs)
135                 .contains(
136                     TABLE_LOG_DATE_FORMAT.format(100L) +
137                         SEPARATOR +
138                         FULL_NAME +
139                         SEPARATOR +
140                         IS_INITIAL_PREFIX +
141                         "false"
142                 )
143             assertThat(logs)
144                 .contains(
145                     TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "true"
146                 )
147             assertThat(logs)
148                 .contains(
149                     TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "false"
150                 )
151             assertThat(logs)
152                 .contains(
153                     TABLE_LOG_DATE_FORMAT.format(400L) + SEPARATOR + FULL_NAME + SEPARATOR + "true"
154                 )
155 
156             job.cancel()
157         }
158 
159     @Test
160     fun boolean_doesNotLogIfSameValue() =
161         testScope.runTest {
162             systemClock.setCurrentTimeMillis(100L)
163             val flow = flow {
164                 for (bool in listOf(true, true, false, false, true)) {
165                     systemClock.advanceTime(100L)
166                     emit(bool)
167                 }
168             }
169 
170             val flowWithLogging =
171                 flow.logDiffsForTable(
172                     tableLogBuffer,
173                     COLUMN_PREFIX,
174                     COLUMN_NAME,
175                     initialValue = true,
176                 )
177 
178             val job = launch { flowWithLogging.collect() }
179 
180             val logs = dumpLog()
181             // Input flow: true@100, true@200, true@300, false@400, false@500, true@600
182             // Output log: true@100, --------, --------, false@400, ---------, true@600
183             val expected1 =
184                 TABLE_LOG_DATE_FORMAT.format(100L) +
185                     SEPARATOR +
186                     FULL_NAME +
187                     SEPARATOR +
188                     IS_INITIAL_PREFIX +
189                     "true"
190             val expected4 =
191                 TABLE_LOG_DATE_FORMAT.format(400L) + SEPARATOR + FULL_NAME + SEPARATOR + "false"
192             val expected6 =
193                 TABLE_LOG_DATE_FORMAT.format(600L) + SEPARATOR + FULL_NAME + SEPARATOR + "true"
194             assertThat(logs).contains(expected1)
195             assertThat(logs).contains(expected4)
196             assertThat(logs).contains(expected6)
197 
198             val unexpected2 =
199                 TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "true"
200             val unexpected3 =
201                 TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "true"
202             val unexpected5 =
203                 TABLE_LOG_DATE_FORMAT.format(500L) + SEPARATOR + FULL_NAME + SEPARATOR + "false"
204             assertThat(logs).doesNotContain(unexpected2)
205             assertThat(logs).doesNotContain(unexpected3)
206             assertThat(logs).doesNotContain(unexpected5)
207 
208             job.cancel()
209         }
210 
211     @Test
212     fun boolean_worksForStateFlows() =
213         testScope.runTest {
214             val flow = MutableStateFlow(false)
215 
216             val flowWithLogging =
217                 flow.logDiffsForTable(
218                     tableLogBuffer,
219                     COLUMN_PREFIX,
220                     COLUMN_NAME,
221                     initialValue = false,
222                 )
223 
224             systemClock.setCurrentTimeMillis(50L)
225             val job = launch { flowWithLogging.collect() }
226             assertThat(dumpLog())
227                 .contains(
228                     TABLE_LOG_DATE_FORMAT.format(50L) +
229                         SEPARATOR +
230                         FULL_NAME +
231                         SEPARATOR +
232                         IS_INITIAL_PREFIX +
233                         "false"
234                 )
235 
236             systemClock.setCurrentTimeMillis(100L)
237             flow.emit(true)
238             assertThat(dumpLog())
239                 .contains(
240                     TABLE_LOG_DATE_FORMAT.format(100L) + SEPARATOR + FULL_NAME + SEPARATOR + "true"
241                 )
242 
243             systemClock.setCurrentTimeMillis(200L)
244             flow.emit(false)
245             assertThat(dumpLog())
246                 .contains(
247                     TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "false"
248                 )
249 
250             // Doesn't log duplicates
251             systemClock.setCurrentTimeMillis(300L)
252             flow.emit(false)
253             assertThat(dumpLog())
254                 .doesNotContain(
255                     TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "false"
256                 )
257 
258             job.cancel()
259         }
260 
261     // ---- Flow<Int> tests ----
262 
263     @Test
264     fun int_doesNotLogWhenNotCollected() {
265         val flow = flowOf(5, 6, 7)
266 
267         flow.logDiffsForTable(
268             tableLogBuffer,
269             COLUMN_PREFIX,
270             COLUMN_NAME,
271             initialValue = 1234,
272         )
273 
274         val logs = dumpLog()
275         assertThat(logs).doesNotContain(COLUMN_PREFIX)
276         assertThat(logs).doesNotContain(COLUMN_NAME)
277         assertThat(logs).doesNotContain("1234")
278     }
279 
280     @Test
281     fun int_logsInitialWhenCollected() =
282         testScope.runTest {
283             val flow = flowOf(5, 6, 7)
284 
285             val flowWithLogging =
286                 flow.logDiffsForTable(
287                     tableLogBuffer,
288                     COLUMN_PREFIX,
289                     COLUMN_NAME,
290                     initialValue = 1234,
291                 )
292 
293             systemClock.setCurrentTimeMillis(3000L)
294             val job = launch { flowWithLogging.collect() }
295 
296             val logs = dumpLog()
297             assertThat(logs)
298                 .contains(
299                     TABLE_LOG_DATE_FORMAT.format(3000L) +
300                         SEPARATOR +
301                         FULL_NAME +
302                         SEPARATOR +
303                         IS_INITIAL_PREFIX +
304                         "1234"
305                 )
306 
307             job.cancel()
308         }
309 
310     @Test
311     fun intNullable_logsNull() =
312         testScope.runTest {
313             systemClock.setCurrentTimeMillis(100L)
314             val flow = flow {
315                 for (int in listOf(null, 6, null, 8)) {
316                     systemClock.advanceTime(100L)
317                     emit(int)
318                 }
319             }
320 
321             val flowWithLogging =
322                 flow.logDiffsForTable(
323                     tableLogBuffer,
324                     COLUMN_PREFIX,
325                     COLUMN_NAME,
326                     initialValue = 1234,
327                 )
328 
329             val job = launch { flowWithLogging.collect() }
330 
331             val logs = dumpLog()
332             assertThat(logs)
333                 .contains(
334                     TABLE_LOG_DATE_FORMAT.format(100L) +
335                         SEPARATOR +
336                         FULL_NAME +
337                         SEPARATOR +
338                         IS_INITIAL_PREFIX +
339                         "1234"
340                 )
341             assertThat(logs)
342                 .contains(
343                     TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "null"
344                 )
345             assertThat(logs)
346                 .contains(
347                     TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "6"
348                 )
349             assertThat(logs)
350                 .contains(
351                     TABLE_LOG_DATE_FORMAT.format(400L) + SEPARATOR + FULL_NAME + SEPARATOR + "null"
352                 )
353             assertThat(logs)
354                 .contains(
355                     TABLE_LOG_DATE_FORMAT.format(500L) + SEPARATOR + FULL_NAME + SEPARATOR + "8"
356                 )
357 
358             job.cancel()
359         }
360 
361     @Test
362     fun int_logsUpdates() =
363         testScope.runTest {
364             systemClock.setCurrentTimeMillis(100L)
365             val flow = flow {
366                 for (int in listOf(2, 3, 4)) {
367                     systemClock.advanceTime(100L)
368                     emit(int)
369                 }
370             }
371 
372             val flowWithLogging =
373                 flow.logDiffsForTable(
374                     tableLogBuffer,
375                     COLUMN_PREFIX,
376                     COLUMN_NAME,
377                     initialValue = 1,
378                 )
379 
380             val job = launch { flowWithLogging.collect() }
381 
382             val logs = dumpLog()
383             assertThat(logs)
384                 .contains(
385                     TABLE_LOG_DATE_FORMAT.format(100L) +
386                         SEPARATOR +
387                         FULL_NAME +
388                         SEPARATOR +
389                         IS_INITIAL_PREFIX +
390                         "1"
391                 )
392             assertThat(logs)
393                 .contains(
394                     TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "2"
395                 )
396             assertThat(logs)
397                 .contains(
398                     TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "3"
399                 )
400             assertThat(logs)
401                 .contains(
402                     TABLE_LOG_DATE_FORMAT.format(400L) + SEPARATOR + FULL_NAME + SEPARATOR + "4"
403                 )
404 
405             job.cancel()
406         }
407 
408     @Test
409     fun int_doesNotLogIfSameValue() =
410         testScope.runTest {
411             systemClock.setCurrentTimeMillis(100L)
412             val flow = flow {
413                 for (bool in listOf(2, 3, 3, 3, 2, 6, 6)) {
414                     systemClock.advanceTime(100L)
415                     emit(bool)
416                 }
417             }
418 
419             val flowWithLogging =
420                 flow.logDiffsForTable(
421                     tableLogBuffer,
422                     COLUMN_PREFIX,
423                     COLUMN_NAME,
424                     initialValue = 1,
425                 )
426 
427             val job = launch { flowWithLogging.collect() }
428 
429             val logs = dumpLog()
430             // Input flow: 1@100, 2@200, 3@300, 3@400, 3@500, 2@600, 6@700, 6@800
431             // Output log: 1@100, 2@200, 3@300, -----, -----, 2@600, 6@700, -----
432             val expected1 =
433                 TABLE_LOG_DATE_FORMAT.format(100L) +
434                     SEPARATOR +
435                     FULL_NAME +
436                     SEPARATOR +
437                     IS_INITIAL_PREFIX +
438                     "1"
439             val expected2 =
440                 TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "2"
441             val expected3 =
442                 TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "3"
443             val expected6 =
444                 TABLE_LOG_DATE_FORMAT.format(600L) + SEPARATOR + FULL_NAME + SEPARATOR + "2"
445             val expected7 =
446                 TABLE_LOG_DATE_FORMAT.format(700L) + SEPARATOR + FULL_NAME + SEPARATOR + "6"
447             assertThat(logs).contains(expected1)
448             assertThat(logs).contains(expected2)
449             assertThat(logs).contains(expected3)
450             assertThat(logs).contains(expected6)
451             assertThat(logs).contains(expected7)
452 
453             val unexpected4 =
454                 TABLE_LOG_DATE_FORMAT.format(400L) + SEPARATOR + FULL_NAME + SEPARATOR + "3"
455             val unexpected5 =
456                 TABLE_LOG_DATE_FORMAT.format(500L) + SEPARATOR + FULL_NAME + SEPARATOR + "3"
457             val unexpected8 =
458                 TABLE_LOG_DATE_FORMAT.format(800L) + SEPARATOR + FULL_NAME + SEPARATOR + "6"
459             assertThat(logs).doesNotContain(unexpected4)
460             assertThat(logs).doesNotContain(unexpected5)
461             assertThat(logs).doesNotContain(unexpected8)
462             job.cancel()
463         }
464 
465     @Test
466     fun int_worksForStateFlows() =
467         testScope.runTest {
468             val flow = MutableStateFlow(1111)
469 
470             val flowWithLogging =
471                 flow.logDiffsForTable(
472                     tableLogBuffer,
473                     COLUMN_PREFIX,
474                     COLUMN_NAME,
475                     initialValue = 1111,
476                 )
477 
478             systemClock.setCurrentTimeMillis(50L)
479             val job = launch { flowWithLogging.collect() }
480             assertThat(dumpLog())
481                 .contains(
482                     TABLE_LOG_DATE_FORMAT.format(50L) +
483                         SEPARATOR +
484                         FULL_NAME +
485                         SEPARATOR +
486                         IS_INITIAL_PREFIX +
487                         "1111"
488                 )
489 
490             systemClock.setCurrentTimeMillis(100L)
491             flow.emit(2222)
492             assertThat(dumpLog())
493                 .contains(
494                     TABLE_LOG_DATE_FORMAT.format(100L) + SEPARATOR + FULL_NAME + SEPARATOR + "2222"
495                 )
496 
497             systemClock.setCurrentTimeMillis(200L)
498             flow.emit(3333)
499             assertThat(dumpLog())
500                 .contains(
501                     TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "3333"
502                 )
503 
504             // Doesn't log duplicates
505             systemClock.setCurrentTimeMillis(300L)
506             flow.emit(3333)
507             assertThat(dumpLog())
508                 .doesNotContain(
509                     TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "3333"
510                 )
511 
512             job.cancel()
513         }
514 
515     // ---- Flow<String> tests ----
516 
517     @Test
518     fun string_doesNotLogWhenNotCollected() {
519         val flow = flowOf("val5", "val6", "val7")
520 
521         flow.logDiffsForTable(
522             tableLogBuffer,
523             COLUMN_PREFIX,
524             COLUMN_NAME,
525             initialValue = "val1234",
526         )
527 
528         val logs = dumpLog()
529         assertThat(logs).doesNotContain(COLUMN_PREFIX)
530         assertThat(logs).doesNotContain(COLUMN_NAME)
531         assertThat(logs).doesNotContain("val1234")
532     }
533 
534     @Test
535     fun string_logsInitialWhenCollected() =
536         testScope.runTest {
537             val flow = flowOf("val5", "val6", "val7")
538 
539             val flowWithLogging =
540                 flow.logDiffsForTable(
541                     tableLogBuffer,
542                     COLUMN_PREFIX,
543                     COLUMN_NAME,
544                     initialValue = "val1234",
545                 )
546 
547             systemClock.setCurrentTimeMillis(3000L)
548             val job = launch { flowWithLogging.collect() }
549 
550             val logs = dumpLog()
551             assertThat(logs)
552                 .contains(
553                     TABLE_LOG_DATE_FORMAT.format(3000L) +
554                         SEPARATOR +
555                         FULL_NAME +
556                         SEPARATOR +
557                         IS_INITIAL_PREFIX +
558                         "val1234"
559                 )
560 
561             job.cancel()
562         }
563 
564     @Test
565     fun string_logsUpdates() =
566         testScope.runTest {
567             systemClock.setCurrentTimeMillis(100L)
568             val flow = flow {
569                 for (int in listOf("val2", "val3", "val4")) {
570                     systemClock.advanceTime(100L)
571                     emit(int)
572                 }
573             }
574 
575             val flowWithLogging =
576                 flow.logDiffsForTable(
577                     tableLogBuffer,
578                     COLUMN_PREFIX,
579                     COLUMN_NAME,
580                     initialValue = "val1",
581                 )
582 
583             val job = launch { flowWithLogging.collect() }
584 
585             val logs = dumpLog()
586             assertThat(logs)
587                 .contains(
588                     TABLE_LOG_DATE_FORMAT.format(100L) +
589                         SEPARATOR +
590                         FULL_NAME +
591                         SEPARATOR +
592                         IS_INITIAL_PREFIX +
593                         "val1"
594                 )
595             assertThat(logs)
596                 .contains(
597                     TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "val2"
598                 )
599             assertThat(logs)
600                 .contains(
601                     TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "val3"
602                 )
603             assertThat(logs)
604                 .contains(
605                     TABLE_LOG_DATE_FORMAT.format(400L) + SEPARATOR + FULL_NAME + SEPARATOR + "val4"
606                 )
607 
608             job.cancel()
609         }
610 
611     @Test
612     fun string_logsNull() =
613         testScope.runTest {
614             systemClock.setCurrentTimeMillis(100L)
615             val flow = flow {
616                 for (int in listOf(null, "something", null)) {
617                     systemClock.advanceTime(100L)
618                     emit(int)
619                 }
620             }
621 
622             val flowWithLogging =
623                 flow.logDiffsForTable(
624                     tableLogBuffer,
625                     COLUMN_PREFIX,
626                     COLUMN_NAME,
627                     initialValue = "start",
628                 )
629 
630             val job = launch { flowWithLogging.collect() }
631 
632             val logs = dumpLog()
633             assertThat(logs)
634                 .contains(
635                     TABLE_LOG_DATE_FORMAT.format(100L) +
636                         SEPARATOR +
637                         FULL_NAME +
638                         SEPARATOR +
639                         IS_INITIAL_PREFIX +
640                         "start"
641                 )
642             assertThat(logs)
643                 .contains(
644                     TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "null"
645                 )
646             assertThat(logs)
647                 .contains(
648                     TABLE_LOG_DATE_FORMAT.format(300L) +
649                         SEPARATOR +
650                         FULL_NAME +
651                         SEPARATOR +
652                         "something"
653                 )
654             assertThat(logs)
655                 .contains(
656                     TABLE_LOG_DATE_FORMAT.format(400L) + SEPARATOR + FULL_NAME + SEPARATOR + "null"
657                 )
658 
659             job.cancel()
660         }
661 
662     @Test
663     fun string_doesNotLogIfSameValue() =
664         testScope.runTest {
665             systemClock.setCurrentTimeMillis(100L)
666             val flow = flow {
667                 for (bool in listOf("start", "new", "new", "newer", "newest", "newest")) {
668                     systemClock.advanceTime(100L)
669                     emit(bool)
670                 }
671             }
672 
673             val flowWithLogging =
674                 flow.logDiffsForTable(
675                     tableLogBuffer,
676                     COLUMN_PREFIX,
677                     COLUMN_NAME,
678                     initialValue = "start",
679                 )
680 
681             val job = launch { flowWithLogging.collect() }
682 
683             val logs = dumpLog()
684             // Input flow: start@100, start@200, new@300, new@400, newer@500, newest@600, newest@700
685             // Output log: start@100, ---------, new@300, -------, newer@500, newest@600, ----------
686             val expected1 =
687                 TABLE_LOG_DATE_FORMAT.format(100L) +
688                     SEPARATOR +
689                     FULL_NAME +
690                     SEPARATOR +
691                     IS_INITIAL_PREFIX +
692                     "start"
693             val expected3 =
694                 TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "new"
695             val expected5 =
696                 TABLE_LOG_DATE_FORMAT.format(500L) + SEPARATOR + FULL_NAME + SEPARATOR + "newer"
697             val expected6 =
698                 TABLE_LOG_DATE_FORMAT.format(600L) + SEPARATOR + FULL_NAME + SEPARATOR + "newest"
699             assertThat(logs).contains(expected1)
700             assertThat(logs).contains(expected3)
701             assertThat(logs).contains(expected5)
702             assertThat(logs).contains(expected6)
703 
704             val unexpected2 =
705                 TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "start"
706             val unexpected4 =
707                 TABLE_LOG_DATE_FORMAT.format(400L) + SEPARATOR + FULL_NAME + SEPARATOR + "new"
708             val unexpected7 =
709                 TABLE_LOG_DATE_FORMAT.format(700L) + SEPARATOR + FULL_NAME + SEPARATOR + "newest"
710             assertThat(logs).doesNotContain(unexpected2)
711             assertThat(logs).doesNotContain(unexpected4)
712             assertThat(logs).doesNotContain(unexpected7)
713 
714             job.cancel()
715         }
716 
717     @Test
718     fun string_worksForStateFlows() =
719         testScope.runTest {
720             val flow = MutableStateFlow("initial")
721 
722             val flowWithLogging =
723                 flow.logDiffsForTable(
724                     tableLogBuffer,
725                     COLUMN_PREFIX,
726                     COLUMN_NAME,
727                     initialValue = "initial",
728                 )
729 
730             systemClock.setCurrentTimeMillis(50L)
731             val job = launch { flowWithLogging.collect() }
732             assertThat(dumpLog())
733                 .contains(
734                     TABLE_LOG_DATE_FORMAT.format(50L) +
735                         SEPARATOR +
736                         FULL_NAME +
737                         SEPARATOR +
738                         IS_INITIAL_PREFIX +
739                         "initial"
740                 )
741 
742             systemClock.setCurrentTimeMillis(100L)
743             flow.emit("nextVal")
744             assertThat(dumpLog())
745                 .contains(
746                     TABLE_LOG_DATE_FORMAT.format(100L) +
747                         SEPARATOR +
748                         FULL_NAME +
749                         SEPARATOR +
750                         "nextVal"
751                 )
752 
753             systemClock.setCurrentTimeMillis(200L)
754             flow.emit("nextNextVal")
755             assertThat(dumpLog())
756                 .contains(
757                     TABLE_LOG_DATE_FORMAT.format(200L) +
758                         SEPARATOR +
759                         FULL_NAME +
760                         SEPARATOR +
761                         "nextNextVal"
762                 )
763 
764             // Doesn't log duplicates
765             systemClock.setCurrentTimeMillis(300L)
766             flow.emit("nextNextVal")
767             assertThat(dumpLog())
768                 .doesNotContain(
769                     TABLE_LOG_DATE_FORMAT.format(300L) +
770                         SEPARATOR +
771                         FULL_NAME +
772                         SEPARATOR +
773                         "nextNextVal"
774                 )
775 
776             job.cancel()
777         }
778 
779     // ---- Flow<Diffable> tests ----
780 
781     @Test
782     fun diffable_doesNotLogWhenNotCollected() {
783         val flow =
784             flowOf(
785                 TestDiffable(1, "1", true),
786                 TestDiffable(2, "2", false),
787             )
788 
789         val initial = TestDiffable(0, "0", false)
790         flow.logDiffsForTable(
791             tableLogBuffer,
792             COLUMN_PREFIX,
793             initial,
794         )
795 
796         val logs = dumpLog()
797         assertThat(logs).doesNotContain(COLUMN_PREFIX)
798         assertThat(logs).doesNotContain(TestDiffable.COL_FULL)
799         assertThat(logs).doesNotContain(TestDiffable.COL_INT)
800         assertThat(logs).doesNotContain(TestDiffable.COL_STRING)
801         assertThat(logs).doesNotContain(TestDiffable.COL_BOOLEAN)
802     }
803 
804     @Test
805     fun diffable_logsInitialWhenCollected_usingLogFull() =
806         testScope.runTest {
807             val flow =
808                 flowOf(
809                     TestDiffable(1, "1", true),
810                     TestDiffable(2, "2", false),
811                 )
812 
813             val initial = TestDiffable(1234, "string1234", false)
814             val flowWithLogging =
815                 flow.logDiffsForTable(
816                     tableLogBuffer,
817                     COLUMN_PREFIX,
818                     initial,
819                 )
820 
821             systemClock.setCurrentTimeMillis(3000L)
822             val job = launch { flowWithLogging.collect() }
823 
824             val logs = dumpLog()
825             assertThat(logs)
826                 .contains(
827                     TABLE_LOG_DATE_FORMAT.format(3000L) +
828                         SEPARATOR +
829                         COLUMN_PREFIX +
830                         "." +
831                         TestDiffable.COL_FULL +
832                         SEPARATOR +
833                         IS_INITIAL_PREFIX +
834                         "true"
835                 )
836             assertThat(logs)
837                 .contains(
838                     TABLE_LOG_DATE_FORMAT.format(3000L) +
839                         SEPARATOR +
840                         COLUMN_PREFIX +
841                         "." +
842                         TestDiffable.COL_INT +
843                         SEPARATOR +
844                         IS_INITIAL_PREFIX +
845                         "1234"
846                 )
847             assertThat(logs)
848                 .contains(
849                     TABLE_LOG_DATE_FORMAT.format(3000L) +
850                         SEPARATOR +
851                         COLUMN_PREFIX +
852                         "." +
853                         TestDiffable.COL_STRING +
854                         SEPARATOR +
855                         IS_INITIAL_PREFIX +
856                         "string1234"
857                 )
858             assertThat(logs)
859                 .contains(
860                     TABLE_LOG_DATE_FORMAT.format(3000L) +
861                         SEPARATOR +
862                         COLUMN_PREFIX +
863                         "." +
864                         TestDiffable.COL_BOOLEAN +
865                         SEPARATOR +
866                         IS_INITIAL_PREFIX +
867                         "false"
868                 )
869             job.cancel()
870         }
871 
872     @Test
873     fun diffable_logsUpdates_usingLogDiffs() =
874         testScope.runTest {
875             val initialValue = TestDiffable(0, "string0", false)
876             val diffables =
877                 listOf(
878                     TestDiffable(1, "string1", true),
879                     TestDiffable(2, "string1", true),
880                     TestDiffable(2, "string2", false),
881                 )
882 
883             systemClock.setCurrentTimeMillis(100L)
884             val flow = flow {
885                 for (diffable in diffables) {
886                     systemClock.advanceTime(100L)
887                     emit(diffable)
888                 }
889             }
890 
891             val flowWithLogging =
892                 flow.logDiffsForTable(
893                     tableLogBuffer,
894                     COLUMN_PREFIX,
895                     initialValue,
896                 )
897 
898             val job = launch { flowWithLogging.collect() }
899 
900             val logs = dumpLog()
901 
902             // Initial -> first: everything different
903             assertThat(logs)
904                 .contains(
905                     TABLE_LOG_DATE_FORMAT.format(200L) +
906                         SEPARATOR +
907                         COLUMN_PREFIX +
908                         "." +
909                         TestDiffable.COL_FULL +
910                         SEPARATOR +
911                         "false"
912                 )
913             assertThat(logs)
914                 .contains(
915                     TABLE_LOG_DATE_FORMAT.format(200L) +
916                         SEPARATOR +
917                         COLUMN_PREFIX +
918                         "." +
919                         TestDiffable.COL_INT +
920                         SEPARATOR +
921                         "1"
922                 )
923             assertThat(logs)
924                 .contains(
925                     TABLE_LOG_DATE_FORMAT.format(200L) +
926                         SEPARATOR +
927                         COLUMN_PREFIX +
928                         "." +
929                         TestDiffable.COL_STRING +
930                         SEPARATOR +
931                         "string1"
932                 )
933             assertThat(logs)
934                 .contains(
935                     TABLE_LOG_DATE_FORMAT.format(200L) +
936                         SEPARATOR +
937                         COLUMN_PREFIX +
938                         "." +
939                         TestDiffable.COL_BOOLEAN +
940                         SEPARATOR +
941                         "true"
942                 )
943 
944             // First -> second: int different
945             assertThat(logs)
946                 .contains(
947                     TABLE_LOG_DATE_FORMAT.format(300L) +
948                         SEPARATOR +
949                         COLUMN_PREFIX +
950                         "." +
951                         TestDiffable.COL_FULL +
952                         SEPARATOR +
953                         "false"
954                 )
955             assertThat(logs)
956                 .contains(
957                     TABLE_LOG_DATE_FORMAT.format(300L) +
958                         SEPARATOR +
959                         COLUMN_PREFIX +
960                         "." +
961                         TestDiffable.COL_INT +
962                         SEPARATOR +
963                         "2"
964                 )
965             assertThat(logs)
966                 .doesNotContain(
967                     TABLE_LOG_DATE_FORMAT.format(300L) +
968                         SEPARATOR +
969                         COLUMN_PREFIX +
970                         "." +
971                         TestDiffable.COL_STRING +
972                         SEPARATOR +
973                         "string1"
974                 )
975             assertThat(logs)
976                 .doesNotContain(
977                     TABLE_LOG_DATE_FORMAT.format(300L) +
978                         SEPARATOR +
979                         COLUMN_PREFIX +
980                         "." +
981                         TestDiffable.COL_BOOLEAN +
982                         SEPARATOR +
983                         "true"
984                 )
985 
986             // Second -> third: string & boolean different
987             assertThat(logs)
988                 .contains(
989                     TABLE_LOG_DATE_FORMAT.format(400L) +
990                         SEPARATOR +
991                         COLUMN_PREFIX +
992                         "." +
993                         TestDiffable.COL_FULL +
994                         SEPARATOR +
995                         "false"
996                 )
997             assertThat(logs)
998                 .contains(
999                     TABLE_LOG_DATE_FORMAT.format(400L) +
1000                         SEPARATOR +
1001                         COLUMN_PREFIX +
1002                         "." +
1003                         TestDiffable.COL_STRING +
1004                         SEPARATOR +
1005                         "string2"
1006                 )
1007             assertThat(logs)
1008                 .contains(
1009                     TABLE_LOG_DATE_FORMAT.format(400L) +
1010                         SEPARATOR +
1011                         COLUMN_PREFIX +
1012                         "." +
1013                         TestDiffable.COL_BOOLEAN +
1014                         SEPARATOR +
1015                         "false"
1016                 )
1017             assertThat(logs)
1018                 .doesNotContain(
1019                     TABLE_LOG_DATE_FORMAT.format(400L) +
1020                         SEPARATOR +
1021                         COLUMN_PREFIX +
1022                         "." +
1023                         TestDiffable.COL_INT +
1024                         SEPARATOR +
1025                         "2"
1026                 )
1027 
1028             job.cancel()
1029         }
1030 
1031     @Test
1032     fun diffable_worksForStateFlows() =
1033         testScope.runTest {
1034             val initialValue = TestDiffable(0, "string0", false)
1035             val flow = MutableStateFlow(initialValue)
1036             val flowWithLogging =
1037                 flow.logDiffsForTable(
1038                     tableLogBuffer,
1039                     COLUMN_PREFIX,
1040                     initialValue,
1041                 )
1042 
1043             systemClock.setCurrentTimeMillis(50L)
1044             val job = launch { flowWithLogging.collect() }
1045 
1046             var logs = dumpLog()
1047             assertThat(logs)
1048                 .contains(
1049                     TABLE_LOG_DATE_FORMAT.format(50L) +
1050                         SEPARATOR +
1051                         COLUMN_PREFIX +
1052                         "." +
1053                         TestDiffable.COL_INT +
1054                         SEPARATOR +
1055                         IS_INITIAL_PREFIX +
1056                         "0"
1057                 )
1058             assertThat(logs)
1059                 .contains(
1060                     TABLE_LOG_DATE_FORMAT.format(50L) +
1061                         SEPARATOR +
1062                         COLUMN_PREFIX +
1063                         "." +
1064                         TestDiffable.COL_STRING +
1065                         SEPARATOR +
1066                         IS_INITIAL_PREFIX +
1067                         "string0"
1068                 )
1069             assertThat(logs)
1070                 .contains(
1071                     TABLE_LOG_DATE_FORMAT.format(50L) +
1072                         SEPARATOR +
1073                         COLUMN_PREFIX +
1074                         "." +
1075                         TestDiffable.COL_BOOLEAN +
1076                         SEPARATOR +
1077                         IS_INITIAL_PREFIX +
1078                         "false"
1079                 )
1080 
1081             systemClock.setCurrentTimeMillis(100L)
1082             flow.emit(TestDiffable(1, "string1", true))
1083 
1084             logs = dumpLog()
1085             assertThat(logs)
1086                 .contains(
1087                     TABLE_LOG_DATE_FORMAT.format(100L) +
1088                         SEPARATOR +
1089                         COLUMN_PREFIX +
1090                         "." +
1091                         TestDiffable.COL_INT +
1092                         SEPARATOR +
1093                         "1"
1094                 )
1095             assertThat(logs)
1096                 .contains(
1097                     TABLE_LOG_DATE_FORMAT.format(100L) +
1098                         SEPARATOR +
1099                         COLUMN_PREFIX +
1100                         "." +
1101                         TestDiffable.COL_STRING +
1102                         SEPARATOR +
1103                         "string1"
1104                 )
1105             assertThat(logs)
1106                 .contains(
1107                     TABLE_LOG_DATE_FORMAT.format(100L) +
1108                         SEPARATOR +
1109                         COLUMN_PREFIX +
1110                         "." +
1111                         TestDiffable.COL_BOOLEAN +
1112                         SEPARATOR +
1113                         "true"
1114                 )
1115 
1116             // Doesn't log duplicates
1117             systemClock.setCurrentTimeMillis(200L)
1118             flow.emit(TestDiffable(1, "newString", true))
1119 
1120             logs = dumpLog()
1121             assertThat(logs)
1122                 .doesNotContain(
1123                     TABLE_LOG_DATE_FORMAT.format(200L) +
1124                         SEPARATOR +
1125                         COLUMN_PREFIX +
1126                         "." +
1127                         TestDiffable.COL_INT +
1128                         SEPARATOR +
1129                         "1"
1130                 )
1131             assertThat(logs)
1132                 .contains(
1133                     TABLE_LOG_DATE_FORMAT.format(200L) +
1134                         SEPARATOR +
1135                         COLUMN_PREFIX +
1136                         "." +
1137                         TestDiffable.COL_STRING +
1138                         SEPARATOR +
1139                         "newString"
1140                 )
1141             assertThat(logs)
1142                 .doesNotContain(
1143                     TABLE_LOG_DATE_FORMAT.format(200L) +
1144                         SEPARATOR +
1145                         COLUMN_PREFIX +
1146                         "." +
1147                         TestDiffable.COL_BOOLEAN +
1148                         SEPARATOR +
1149                         "true"
1150                 )
1151 
1152             job.cancel()
1153         }
1154 
1155     // ---- Flow<List<T>> tests ----
1156 
1157     @Test
1158     fun list_doesNotLogWhenNotCollected() {
1159         val flow = flowOf(listOf(5), listOf(6), listOf(7))
1160 
1161         flow.logDiffsForTable(
1162             tableLogBuffer,
1163             COLUMN_PREFIX,
1164             COLUMN_NAME,
1165             initialValue = listOf(1234),
1166         )
1167 
1168         val logs = dumpLog()
1169         assertThat(logs).doesNotContain(COLUMN_PREFIX)
1170         assertThat(logs).doesNotContain(COLUMN_NAME)
1171         assertThat(logs).doesNotContain("1234")
1172     }
1173 
1174     @Test
1175     fun list_logsInitialWhenCollected() =
1176         testScope.runTest {
1177             val flow = flowOf(listOf(5), listOf(6), listOf(7))
1178 
1179             val flowWithLogging =
1180                 flow.logDiffsForTable(
1181                     tableLogBuffer,
1182                     COLUMN_PREFIX,
1183                     COLUMN_NAME,
1184                     initialValue = listOf(1234),
1185                 )
1186 
1187             systemClock.setCurrentTimeMillis(3000L)
1188             val job = launch { flowWithLogging.collect() }
1189 
1190             val logs = dumpLog()
1191             assertThat(logs)
1192                 .contains(
1193                     TABLE_LOG_DATE_FORMAT.format(3000L) +
1194                         SEPARATOR +
1195                         FULL_NAME +
1196                         SEPARATOR +
1197                         IS_INITIAL_PREFIX +
1198                         listOf(1234).toString()
1199                 )
1200 
1201             job.cancel()
1202         }
1203 
1204     @Test
1205     fun list_logsUpdates() =
1206         testScope.runTest {
1207             systemClock.setCurrentTimeMillis(100L)
1208 
1209             val listItems =
1210                 listOf(listOf("val1", "val2"), listOf("val3"), listOf("val4", "val5", "val6"))
1211             val flow = flow {
1212                 for (list in listItems) {
1213                     systemClock.advanceTime(100L)
1214                     emit(list)
1215                 }
1216             }
1217 
1218             val flowWithLogging =
1219                 flow.logDiffsForTable(
1220                     tableLogBuffer,
1221                     COLUMN_PREFIX,
1222                     COLUMN_NAME,
1223                     initialValue = listOf("val0", "val00"),
1224                 )
1225 
1226             val job = launch { flowWithLogging.collect() }
1227 
1228             val logs = dumpLog()
1229             assertThat(logs)
1230                 .contains(
1231                     TABLE_LOG_DATE_FORMAT.format(100L) +
1232                         SEPARATOR +
1233                         FULL_NAME +
1234                         SEPARATOR +
1235                         IS_INITIAL_PREFIX +
1236                         listOf("val0", "val00").toString()
1237                 )
1238             assertThat(logs)
1239                 .contains(
1240                     TABLE_LOG_DATE_FORMAT.format(200L) +
1241                         SEPARATOR +
1242                         FULL_NAME +
1243                         SEPARATOR +
1244                         listOf("val1", "val2").toString()
1245                 )
1246             assertThat(logs)
1247                 .contains(
1248                     TABLE_LOG_DATE_FORMAT.format(300L) +
1249                         SEPARATOR +
1250                         FULL_NAME +
1251                         SEPARATOR +
1252                         listOf("val3").toString()
1253                 )
1254             assertThat(logs)
1255                 .contains(
1256                     TABLE_LOG_DATE_FORMAT.format(400L) +
1257                         SEPARATOR +
1258                         FULL_NAME +
1259                         SEPARATOR +
1260                         listOf("val4", "val5", "val6").toString()
1261                 )
1262 
1263             job.cancel()
1264         }
1265 
1266     @Test
1267     fun list_doesNotLogIfSameValue() =
1268         testScope.runTest {
1269             systemClock.setCurrentTimeMillis(100L)
1270 
1271             val listItems =
1272                 listOf(
1273                     listOf("val0", "val00"),
1274                     listOf("val1"),
1275                     listOf("val1"),
1276                     listOf("val1", "val2"),
1277                 )
1278             val flow = flow {
1279                 for (bool in listItems) {
1280                     systemClock.advanceTime(100L)
1281                     emit(bool)
1282                 }
1283             }
1284 
1285             val flowWithLogging =
1286                 flow.logDiffsForTable(
1287                     tableLogBuffer,
1288                     COLUMN_PREFIX,
1289                     COLUMN_NAME,
1290                     initialValue = listOf("val0", "val00"),
1291                 )
1292 
1293             val job = launch { flowWithLogging.collect() }
1294 
1295             val logs = dumpLog()
1296 
1297             val expected1 =
1298                 TABLE_LOG_DATE_FORMAT.format(100L) +
1299                     SEPARATOR +
1300                     FULL_NAME +
1301                     SEPARATOR +
1302                     IS_INITIAL_PREFIX +
1303                     listOf("val0", "val00").toString()
1304             val expected3 =
1305                 TABLE_LOG_DATE_FORMAT.format(300L) +
1306                     SEPARATOR +
1307                     FULL_NAME +
1308                     SEPARATOR +
1309                     listOf("val1").toString()
1310             val expected5 =
1311                 TABLE_LOG_DATE_FORMAT.format(500L) +
1312                     SEPARATOR +
1313                     FULL_NAME +
1314                     SEPARATOR +
1315                     listOf("val1", "val2").toString()
1316             assertThat(logs).contains(expected1)
1317             assertThat(logs).contains(expected3)
1318             assertThat(logs).contains(expected5)
1319 
1320             val unexpected2 =
1321                 TABLE_LOG_DATE_FORMAT.format(200L) +
1322                     SEPARATOR +
1323                     FULL_NAME +
1324                     SEPARATOR +
1325                     listOf("val0", "val00")
1326             val unexpected4 =
1327                 TABLE_LOG_DATE_FORMAT.format(400L) +
1328                     SEPARATOR +
1329                     FULL_NAME +
1330                     SEPARATOR +
1331                     listOf("val1")
1332             assertThat(logs).doesNotContain(unexpected2)
1333             assertThat(logs).doesNotContain(unexpected4)
1334             job.cancel()
1335         }
1336 
1337     @Test
1338     fun list_worksForStateFlows() =
1339         testScope.runTest {
1340             val flow = MutableStateFlow(listOf(1111))
1341 
1342             val flowWithLogging =
1343                 flow.logDiffsForTable(
1344                     tableLogBuffer,
1345                     COLUMN_PREFIX,
1346                     COLUMN_NAME,
1347                     initialValue = listOf(1111),
1348                 )
1349 
1350             systemClock.setCurrentTimeMillis(50L)
1351             val job = launch { flowWithLogging.collect() }
1352             assertThat(dumpLog())
1353                 .contains(
1354                     TABLE_LOG_DATE_FORMAT.format(50L) +
1355                         SEPARATOR +
1356                         FULL_NAME +
1357                         SEPARATOR +
1358                         IS_INITIAL_PREFIX +
1359                         listOf(1111).toString()
1360                 )
1361 
1362             systemClock.setCurrentTimeMillis(100L)
1363             flow.emit(listOf(2222, 3333))
1364             assertThat(dumpLog())
1365                 .contains(
1366                     TABLE_LOG_DATE_FORMAT.format(100L) +
1367                         SEPARATOR +
1368                         FULL_NAME +
1369                         SEPARATOR +
1370                         listOf(2222, 3333).toString()
1371                 )
1372 
1373             systemClock.setCurrentTimeMillis(200L)
1374             flow.emit(listOf(3333, 4444))
1375             assertThat(dumpLog())
1376                 .contains(
1377                     TABLE_LOG_DATE_FORMAT.format(200L) +
1378                         SEPARATOR +
1379                         FULL_NAME +
1380                         SEPARATOR +
1381                         listOf(3333, 4444).toString()
1382                 )
1383 
1384             // Doesn't log duplicates
1385             systemClock.setCurrentTimeMillis(300L)
1386             flow.emit(listOf(3333, 4444))
1387             assertThat(dumpLog())
1388                 .doesNotContain(
1389                     TABLE_LOG_DATE_FORMAT.format(300L) +
1390                         SEPARATOR +
1391                         FULL_NAME +
1392                         SEPARATOR +
1393                         listOf(3333, 4444).toString()
1394                 )
1395 
1396             job.cancel()
1397         }
1398 
1399     private fun dumpLog(): String {
1400         val outputWriter = StringWriter()
1401         tableLogBuffer.dump(PrintWriter(outputWriter), arrayOf())
1402         return outputWriter.toString()
1403     }
1404 
1405     class TestDiffable(
1406         private val testInt: Int,
1407         private val testString: String,
1408         private val testBoolean: Boolean,
1409     ) : Diffable<TestDiffable> {
1410         override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
1411             row.logChange(COL_FULL, false)
1412 
1413             if (testInt != prevVal.testInt) {
1414                 row.logChange(COL_INT, testInt)
1415             }
1416             if (testString != prevVal.testString) {
1417                 row.logChange(COL_STRING, testString)
1418             }
1419             if (testBoolean != prevVal.testBoolean) {
1420                 row.logChange(COL_BOOLEAN, testBoolean)
1421             }
1422         }
1423 
1424         override fun logFull(row: TableRowLogger) {
1425             row.logChange(COL_FULL, true)
1426             row.logChange(COL_INT, testInt)
1427             row.logChange(COL_STRING, testString)
1428             row.logChange(COL_BOOLEAN, testBoolean)
1429         }
1430 
1431         companion object {
1432             const val COL_INT = "intColumn"
1433             const val COL_STRING = "stringColumn"
1434             const val COL_BOOLEAN = "booleanColumn"
1435             const val COL_FULL = "loggedFullColumn"
1436         }
1437     }
1438 
1439     private companion object {
1440         const val MAX_SIZE = 50
1441         const val BUFFER_NAME = "LogDiffsForTableTest"
1442         const val COLUMN_PREFIX = "columnPrefix"
1443         const val COLUMN_NAME = "columnName"
1444         const val FULL_NAME = "$COLUMN_PREFIX.$COLUMN_NAME"
1445         private const val SEPARATOR = "|"
1446     }
1447 }
1448