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 package com.android.helpers.tests;
17 
18 import static org.junit.Assert.assertEquals;
19 import static org.junit.Assert.assertFalse;
20 import static org.junit.Assert.assertNull;
21 import static org.junit.Assert.assertTrue;
22 
23 import androidx.test.runner.AndroidJUnit4;
24 
25 import com.android.helpers.BugReportDurationHelper;
26 import com.android.helpers.BugReportDurationHelper.BugReportDurationLines;
27 import com.android.helpers.BugReportDurationHelper.DumpstateBoardLines;
28 
29 import org.junit.After;
30 import org.junit.Before;
31 import org.junit.Test;
32 import org.junit.runner.RunWith;
33 
34 import java.io.BufferedOutputStream;
35 import java.io.File;
36 import java.io.FileOutputStream;
37 import java.io.IOException;
38 import java.nio.file.Files;
39 import java.util.ArrayList;
40 import java.util.Arrays;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.zip.ZipEntry;
44 import java.util.zip.ZipOutputStream;
45 
46 /**
47  * Android Unit tests for {@link BugReportDurationHelper}.
48  *
49  * <p>atest CollectorsHelperTest:com.android.helpers.BugReportDurationHelperTest
50  */
51 @RunWith(AndroidJUnit4.class)
52 public class BugReportDurationHelperTest {
53 
54     private static final String TAG = BugReportDurationHelperTest.class.getSimpleName();
55 
56     // Comparison of floating-point numbers with assertEquals requires a maximum delta.
57     private static final double DELTA = 0.00001;
58 
59     private BugReportDurationHelper helper;
60     private File testDir;
61 
62     @Before
setUp()63     public void setUp() throws IOException {
64         testDir = Files.createTempDirectory("test_dir").toFile();
65         helper = new BugReportDurationHelper(testDir.getPath());
66         helper.startCollecting();
67     }
68 
69     @After
tearDown()70     public void tearDown() {
71         // stopCollecting() is currently a no-op but is included here in case it is updated.
72         helper.stopCollecting();
73 
74         // Deletes the files in the test directory, then the test directory.
75         File[] files = testDir.listFiles();
76         for (File f : files) {
77             if (f.isFile()) {
78                 f.delete();
79             }
80         }
81         testDir.delete();
82     }
83 
84     // Creates a .zip archive with an identically-named .txt file and a dumpstate_board file
85     // containing the input lines.
createArchive( String name, List<String> bugReportLines, List<String> dumpstateBoardLines)86     private File createArchive(
87             String name, List<String> bugReportLines, List<String> dumpstateBoardLines)
88             throws IOException {
89         File f = new File(testDir, name + ".zip");
90         FileOutputStream fos = new FileOutputStream(f);
91         BufferedOutputStream bos = new BufferedOutputStream(fos);
92         ZipOutputStream zos = new ZipOutputStream(bos);
93         try {
94             if (bugReportLines != null) {
95                 zos.putNextEntry(new ZipEntry(name + ".txt"));
96                 for (String line : bugReportLines) {
97                     zos.write((line + "\n").getBytes());
98                 }
99                 zos.closeEntry();
100             }
101             if (dumpstateBoardLines != null) {
102                 zos.putNextEntry(new ZipEntry("dumpstate_board.txt"));
103                 for (String line : dumpstateBoardLines) {
104                     zos.write((line + "\n").getBytes());
105                 }
106                 zos.closeEntry();
107             }
108         } finally {
109             zos.close();
110         }
111         return f;
112     }
113 
114     @Test
testGetMetrics()115     public void testGetMetrics() throws IOException {
116         List<String> bugReportLines =
117                 Arrays.asList(
118                         "------ 44.619s was the duration of \'dumpstate_board()\' ------",
119                         "------ 21.397s was the duration of \'DUMPSYS\' ------",
120                         "------ 0.022s was the duration of \'DUMPSYS CRITICAL PROTO\' ------",
121                         "--------- 0.051s was the duration of dumpsys SurfaceFlinger, ending at:"
122                                 + " 2023-04-27 23:50:35",
123                         "--------- 24.741s was the duration of dumpsys meminfo, ending at:"
124                                 + " 2023-04-27 23:51:38",
125                         "unrelated log line");
126         List<String> dumpstateBoardLines =
127                 Arrays.asList(
128                         "------ Section end: dump_display ------",
129                         "Elapsed msec: 103",
130                         "------ Section end: dump_modemlog ------",
131                         "Elapsed msec: 12532",
132                         "------ Section end: dump_thermal.sh ------",
133                         "Elapsed msec: 7705",
134                         "unrelated line");
135 
136         createArchive("bugreport", bugReportLines, dumpstateBoardLines);
137 
138         Map<String, Double> metrics = helper.getMetrics();
139         assertEquals(9, metrics.size());
140         assertEquals(44.619, metrics.get("bugreport-duration-dumpstate_board()"), DELTA);
141         assertEquals(21.397, metrics.get("bugreport-duration-dumpsys"), DELTA);
142         assertEquals(0.022, metrics.get("bugreport-duration-dumpsys-critical-proto"), DELTA);
143         assertEquals(0.051, metrics.get("bugreport-dumpsys-duration-SurfaceFlinger"), DELTA);
144         assertEquals(24.741, metrics.get("bugreport-dumpsys-duration-meminfo"), DELTA);
145         assertEquals(103, metrics.get("bugreport-dumpstate_board-duration-dump_display"), DELTA);
146         assertEquals(12532, metrics.get("bugreport-dumpstate_board-duration-dump_modemlog"), DELTA);
147         assertEquals(
148                 7705, metrics.get("bugreport-dumpstate_board-duration-dump_thermal.sh"), DELTA);
149         assertEquals(0, metrics.get("dumpstate-section-timeout-count"), DELTA);
150     }
151 
152     @Test
testGetLatestBugReport()153     public void testGetLatestBugReport() throws IOException {
154         List<String> empty = new ArrayList<>();
155         createArchive("bugreport-2022-04-23-03-12-33", empty, empty);
156         createArchive("bugreport-2022-04-20-21-44-11", empty, empty);
157         createArchive("bugreport-2021-12-28-10-32-10", empty, empty);
158         assertEquals("bugreport-2022-04-23-03-12-33.zip", helper.getLatestBugReport());
159     }
160 
161     @Test
testExtractAndFilterBugReport()162     public void testExtractAndFilterBugReport() throws IOException {
163         // dumpstate section lines
164         String dumpstateLine1 = "------ 44.619s was the duration of \'dumpstate_board()\' ------";
165         String dumpstateLine2 = "------ 21.397s was the duration of \'DUMPSYS\' ------";
166 
167         // dumpsys section lines
168         String dumpsysLine1 =
169                 "--------- 0.051s was the duration of dumpsys SurfaceFlinger, ending at: 2023-04-27"
170                         + " 23:50:35";
171         String dumpsysLine2 =
172                 "--------- 24.741s was the duration of dumpsys meminfo, ending at: 2023-04-27"
173                         + " 23:51:38";
174 
175         // Lines that should be filtered out
176         String invalidLine = "unrelated log line";
177         String showmapLine =
178                 "------ 0.076s was the duration of \'SHOW MAP 22930 (com.android.chrome)\' ------";
179 
180         // Timeout lines should be gathered from logs instead of the "raw" text, as the latter is a
181         // subset of the former.
182         String validTimeoutLine =
183                 "02-12 16:34:21.826 shell 14095 14095 E dumpstate: "
184                         + "*** command '/system/xbin/su root bugreport_procdump' "
185                         + "timed out after 10.002s (killing pid 15063)";
186         String invalidTimeoutLine =
187                 "*** command '/system/xbin/su root bugreport_procdump' "
188                         + "timed out after 10.002s (killing pid 15063)";
189         List<String> lines =
190                 Arrays.asList(
191                         dumpstateLine1,
192                         dumpstateLine2,
193                         dumpsysLine1,
194                         dumpsysLine2,
195                         invalidLine,
196                         showmapLine,
197                         validTimeoutLine,
198                         invalidTimeoutLine);
199 
200         File archive = createArchive("bugreport", lines, null);
201 
202         String zip = archive.getName();
203 
204         BugReportDurationLines filtered = helper.extractAndFilterBugReport(zip);
205         assertTrue(filtered.contains(dumpstateLine1));
206         assertTrue(filtered.contains(dumpstateLine2));
207         assertTrue(filtered.contains(dumpsysLine1));
208         assertTrue(filtered.contains(dumpsysLine2));
209         assertTrue(filtered.contains(validTimeoutLine));
210         assertFalse(filtered.contains(invalidLine));
211         assertFalse(filtered.contains(showmapLine));
212         assertFalse(filtered.contains(invalidTimeoutLine));
213     }
214 
215     @Test
testExtractAndFilterDumpstateBoard()216     public void testExtractAndFilterDumpstateBoard() throws IOException {
217         String sectionLine1 = "------ Section end: dump_display ------";
218         String sectionLine2 = "------ Section end: dump_modemlog ------";
219         String sectionLine3 = "------ Section end: dump_thermal.sh ------";
220         String durationLine1 = "Elapsed msec: 103";
221         String durationLine2 = "Elapsed msec: 12532";
222         String durationLine3 = "Elapsed msec: 7705";
223 
224         // Lines that should be filtered out
225         String invalidLine = "unrelated log line";
226         List<String> lines =
227                 Arrays.asList(
228                         sectionLine1,
229                         durationLine1,
230                         sectionLine2,
231                         durationLine2,
232                         sectionLine3,
233                         durationLine3,
234                         invalidLine);
235 
236         File archive = createArchive("bugreport", null, lines);
237 
238         String zip = archive.getName();
239 
240         DumpstateBoardLines filtered = helper.extractAndFilterDumpstateBoard(zip);
241         assertTrue(filtered.contains(sectionLine1));
242         assertTrue(filtered.contains(sectionLine2));
243         assertTrue(filtered.contains(sectionLine3));
244         assertTrue(filtered.contains(durationLine1));
245         assertTrue(filtered.contains(durationLine2));
246         assertTrue(filtered.contains(durationLine3));
247         assertFalse(filtered.contains(invalidLine));
248     }
249 
250     @Test
testParseDecimalDurationForDumpstate()251     public void testParseDecimalDurationForDumpstate() {
252         String line1 = "------ 44.619s was the duration of \'dumpstate_board()\' ------";
253         String line2 = "------ 21.397s was the duration of \'DUMPSYS\' ------";
254         // This isn't a "real" case, since parseDecimalDuration() is only ever passed valid lines.
255         String invalidLine = "unrelated log line";
256         assertEquals(44.619, helper.parseDecimalDuration(line1), DELTA);
257         assertEquals(21.397, helper.parseDecimalDuration(line2), DELTA);
258         assertEquals(-1, helper.parseDecimalDuration(invalidLine), DELTA);
259     }
260 
261     @Test
testParseDecimalDurationForDumpsys()262     public void testParseDecimalDurationForDumpsys() {
263         String line1 =
264                 "--------- 0.051s was the duration of dumpsys SurfaceFlinger, ending at: 2023-04-27"
265                         + " 23:50:35";
266         String line2 =
267                 "--------- 24.741s was the duration of dumpsys meminfo, ending at: 2023-04-27"
268                         + " 23:51:38";
269         String line3 =
270                 "--------- 0.044s was the duration of dumpsys"
271                     + " android.frameworks.stats.IStats/default, ending at: 2023-04-27 23:51:40";
272         assertEquals(0.051, helper.parseDecimalDuration(line1), DELTA);
273         assertEquals(24.741, helper.parseDecimalDuration(line2), DELTA);
274         assertEquals(0.044, helper.parseDecimalDuration(line3), DELTA);
275     }
276 
277     @Test
testParseIntegerDurationForDumpstateBoard()278     public void testParseIntegerDurationForDumpstateBoard() {
279         String line1 = "Elapsed msec: 103";
280         String line2 = "Elapsed msec: 12532";
281         String line3 = "Elapsed msec: 7705";
282         assertEquals(103, helper.parseIntegerDuration(line1), DELTA);
283         assertEquals(12532, helper.parseIntegerDuration(line2), DELTA);
284         assertEquals(7705, helper.parseIntegerDuration(line3), DELTA);
285     }
286 
287     @Test
testParseDumpstateSection()288     public void testParseDumpstateSection() {
289         String line1 = "------ 44.619s was the duration of \'dumpstate_board()\' ------";
290         String line2 = "------ 21.397s was the duration of \'DUMPSYS\' ------";
291         // This isn't a "real" case, since parseDumpstateSection() is only ever passed valid lines.
292         String invalidLine = "unrelated log line";
293         assertEquals("dumpstate_board()", helper.parseDumpstateSection(line1));
294         assertEquals("DUMPSYS", helper.parseDumpstateSection(line2));
295         assertNull(helper.parseDumpstateSection(invalidLine));
296     }
297 
298     @Test
testParseDumpsysSection()299     public void testParseDumpsysSection() {
300         String line1 =
301                 "--------- 0.051s was the duration of dumpsys SurfaceFlinger, ending at: 2023-04-27"
302                         + " 23:50:35";
303         String line2 =
304                 "--------- 24.741s was the duration of dumpsys meminfo, ending at: 2023-04-27"
305                         + " 23:51:38";
306         String line3 =
307                 "--------- 0.044s was the duration of dumpsys"
308                     + " android.frameworks.stats.IStats/default, ending at: 2023-04-27 23:51:40";
309         assertEquals("SurfaceFlinger", helper.parseDumpsysSection(line1));
310         assertEquals("meminfo", helper.parseDumpsysSection(line2));
311         assertEquals("android.frameworks.stats.IStats/default", helper.parseDumpsysSection(line3));
312     }
313 
314     @Test
testParseDumpstateBoardSection()315     public void testParseDumpstateBoardSection() {
316         String line1 = "------ Section end: dump_display ------";
317         String line2 = "------ Section end: dump_modemlog ------";
318         String line3 = "------ Section end: dump_thermal.sh ------";
319         assertEquals("dump_display", helper.parseDumpstateBoardSection(line1));
320         assertEquals("dump_modemlog", helper.parseDumpstateBoardSection(line2));
321         assertEquals("dump_thermal.sh", helper.parseDumpstateBoardSection(line3));
322     }
323 
324     @Test
testConvertDumpstateSectionToKey()325     public void testConvertDumpstateSectionToKey() {
326         String dumpstate1 = "PROCRANK";
327         String dumpstate2 = "PROCESSES AND THREADS";
328         String dumpstate3 = "dumpstate_board()";
329         assertEquals(
330                 "bugreport-duration-procrank", helper.convertDumpstateSectionToKey(dumpstate1));
331         assertEquals(
332                 "bugreport-duration-processes-and-threads",
333                 helper.convertDumpstateSectionToKey(dumpstate2));
334         assertEquals(
335                 "bugreport-duration-dumpstate_board()",
336                 helper.convertDumpstateSectionToKey(dumpstate3));
337     }
338 
339     @Test
testConvertDumpsysSectionToKey()340     public void testConvertDumpsysSectionToKey() {
341         String dumpsys1 = "SurfaceFlinger";
342         String dumpsys2 = "meminfo";
343         String dumpsys3 = "android.frameworks.stats.IStats/default";
344         assertEquals(
345                 "bugreport-dumpsys-duration-SurfaceFlinger",
346                 helper.convertDumpsysSectionToKey(dumpsys1));
347         assertEquals(
348                 "bugreport-dumpsys-duration-meminfo", helper.convertDumpsysSectionToKey(dumpsys2));
349         assertEquals(
350                 "bugreport-dumpsys-duration-android.frameworks.stats.IStats/default",
351                 helper.convertDumpsysSectionToKey(dumpsys3));
352     }
353 
354     @Test
testConvertDumpstateBoardSectionToKey()355     public void testConvertDumpstateBoardSectionToKey() {
356         String dumpstateBoard1 = "dump_display";
357         String dumpstateBoard2 = "dump_modemlog";
358         String dumpstateBoard3 = "dump_thermal.sh";
359         assertEquals(
360                 "bugreport-dumpstate_board-duration-dump_display",
361                 helper.convertDumpstateBoardSectionToKey(dumpstateBoard1));
362         assertEquals(
363                 "bugreport-dumpstate_board-duration-dump_modemlog",
364                 helper.convertDumpstateBoardSectionToKey(dumpstateBoard2));
365         assertEquals(
366                 "bugreport-dumpstate_board-duration-dump_thermal.sh",
367                 helper.convertDumpstateBoardSectionToKey(dumpstateBoard3));
368     }
369 }
370