1 /*
2  * Copyright (C) 2020 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.dump
18 
19 import androidx.test.filters.SmallTest
20 import com.android.systemui.SysuiTestCase
21 import com.android.systemui.dump.DumpHandler.Companion.dump
22 import com.android.systemui.log.LogBuffer
23 import com.android.systemui.util.io.FakeBasicFileAttributes
24 import com.android.systemui.util.io.Files
25 import com.android.systemui.util.mockito.any
26 import com.android.systemui.util.mockito.eq
27 import com.android.systemui.util.mockito.whenever
28 import com.android.systemui.util.time.FakeSystemClock
29 import java.io.BufferedWriter
30 import java.io.ByteArrayOutputStream
31 import java.io.IOException
32 import java.io.OutputStreamWriter
33 import java.io.PrintWriter
34 import java.nio.file.LinkOption
35 import java.nio.file.OpenOption
36 import java.nio.file.Paths
37 import java.nio.file.attribute.BasicFileAttributes
38 import java.util.Arrays
39 import org.junit.Assert.assertEquals
40 import org.junit.Assert.assertTrue
41 import org.junit.Before
42 import org.junit.Test
43 import org.mockito.ArgumentMatchers.anyInt
44 import org.mockito.Mock
45 import org.mockito.Mockito
46 import org.mockito.Mockito.never
47 import org.mockito.Mockito.verify
48 import org.mockito.MockitoAnnotations
49 
50 @SmallTest
51 class LogEulogizerTest : SysuiTestCase() {
52 
53     lateinit var eulogizer: LogBufferEulogizer
54 
55     @Mock lateinit var dumpManager: DumpManager
56     @Mock lateinit var logBuffer1: LogBuffer
57     lateinit var logBufferEntry1: DumpsysEntry.LogBufferEntry
58     @Mock lateinit var logBuffer2: LogBuffer
59     lateinit var logBufferEntry2: DumpsysEntry.LogBufferEntry
60 
61     @Mock lateinit var files: Files
62 
63     private val clock = FakeSystemClock()
64 
65     private val path = Paths.get("/foo/bar/baz.txt")
66     private val fileAttrs = FakeBasicFileAttributes()
67     private val fileStream = ByteArrayOutputStream()
68     private val fileWriter = BufferedWriter(OutputStreamWriter(fileStream))
69 
70     private val dumpStream = ByteArrayOutputStream()
71     private val dumpWriter = PrintWriter(OutputStreamWriter(dumpStream))
72 
73     @Before
74     fun setUp() {
75         MockitoAnnotations.initMocks(this)
76         logBufferEntry1 = DumpsysEntry.LogBufferEntry(logBuffer1, "logbuffer1")
77         logBufferEntry2 = DumpsysEntry.LogBufferEntry(logBuffer2, "logbuffer2")
78 
79         eulogizer = LogBufferEulogizer(dumpManager, clock, files, path, MIN_WRITE_GAP, MAX_READ_AGE)
80 
81         Mockito.`when`(files.newBufferedWriter(eq(path), any(OpenOption::class.java)))
82             .thenReturn(fileWriter)
83 
84         Mockito.`when`(
85                 files.readAttributes(
86                     eq(path),
87                     eq(BasicFileAttributes::class.java),
88                     any(LinkOption::class.java)
89                 )
90             )
91             .thenReturn(fileAttrs)
92 
93         Mockito.`when`(files.lines(eq(path))).thenReturn(Arrays.stream(FAKE_LINES))
94 
95         whenever(dumpManager.getLogBuffers()).thenReturn(listOf(logBufferEntry1, logBufferEntry2))
96     }
97 
98     @Test
99     fun testFileIsCreated() {
100         // GIVEN that the log file doesn't already exist
101         Mockito.`when`(
102                 files.readAttributes(
103                     eq(path),
104                     eq(BasicFileAttributes::class.java),
105                     any(LinkOption::class.java)
106                 )
107             )
108             .thenThrow(IOException("File not found"))
109 
110         // WHEN .record() is called
111         val exception = RuntimeException("Something bad happened")
112         assertEquals(exception, eulogizer.record(exception))
113 
114         // THEN the buffers are dumped to the file
115         verify(logBuffer1).dump(any(PrintWriter::class.java), anyInt())
116         verify(logBuffer2).dump(any(PrintWriter::class.java), anyInt())
117         assertTrue(fileStream.toString().isNotEmpty())
118     }
119 
120     @Test
121     fun testExistingFileIsOverwritten() {
122         // GIVEN that the log file already exists but hasn't been modified in a while
123         fileAttrs.setLastModifiedTime(clock.currentTimeMillis() - MIN_WRITE_GAP - 20)
124 
125         // WHEN .record() is called
126         val exception = RuntimeException("Something bad happened")
127         assertEquals(exception, eulogizer.record(exception))
128 
129         // THEN the buffers are dumped to the file
130         verify(logBuffer1).dump(any(PrintWriter::class.java), anyInt())
131         verify(logBuffer2).dump(any(PrintWriter::class.java), anyInt())
132         assertTrue(fileStream.toString().isNotEmpty())
133     }
134 
135     @Test
136     fun testYoungFileIsNotOverwritten() {
137         // GIVEN that the log file has been modified recently
138         fileAttrs.setLastModifiedTime(clock.currentTimeMillis() - MIN_WRITE_GAP + 7)
139 
140         // WHEN .record() is called
141         val exception = RuntimeException("Something bad happened")
142         assertEquals(exception, eulogizer.record(exception))
143 
144         // THEN the file isn't written to
145         verify(logBuffer1, never()).dump(any(PrintWriter::class.java), anyInt())
146         verify(logBuffer2, never()).dump(any(PrintWriter::class.java), anyInt())
147         assertTrue(fileStream.toString().isEmpty())
148     }
149 
150     @Test
151     fun testRecentFileIsDumped() {
152         // GIVEN that the log file was written to "recently"
153         fileAttrs.setLastModifiedTime(clock.currentTimeMillis() - MAX_READ_AGE + 7)
154 
155         // WHEN we're asked to eulogize the log
156         eulogizer.readEulogyIfPresent(dumpWriter)
157         dumpWriter.close()
158 
159         // THEN the log file is written to the output stream
160         verify(files).lines(eq(path))
161         assertTrue(dumpStream.toString().isNotBlank())
162     }
163 
164     @Test
165     fun testOldFileIsNotDumped() {
166         // GIVEN that the log file was written to a long time ago
167         fileAttrs.setLastModifiedTime(clock.currentTimeMillis() - MAX_READ_AGE - 7)
168 
169         // WHEN we're asked to eulogize the log
170         eulogizer.readEulogyIfPresent(dumpWriter)
171         dumpWriter.close()
172 
173         // THEN the log file is NOT written to the output stream
174         verify(files, never()).lines(eq(path))
175         assertTrue(dumpStream.toString().isEmpty())
176     }
177 }
178 
179 private const val MIN_WRITE_GAP = 10L
180 private const val MAX_READ_AGE = 100L
181 
182 private val FAKE_LINES = arrayOf("First line", "Second line", "Third line")
183