blob: 6801106f70b57a5be1715cfdc6417a9c69fc3353 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2013 The ChromiumOS Authors
Mike Frysinger69cb41d2013-08-11 20:08:19 -04002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Don Garrett25f309a2014-03-19 14:02:12 -07005"""Test cros_generate_breakpad_symbols."""
6
Mike Frysinger69cb41d2013-08-11 20:08:19 -04007import ctypes
Mike Frysinger374ba4f2019-11-14 23:45:15 -05008import io
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -08009import logging
10import multiprocessing
Mike Frysinger69cb41d2013-08-11 20:08:19 -040011import os
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -080012import pathlib
Mike Frysinger166fea02021-02-12 05:30:33 -050013from unittest import mock
Mike Frysinger69cb41d2013-08-11 20:08:19 -040014
Mike Frysinger69cb41d2013-08-11 20:08:19 -040015from chromite.lib import cros_test_lib
16from chromite.lib import osutils
Mike Frysinger69cb41d2013-08-11 20:08:19 -040017from chromite.lib import parallel_unittest
18from chromite.lib import partial_mock
19from chromite.scripts import cros_generate_breakpad_symbols
20
Mike Frysinger69cb41d2013-08-11 20:08:19 -040021
22class FindDebugDirMock(partial_mock.PartialMock):
Alex Klein1699fab2022-09-08 08:46:06 -060023 """Mock out the DebugDir helper so we can point it to a tempdir."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -040024
Alex Klein1699fab2022-09-08 08:46:06 -060025 TARGET = "chromite.scripts.cros_generate_breakpad_symbols"
26 ATTRS = ("FindDebugDir",)
27 DEFAULT_ATTR = "FindDebugDir"
Mike Frysinger69cb41d2013-08-11 20:08:19 -040028
Alex Klein1699fab2022-09-08 08:46:06 -060029 def __init__(self, path, *args, **kwargs):
30 self.path = path
31 super().__init__(*args, **kwargs)
Mike Frysinger69cb41d2013-08-11 20:08:19 -040032
Alex Klein1699fab2022-09-08 08:46:06 -060033 # pylint: disable=unused-argument
34 def FindDebugDir(self, _board, sysroot=None):
35 return self.path
Mike Frysinger69cb41d2013-08-11 20:08:19 -040036
37
Mike Frysinger3ef6d972019-08-24 20:07:42 -040038# This long decorator triggers a false positive in the docstring test.
39# https://github.com/PyCQA/pylint/issues/3077
40# pylint: disable=bad-docstring-quotes
Alex Klein1699fab2022-09-08 08:46:06 -060041@mock.patch(
42 "chromite.scripts.cros_generate_breakpad_symbols." "GenerateBreakpadSymbol"
43)
Mike Frysinger69cb41d2013-08-11 20:08:19 -040044class GenerateSymbolsTest(cros_test_lib.MockTempDirTestCase):
Alex Klein1699fab2022-09-08 08:46:06 -060045 """Test GenerateBreakpadSymbols."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -040046
Alex Klein1699fab2022-09-08 08:46:06 -060047 def setUp(self):
48 self.board = "monkey-board"
49 self.board_dir = os.path.join(self.tempdir, "build", self.board)
50 self.debug_dir = os.path.join(self.board_dir, "usr", "lib", "debug")
51 self.breakpad_dir = os.path.join(self.debug_dir, "breakpad")
Mike Frysinger69cb41d2013-08-11 20:08:19 -040052
Alex Klein1699fab2022-09-08 08:46:06 -060053 # Generate a tree of files which we'll scan through.
54 elf_files = [
55 "bin/elf",
56 "iii/large-elf",
57 # Need some kernel modules (with & without matching .debug).
58 "lib/modules/3.10/module.ko",
59 "lib/modules/3.10/module-no-debug.ko",
60 # Need a file which has an ELF only, but not a .debug.
61 "usr/bin/elf-only",
62 "usr/sbin/elf",
63 ]
64 debug_files = [
65 "bin/bad-file",
66 "bin/elf.debug",
67 "iii/large-elf.debug",
68 "boot/vmlinux.debug",
69 "lib/modules/3.10/module.ko.debug",
70 # Need a file which has a .debug only, but not an ELF.
71 "sbin/debug-only.debug",
72 "usr/sbin/elf.debug",
73 ]
74 for f in [os.path.join(self.board_dir, x) for x in elf_files] + [
75 os.path.join(self.debug_dir, x) for x in debug_files
76 ]:
77 osutils.Touch(f, makedirs=True)
Mike Frysinger69cb41d2013-08-11 20:08:19 -040078
Alex Klein1699fab2022-09-08 08:46:06 -060079 # Set up random build dirs and symlinks.
80 buildid = os.path.join(self.debug_dir, ".build-id", "00")
81 osutils.SafeMakedirs(buildid)
82 os.symlink("/asdf", os.path.join(buildid, "foo"))
83 os.symlink("/bin/sh", os.path.join(buildid, "foo.debug"))
84 os.symlink("/bin/sh", os.path.join(self.debug_dir, "file.debug"))
85 osutils.WriteFile(
86 os.path.join(self.debug_dir, "iii", "large-elf.debug"),
87 "just some content",
88 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -040089
Alex Klein1699fab2022-09-08 08:46:06 -060090 self.StartPatcher(FindDebugDirMock(self.debug_dir))
Mike Frysinger69cb41d2013-08-11 20:08:19 -040091
Alex Klein1699fab2022-09-08 08:46:06 -060092 def testNormal(self, gen_mock):
93 """Verify all the files we expect to get generated do"""
94 with parallel_unittest.ParallelMock():
95 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
96 self.board, sysroot=self.board_dir
97 )
98 self.assertEqual(ret, 0)
99 self.assertEqual(gen_mock.call_count, 5)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400100
Alex Klein1699fab2022-09-08 08:46:06 -0600101 # The largest ELF should be processed first.
102 call1 = (
103 os.path.join(self.board_dir, "iii/large-elf"),
104 os.path.join(self.debug_dir, "iii/large-elf.debug"),
105 )
106 self.assertEqual(gen_mock.call_args_list[0][0], call1)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400107
Alex Klein1699fab2022-09-08 08:46:06 -0600108 # The other ELFs can be called in any order.
109 call2 = (
110 os.path.join(self.board_dir, "bin/elf"),
111 os.path.join(self.debug_dir, "bin/elf.debug"),
112 )
113 call3 = (
114 os.path.join(self.board_dir, "usr/sbin/elf"),
115 os.path.join(self.debug_dir, "usr/sbin/elf.debug"),
116 )
117 call4 = (
118 os.path.join(self.board_dir, "lib/modules/3.10/module.ko"),
119 os.path.join(
120 self.debug_dir, "lib/modules/3.10/module.ko.debug"
121 ),
122 )
123 call5 = (
124 os.path.join(self.board_dir, "boot/vmlinux"),
125 os.path.join(self.debug_dir, "boot/vmlinux.debug"),
126 )
127 exp_calls = set((call2, call3, call4, call5))
128 actual_calls = set(
129 (
130 gen_mock.call_args_list[1][0],
131 gen_mock.call_args_list[2][0],
132 gen_mock.call_args_list[3][0],
133 gen_mock.call_args_list[4][0],
134 )
135 )
136 self.assertEqual(exp_calls, actual_calls)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400137
Alex Klein1699fab2022-09-08 08:46:06 -0600138 def testFileList(self, gen_mock):
139 """Verify that file_list restricts the symbols generated"""
140 with parallel_unittest.ParallelMock():
141 call1 = (
142 os.path.join(self.board_dir, "usr/sbin/elf"),
143 os.path.join(self.debug_dir, "usr/sbin/elf.debug"),
144 )
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700145
Alex Klein1699fab2022-09-08 08:46:06 -0600146 # Filter with elf path.
147 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
148 self.board,
149 sysroot=self.board_dir,
150 breakpad_dir=self.breakpad_dir,
151 file_list=[os.path.join(self.board_dir, "usr", "sbin", "elf")],
152 )
153 self.assertEqual(ret, 0)
154 self.assertEqual(gen_mock.call_count, 1)
155 self.assertEqual(gen_mock.call_args_list[0][0], call1)
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700156
Alex Klein1699fab2022-09-08 08:46:06 -0600157 # Filter with debug symbols file path.
158 gen_mock.reset_mock()
159 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
160 self.board,
161 sysroot=self.board_dir,
162 breakpad_dir=self.breakpad_dir,
163 file_list=[
164 os.path.join(self.debug_dir, "usr", "sbin", "elf.debug")
165 ],
166 )
167 self.assertEqual(ret, 0)
168 self.assertEqual(gen_mock.call_count, 1)
169 self.assertEqual(gen_mock.call_args_list[0][0], call1)
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700170
Alex Klein1699fab2022-09-08 08:46:06 -0600171 def testGenLimit(self, gen_mock):
172 """Verify generate_count arg works"""
173 with parallel_unittest.ParallelMock():
174 # Generate nothing!
175 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
176 self.board,
177 sysroot=self.board_dir,
178 breakpad_dir=self.breakpad_dir,
179 generate_count=0,
180 )
181 self.assertEqual(ret, 0)
182 self.assertEqual(gen_mock.call_count, 0)
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700183
Alex Klein1699fab2022-09-08 08:46:06 -0600184 # Generate just one.
185 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
186 self.board,
187 sysroot=self.board_dir,
188 breakpad_dir=self.breakpad_dir,
189 generate_count=1,
190 )
191 self.assertEqual(ret, 0)
192 self.assertEqual(gen_mock.call_count, 1)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400193
Alex Klein1699fab2022-09-08 08:46:06 -0600194 # The largest ELF should be processed first.
195 call1 = (
196 os.path.join(self.board_dir, "iii/large-elf"),
197 os.path.join(self.debug_dir, "iii/large-elf.debug"),
198 )
199 self.assertEqual(gen_mock.call_args_list[0][0], call1)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400200
Alex Klein1699fab2022-09-08 08:46:06 -0600201 def testGenErrors(self, gen_mock):
202 """Verify we handle errors from generation correctly"""
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400203
Alex Klein1699fab2022-09-08 08:46:06 -0600204 def _SetError(*_args, **kwargs):
205 kwargs["num_errors"].value += 1
206 return 1
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400207
Alex Klein1699fab2022-09-08 08:46:06 -0600208 gen_mock.side_effect = _SetError
209 with parallel_unittest.ParallelMock():
210 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
211 self.board, sysroot=self.board_dir
212 )
213 self.assertEqual(ret, 5)
214 self.assertEqual(gen_mock.call_count, 5)
Mike Frysinger9a628bb2013-10-24 15:51:37 -0400215
Alex Klein1699fab2022-09-08 08:46:06 -0600216 def testCleaningTrue(self, gen_mock):
217 """Verify behavior of clean_breakpad=True"""
218 with parallel_unittest.ParallelMock():
219 # Dir does not exist, and then does.
220 self.assertNotExists(self.breakpad_dir)
221 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
222 self.board,
223 sysroot=self.board_dir,
224 generate_count=1,
225 clean_breakpad=True,
226 )
227 self.assertEqual(ret, 0)
228 self.assertEqual(gen_mock.call_count, 1)
229 self.assertExists(self.breakpad_dir)
Mike Frysinger9a628bb2013-10-24 15:51:37 -0400230
Alex Klein1699fab2022-09-08 08:46:06 -0600231 # Dir exists before & after.
232 # File exists, but then doesn't.
233 stub_file = os.path.join(self.breakpad_dir, "fooooooooo")
234 osutils.Touch(stub_file)
235 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
236 self.board,
237 sysroot=self.board_dir,
238 generate_count=1,
239 clean_breakpad=True,
240 )
241 self.assertEqual(ret, 0)
242 self.assertEqual(gen_mock.call_count, 2)
243 self.assertNotExists(stub_file)
Mike Frysinger9a628bb2013-10-24 15:51:37 -0400244
Alex Klein1699fab2022-09-08 08:46:06 -0600245 def testCleaningFalse(self, gen_mock):
246 """Verify behavior of clean_breakpad=False"""
247 with parallel_unittest.ParallelMock():
248 # Dir does not exist, and then does.
249 self.assertNotExists(self.breakpad_dir)
250 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
251 self.board,
252 sysroot=self.board_dir,
253 generate_count=1,
254 clean_breakpad=False,
255 )
256 self.assertEqual(ret, 0)
257 self.assertEqual(gen_mock.call_count, 1)
258 self.assertExists(self.breakpad_dir)
Mike Frysinger9a628bb2013-10-24 15:51:37 -0400259
Alex Klein1699fab2022-09-08 08:46:06 -0600260 # Dir exists before & after.
261 # File exists before & after.
262 stub_file = os.path.join(self.breakpad_dir, "fooooooooo")
263 osutils.Touch(stub_file)
264 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
265 self.board,
266 sysroot=self.board_dir,
267 generate_count=1,
268 clean_breakpad=False,
269 )
270 self.assertEqual(ret, 0)
271 self.assertEqual(gen_mock.call_count, 2)
272 self.assertExists(stub_file)
273
274 def testExclusionList(self, gen_mock):
275 """Verify files in directories of the exclusion list are excluded"""
276 exclude_dirs = ["bin", "usr", "fake/dir/fake"]
277 with parallel_unittest.ParallelMock():
278 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
279 self.board, sysroot=self.board_dir, exclude_dirs=exclude_dirs
280 )
281 self.assertEqual(ret, 0)
282 self.assertEqual(gen_mock.call_count, 3)
283
Ian Barkley-Yeungb5274442023-04-28 16:32:20 -0700284 def testExpectedFilesCompleteFailure(self, _):
285 """Verify if no files are processed, all expected files give errors"""
286 with parallel_unittest.ParallelMock() and self.assertLogs(
287 level=logging.WARNING
288 ) as cm:
289 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
290 self.board, sysroot=self.board_dir
291 )
292 self.assertEqual(ret, 0)
293 self.assertIn(
294 "Not all expected files were processed successfully",
295 "\n".join(cm.output),
296 )
297 for output in cm.output:
298 if (
299 "Not all expected files were processed successfully"
300 in output
301 ):
302 # This is the line that lists all the files we didn't find.
303 for (
304 expected_file
305 ) in cros_generate_breakpad_symbols.ExpectedFiles:
306 self.assertIn(expected_file.name, output)
307
308 def testExpectedFilesPartialFailure(self, gen_mock):
309 """If some expected files are processed, the others give errors"""
310 expected_found = (
311 cros_generate_breakpad_symbols.ExpectedFiles.LIBC,
312 cros_generate_breakpad_symbols.ExpectedFiles.CRASH_REPORTER,
313 )
314
315 def _SetFound(*_args, **kwargs):
316 kwargs["found_files"].extend(expected_found)
317 gen_mock.side_effect = None
318 return 1
319
320 gen_mock.side_effect = _SetFound
321 with parallel_unittest.ParallelMock() and self.assertLogs(
322 level=logging.WARNING
323 ) as cm:
324 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
325 self.board, sysroot=self.board_dir
326 )
327 self.assertEqual(ret, 0)
328 self.assertIn(
329 "Not all expected files were processed successfully",
330 "\n".join(cm.output),
331 )
332 for output in cm.output:
333 if (
334 "Not all expected files were processed successfully"
335 in output
336 ):
337 # This is the line that lists all the files we didn't find.
338 for (
339 expected_file
340 ) in cros_generate_breakpad_symbols.ExpectedFiles:
341 if expected_file in expected_found:
342 self.assertNotIn(expected_file.name, output)
343 else:
344 self.assertIn(expected_file.name, output)
345
346 def testExpectedFilesWithSomeIgnored(self, _):
347 """If some expected files are ignored, they don't give errors"""
348 ignore_expected_files = [
349 cros_generate_breakpad_symbols.ExpectedFiles.ASH_CHROME,
350 cros_generate_breakpad_symbols.ExpectedFiles.LIBC,
351 ]
352 with parallel_unittest.ParallelMock() and self.assertLogs(
353 level=logging.WARNING
354 ) as cm:
355 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
356 self.board,
357 sysroot=self.board_dir,
358 ignore_expected_files=ignore_expected_files,
359 )
360 self.assertEqual(ret, 0)
361 self.assertIn(
362 "Not all expected files were processed successfully",
363 "\n".join(cm.output),
364 )
365 for output in cm.output:
366 if (
367 "Not all expected files were processed successfully"
368 in output
369 ):
370 # This is the line that lists all the files we didn't find.
371 for (
372 expected_file
373 ) in cros_generate_breakpad_symbols.ExpectedFiles:
374 if expected_file in ignore_expected_files:
375 self.assertNotIn(expected_file.name, output)
376 else:
377 self.assertIn(expected_file.name, output)
378
379 def testExpectedFilesWithAllIgnored(self, _):
380 """If all expected files are ignored, there is no error"""
381 with parallel_unittest.ParallelMock() and self.assertLogs(
382 level=logging.WARNING
383 ) as cm:
384 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
385 self.board,
386 sysroot=self.board_dir,
387 ignore_expected_files=list(
388 cros_generate_breakpad_symbols.ExpectedFiles
389 ),
390 )
391 self.assertEqual(ret, 0)
392 self.assertNotIn(
393 "Not all expected files were processed successfully",
394 "\n".join(cm.output),
395 )
396
397 def testExpectedFilesWithSomeIgnoredAndSomeFound(self, gen_mock):
398 """Some expected files are ignored, others processed => no error"""
399 expected_found = (
400 cros_generate_breakpad_symbols.ExpectedFiles.LIBC,
401 cros_generate_breakpad_symbols.ExpectedFiles.CRASH_REPORTER,
402 )
403
404 def _SetFound(*_args, **kwargs):
405 kwargs["found_files"].extend(expected_found)
406 gen_mock.side_effect = None
407 return 1
408
409 gen_mock.side_effect = _SetFound
410 with parallel_unittest.ParallelMock() and self.assertLogs(
411 level=logging.WARNING
412 ) as cm:
413 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
414 self.board,
415 sysroot=self.board_dir,
416 ignore_expected_files=[
417 cros_generate_breakpad_symbols.ExpectedFiles.ASH_CHROME,
418 cros_generate_breakpad_symbols.ExpectedFiles.LIBMETRICS,
419 ],
420 )
421 self.assertEqual(ret, 0)
422 self.assertNotIn(
423 "Not all expected files were processed successfully",
424 "\n".join(cm.output),
425 )
426
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400427
Benjamin Gordon121a2aa2018-05-04 16:24:45 -0600428class GenerateSymbolTest(cros_test_lib.RunCommandTempDirTestCase):
Alex Klein1699fab2022-09-08 08:46:06 -0600429 """Test GenerateBreakpadSymbol."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400430
Peter Boström763baca2023-05-25 16:36:33 +0000431 _DUMP_SYMS_BASE_CMD = ["dump_syms", "-v", "-d", "-m"]
432
Alex Klein1699fab2022-09-08 08:46:06 -0600433 def setUp(self):
434 self.elf_file = os.path.join(self.tempdir, "elf")
435 osutils.Touch(self.elf_file)
436 self.debug_dir = os.path.join(self.tempdir, "debug")
437 self.debug_file = os.path.join(self.debug_dir, "elf.debug")
438 osutils.Touch(self.debug_file, makedirs=True)
439 # Not needed as the code itself should create it as needed.
440 self.breakpad_dir = os.path.join(self.debug_dir, "breakpad")
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400441
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800442 self.FILE_OUT = (
443 f"{self.elf_file}: ELF 64-bit LSB pie executable, x86-64, "
444 "version 1 (SYSV), dynamically linked, interpreter "
445 "/lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, "
446 "BuildID[sha1]=cf9a21fa6b14bfb2dfcb76effd713c4536014d95, stripped"
447 )
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800448 # A symbol file which would pass validation.
449 MINIMAL_SYMBOL_FILE = (
450 "MODULE OS CPU ID NAME\n"
451 "PUBLIC f10 0 func\n"
452 "STACK CFI INIT f10 22 .cfa: $rsp 8 + .ra: .cfa -8 + ^\n"
453 )
454 self.rc.SetDefaultCmdResult(stdout=MINIMAL_SYMBOL_FILE)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800455 self.rc.AddCmdResult(
456 ["/usr/bin/file", self.elf_file], stdout=self.FILE_OUT
457 )
Alex Klein1699fab2022-09-08 08:46:06 -0600458 self.assertCommandContains = self.rc.assertCommandContains
459 self.sym_file = os.path.join(self.breakpad_dir, "NAME/ID/NAME.sym")
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400460
Alex Klein1699fab2022-09-08 08:46:06 -0600461 self.StartPatcher(FindDebugDirMock(self.debug_dir))
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400462
Alex Klein1699fab2022-09-08 08:46:06 -0600463 def assertCommandArgs(self, i, args):
464 """Helper for looking at the args of the |i|th call"""
465 self.assertEqual(self.rc.call_args_list[i][0][0], args)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400466
Alex Klein1699fab2022-09-08 08:46:06 -0600467 def testNormal(self):
468 """Normal run -- given an ELF and a debug file"""
469 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800470 self.elf_file,
471 self.debug_file,
472 self.breakpad_dir,
Alex Klein1699fab2022-09-08 08:46:06 -0600473 )
474 self.assertEqual(ret, self.sym_file)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800475 self.assertEqual(self.rc.call_count, 2)
Alex Klein1699fab2022-09-08 08:46:06 -0600476 self.assertCommandArgs(
Peter Boström763baca2023-05-25 16:36:33 +0000477 1, self._DUMP_SYMS_BASE_CMD + [self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600478 )
479 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400480
Alex Klein1699fab2022-09-08 08:46:06 -0600481 def testNormalNoCfi(self):
482 """Normal run w/out CFI"""
483 # Make sure the num_errors flag works too.
Trent Apted1e2e4f32023-05-05 03:50:20 +0000484 num_errors = ctypes.c_int(0)
Alex Klein1699fab2022-09-08 08:46:06 -0600485 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
486 self.elf_file,
487 breakpad_dir=self.breakpad_dir,
488 strip_cfi=True,
489 num_errors=num_errors,
490 )
491 self.assertEqual(ret, self.sym_file)
492 self.assertEqual(num_errors.value, 0)
Peter Boström763baca2023-05-25 16:36:33 +0000493 self.assertCommandArgs(
494 1, self._DUMP_SYMS_BASE_CMD + ["-c", self.elf_file]
495 )
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800496 self.assertEqual(self.rc.call_count, 2)
Alex Klein1699fab2022-09-08 08:46:06 -0600497 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400498
Alex Klein1699fab2022-09-08 08:46:06 -0600499 def testNormalElfOnly(self):
500 """Normal run -- given just an ELF"""
501 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
502 self.elf_file, breakpad_dir=self.breakpad_dir
503 )
504 self.assertEqual(ret, self.sym_file)
Peter Boström763baca2023-05-25 16:36:33 +0000505 self.assertCommandArgs(1, self._DUMP_SYMS_BASE_CMD + [self.elf_file])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800506 self.assertEqual(self.rc.call_count, 2)
Alex Klein1699fab2022-09-08 08:46:06 -0600507 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400508
Alex Klein1699fab2022-09-08 08:46:06 -0600509 def testNormalSudo(self):
510 """Normal run where ELF is readable only by root"""
511 with mock.patch.object(os, "access") as mock_access:
512 mock_access.return_value = False
513 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
514 self.elf_file, breakpad_dir=self.breakpad_dir
515 )
516 self.assertEqual(ret, self.sym_file)
517 self.assertCommandArgs(
Peter Boström763baca2023-05-25 16:36:33 +0000518 1, ["sudo", "--"] + self._DUMP_SYMS_BASE_CMD + [self.elf_file]
Alex Klein1699fab2022-09-08 08:46:06 -0600519 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400520
Alex Klein1699fab2022-09-08 08:46:06 -0600521 def testLargeDebugFail(self):
522 """Running w/large .debug failed, but retry worked"""
523 self.rc.AddCmdResult(
Peter Boström763baca2023-05-25 16:36:33 +0000524 self._DUMP_SYMS_BASE_CMD + [self.elf_file, self.debug_dir],
525 returncode=1,
Alex Klein1699fab2022-09-08 08:46:06 -0600526 )
527 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
528 self.elf_file, self.debug_file, self.breakpad_dir
529 )
530 self.assertEqual(ret, self.sym_file)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800531 self.assertEqual(self.rc.call_count, 4)
Alex Klein1699fab2022-09-08 08:46:06 -0600532 self.assertCommandArgs(
Peter Boström763baca2023-05-25 16:36:33 +0000533 1, self._DUMP_SYMS_BASE_CMD + [self.elf_file, self.debug_dir]
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800534 )
535 # The current fallback from _DumpExpectingSymbols() to
Peter Boström763baca2023-05-25 16:36:33 +0000536 # _DumpAllowingBasicFallback() causes the first dump_syms command to get
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800537 # repeated.
538 self.assertCommandArgs(
Peter Boström763baca2023-05-25 16:36:33 +0000539 2, self._DUMP_SYMS_BASE_CMD + [self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600540 )
541 self.assertCommandArgs(
Peter Boström763baca2023-05-25 16:36:33 +0000542 3,
543 self._DUMP_SYMS_BASE_CMD
544 + ["-c", "-r", self.elf_file, self.debug_dir],
Alex Klein1699fab2022-09-08 08:46:06 -0600545 )
546 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400547
Ian Barkley-Yeungb5274442023-04-28 16:32:20 -0700548 def testForceBasicFallback(self):
549 """Running with force_basic_fallback
550
551 Test force_basic_fallback goes straight to _DumpAllowingBasicFallback().
552 """
553 self.rc.AddCmdResult(
Peter Boström763baca2023-05-25 16:36:33 +0000554 self._DUMP_SYMS_BASE_CMD + [self.elf_file, self.debug_dir],
555 returncode=1,
Ian Barkley-Yeungb5274442023-04-28 16:32:20 -0700556 )
557 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
558 self.elf_file,
559 self.debug_file,
560 self.breakpad_dir,
561 force_basic_fallback=True,
562 )
563 self.assertEqual(ret, self.sym_file)
564 self.assertEqual(self.rc.call_count, 2)
565 # dump_syms -v should only happen once in _DumpAllowingBasicFallback()
566 # and not in _DumpExpectingSymbols(). We don't call /usr/bin/file in
567 # _ExpectGoodSymbols() either, so there's 2 fewer commands than
568 # in testLargeDebugFail.
569 self.assertCommandArgs(
Peter Boström763baca2023-05-25 16:36:33 +0000570 0, self._DUMP_SYMS_BASE_CMD + [self.elf_file, self.debug_dir]
Ian Barkley-Yeungb5274442023-04-28 16:32:20 -0700571 )
572 self.assertCommandArgs(
Peter Boström763baca2023-05-25 16:36:33 +0000573 1,
574 self._DUMP_SYMS_BASE_CMD
575 + ["-c", "-r", self.elf_file, self.debug_dir],
Ian Barkley-Yeungb5274442023-04-28 16:32:20 -0700576 )
577 self.assertExists(self.sym_file)
578
Alex Klein1699fab2022-09-08 08:46:06 -0600579 def testDebugFail(self):
580 """Running w/.debug always failed, but works w/out"""
581 self.rc.AddCmdResult(
Peter Boström763baca2023-05-25 16:36:33 +0000582 self._DUMP_SYMS_BASE_CMD + [self.elf_file, self.debug_dir],
583 returncode=1,
Alex Klein1699fab2022-09-08 08:46:06 -0600584 )
585 self.rc.AddCmdResult(
Peter Boström763baca2023-05-25 16:36:33 +0000586 self._DUMP_SYMS_BASE_CMD
587 + ["-c", "-r", self.elf_file, self.debug_dir],
Alex Klein1699fab2022-09-08 08:46:06 -0600588 returncode=1,
589 )
590 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
591 self.elf_file, self.debug_file, self.breakpad_dir
592 )
593 self.assertEqual(ret, self.sym_file)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800594 self.assertEqual(self.rc.call_count, 5)
Alex Klein1699fab2022-09-08 08:46:06 -0600595 self.assertCommandArgs(
Peter Boström763baca2023-05-25 16:36:33 +0000596 1, self._DUMP_SYMS_BASE_CMD + [self.elf_file, self.debug_dir]
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800597 )
598 # The current fallback from _DumpExpectingSymbols() to
Peter Boström763baca2023-05-25 16:36:33 +0000599 # _DumpAllowingBasicFallback() causes the first dump_syms command to get
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800600 # repeated.
601 self.assertCommandArgs(
Peter Boström763baca2023-05-25 16:36:33 +0000602 2, self._DUMP_SYMS_BASE_CMD + [self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600603 )
604 self.assertCommandArgs(
Peter Boström763baca2023-05-25 16:36:33 +0000605 3,
606 self._DUMP_SYMS_BASE_CMD
607 + ["-c", "-r", self.elf_file, self.debug_dir],
Alex Klein1699fab2022-09-08 08:46:06 -0600608 )
Peter Boström763baca2023-05-25 16:36:33 +0000609 self.assertCommandArgs(4, self._DUMP_SYMS_BASE_CMD + [self.elf_file])
Alex Klein1699fab2022-09-08 08:46:06 -0600610 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400611
Alex Klein1699fab2022-09-08 08:46:06 -0600612 def testCompleteFail(self):
613 """Running dump_syms always fails"""
614 self.rc.SetDefaultCmdResult(returncode=1)
615 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
616 self.elf_file, breakpad_dir=self.breakpad_dir
617 )
618 self.assertEqual(ret, 1)
619 # Make sure the num_errors flag works too.
Trent Apted1e2e4f32023-05-05 03:50:20 +0000620 num_errors = ctypes.c_int(0)
Alex Klein1699fab2022-09-08 08:46:06 -0600621 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
622 self.elf_file, breakpad_dir=self.breakpad_dir, num_errors=num_errors
623 )
624 self.assertEqual(ret, 1)
625 self.assertEqual(num_errors.value, 1)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400626
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800627 def testKernelObjects(self):
628 """Kernel object files should call _DumpAllowingBasicFallback()"""
629 ko_file = os.path.join(self.tempdir, "elf.ko")
630 osutils.Touch(ko_file)
631 self.rc.AddCmdResult(
Peter Boström763baca2023-05-25 16:36:33 +0000632 self._DUMP_SYMS_BASE_CMD + [ko_file, self.debug_dir],
633 returncode=1,
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800634 )
635 self.rc.AddCmdResult(
Peter Boström763baca2023-05-25 16:36:33 +0000636 self._DUMP_SYMS_BASE_CMD + ["-c", "-r", ko_file, self.debug_dir],
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800637 returncode=1,
638 )
639 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
640 ko_file, self.debug_file, self.breakpad_dir
641 )
642 self.assertEqual(ret, self.sym_file)
643 self.assertEqual(self.rc.call_count, 3)
644 # Only one call (at the beginning of _DumpAllowingBasicFallback())
645 # to "dump_syms -v"
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800646 self.assertCommandArgs(
Peter Boström763baca2023-05-25 16:36:33 +0000647 0, self._DUMP_SYMS_BASE_CMD + [ko_file, self.debug_dir]
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800648 )
Peter Boström763baca2023-05-25 16:36:33 +0000649 self.assertCommandArgs(
650 1,
651 self._DUMP_SYMS_BASE_CMD + ["-c", "-r", ko_file, self.debug_dir],
652 )
653 self.assertCommandArgs(2, self._DUMP_SYMS_BASE_CMD + [ko_file])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800654 self.assertExists(self.sym_file)
655
656 def testGoBinary(self):
657 """Go binaries should call _DumpAllowingBasicFallback()
658
659 Also tests that dump_syms failing with 'file contains no debugging
660 information' does not fail the script.
661 """
662 go_binary = os.path.join(self.tempdir, "goprogram")
663 osutils.Touch(go_binary)
664 go_debug_file = os.path.join(self.debug_dir, "goprogram.debug")
665 osutils.Touch(go_debug_file, makedirs=True)
666 FILE_OUT_GO = go_binary + (
667 ": ELF 64-bit LSB executable, x86-64, "
668 "version 1 (SYSV), statically linked, "
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800669 "Go BuildID=KKXVlL66E8Qmngr4qll9/5kOKGZw9I7TmNhoqKLqq/SiYVJam6w5Fo"
670 "39B3BtDo/ba8_ceezZ-3R4qEv6_-K, not stripped"
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800671 )
672 self.rc.AddCmdResult(["/usr/bin/file", go_binary], stdout=FILE_OUT_GO)
673 self.rc.AddCmdResult(
Peter Boström763baca2023-05-25 16:36:33 +0000674 self._DUMP_SYMS_BASE_CMD + [go_binary, self.debug_dir],
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800675 returncode=1,
676 )
677 self.rc.AddCmdResult(
Peter Boström763baca2023-05-25 16:36:33 +0000678 self._DUMP_SYMS_BASE_CMD + ["-c", "-r", go_binary, self.debug_dir],
679 returncode=1,
680 )
681 self.rc.AddCmdResult(
682 self._DUMP_SYMS_BASE_CMD + [go_binary],
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800683 returncode=1,
684 stderr=(
685 f"{go_binary}: file contains no debugging information "
686 '(no ".stab" or ".debug_info" sections)'
687 ),
688 )
Trent Apted1e2e4f32023-05-05 03:50:20 +0000689 num_errors = ctypes.c_int(0)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800690 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
691 go_binary, go_debug_file, self.breakpad_dir
692 )
693 self.assertEqual(ret, 0)
694 self.assertEqual(self.rc.call_count, 4)
695 self.assertCommandArgs(0, ["/usr/bin/file", go_binary])
696 # Only one call (at the beginning of _DumpAllowingBasicFallback())
697 # to "dump_syms -v"
698 self.assertCommandArgs(
Peter Boström763baca2023-05-25 16:36:33 +0000699 1, self._DUMP_SYMS_BASE_CMD + [go_binary, self.debug_dir]
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800700 )
701 self.assertCommandArgs(
Peter Boström763baca2023-05-25 16:36:33 +0000702 2,
703 self._DUMP_SYMS_BASE_CMD + ["-c", "-r", go_binary, self.debug_dir],
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800704 )
Peter Boström763baca2023-05-25 16:36:33 +0000705 self.assertCommandArgs(3, self._DUMP_SYMS_BASE_CMD + [go_binary])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800706 self.assertNotExists(self.sym_file)
707 self.assertEqual(num_errors.value, 0)
708
Ian Barkley-Yeunge1785762023-03-08 18:32:18 -0800709 def _testBinaryIsInLocalFallback(self, directory, filename):
710 binary = os.path.join(self.tempdir, directory, filename)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800711 osutils.Touch(binary, makedirs=True)
Ian Barkley-Yeunge1785762023-03-08 18:32:18 -0800712 debug_dir = os.path.join(self.debug_dir, directory)
713 debug_file = os.path.join(debug_dir, f"{filename}.debug")
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800714 osutils.Touch(debug_file, makedirs=True)
715 self.rc.AddCmdResult(["/usr/bin/file", binary], stdout=self.FILE_OUT)
716 self.rc.AddCmdResult(
Peter Boström763baca2023-05-25 16:36:33 +0000717 self._DUMP_SYMS_BASE_CMD + [binary, debug_dir], returncode=1
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800718 )
719 self.rc.AddCmdResult(
Peter Boström763baca2023-05-25 16:36:33 +0000720 self._DUMP_SYMS_BASE_CMD + ["-c", "-r", binary, debug_dir],
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800721 returncode=1,
722 )
723 self.rc.AddCmdResult(
Peter Boström763baca2023-05-25 16:36:33 +0000724 self._DUMP_SYMS_BASE_CMD + [binary],
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800725 returncode=1,
726 stderr=(
727 f"{binary}: file contains no debugging information "
728 '(no ".stab" or ".debug_info" sections)'
729 ),
730 )
Trent Apted1e2e4f32023-05-05 03:50:20 +0000731 num_errors = ctypes.c_int(0)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800732 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
733 binary, debug_file, self.breakpad_dir, sysroot=self.tempdir
734 )
735 self.assertEqual(ret, 0)
736 self.assertEqual(self.rc.call_count, 4)
737 self.assertCommandArgs(0, ["/usr/bin/file", binary])
738 # Only one call (at the beginning of _DumpAllowingBasicFallback())
739 # to "dump_syms -v"
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800740 self.assertCommandArgs(
Peter Boström763baca2023-05-25 16:36:33 +0000741 1, self._DUMP_SYMS_BASE_CMD + [binary, debug_dir]
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800742 )
Peter Boström763baca2023-05-25 16:36:33 +0000743 self.assertCommandArgs(
744 2, self._DUMP_SYMS_BASE_CMD + ["-c", "-r", binary, debug_dir]
745 )
746 self.assertCommandArgs(3, self._DUMP_SYMS_BASE_CMD + [binary])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800747 self.assertNotExists(self.sym_file)
748 self.assertEqual(num_errors.value, 0)
749
Ian Barkley-Yeunge1785762023-03-08 18:32:18 -0800750 def testAllowlist(self):
751 """Binaries in the allowlist should call _DumpAllowingBasicFallback()"""
752 self._testBinaryIsInLocalFallback("usr/bin", "goldctl")
753
754 def testUsrLocalSkip(self):
755 """Binaries in /usr/local should call _DumpAllowingBasicFallback()"""
756 self._testBinaryIsInLocalFallback("usr/local", "minidump_stackwalk")
757
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400758
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800759class ValidateSymbolFileTest(cros_test_lib.TempDirTestCase):
760 """Tests ValidateSymbolFile"""
761
762 def _GetTestdataFile(self, filename: str) -> str:
763 """Gets the path to a file in the testdata directory.
764
765 Args:
766 filename: The base filename of the file.
767
768 Returns:
769 A string with the complete path to the file.
770 """
771 return os.path.join(os.path.dirname(__file__), "testdata", filename)
772
773 def testValidSymbolFiles(self):
774 """Make sure ValidateSymbolFile passes on valid files"""
775
776 # All files are in the testdata/ subdirectory.
777 VALID_SYMBOL_FILES = [
778 # A "normal" symbol file from an executable.
779 "basic.sym",
780 # A "normal" symbol file from a shared library.
781 "basic_lib.sym",
782 # A symbol file with PUBLIC records but no FUNC records.
783 "public_only.sym",
784 # A symbol file with FUNC records but no PUBLIC records.
785 "func_only.sym",
786 # A symbol file with at least one of every line type.
787 "all_line_types.sym",
788 ]
789
790 for file in VALID_SYMBOL_FILES:
791 with self.subTest(
792 file=file
793 ), multiprocessing.Manager() as mp_manager:
794 found_files = mp_manager.list()
795 self.assertTrue(
796 cros_generate_breakpad_symbols.ValidateSymbolFile(
797 self._GetTestdataFile(file),
798 "/build/board/bin/foo",
799 "/build/board",
800 found_files,
801 )
802 )
803 self.assertFalse(found_files)
804
805 def testInvalidSymbolFiles(self):
806 """Make sure ValidateSymbolFile fails on invalid files.
807
808 This test only covers cases that return false, not cases that raise
809 exceptions.
810 """
811
812 class InvalidSymbolFile:
813 """The name of an invalid symbol file + the expected error msg."""
814
815 def __init__(self, filename, expected_errors):
816 self.filename = filename
817 self.expected_errors = expected_errors
818
819 INVALID_SYMBOL_FILES = [
820 InvalidSymbolFile(
821 "bad_no_func_or_public.sym",
822 [
823 "WARNING:root:/build/board/bin/foo: "
824 "Symbol file has no FUNC or PUBLIC records"
825 ],
826 ),
827 InvalidSymbolFile(
828 "bad_no_stack.sym",
829 [
830 "WARNING:root:/build/board/bin/foo: "
831 "Symbol file has no STACK records"
832 ],
833 ),
834 InvalidSymbolFile(
835 "bad_no_module.sym",
836 [
837 "WARNING:root:/build/board/bin/foo: "
838 "Symbol file has 0 MODULE lines"
839 ],
840 ),
841 InvalidSymbolFile(
842 "bad_two_modules.sym",
843 [
844 "WARNING:root:/build/board/bin/foo: "
845 "Symbol file has 2 MODULE lines"
846 ],
847 ),
848 InvalidSymbolFile(
849 "bad_func_no_line_numbers.sym",
850 [
851 "WARNING:root:/build/board/bin/foo: "
852 "Symbol file has FUNC records but no line numbers"
853 ],
854 ),
855 InvalidSymbolFile(
856 "bad_line_numbers_no_file.sym",
857 [
858 "WARNING:root:/build/board/bin/foo: "
859 "Symbol file has line number records but no FILE records"
860 ],
861 ),
862 InvalidSymbolFile(
863 "bad_inline_no_files.sym",
864 [
865 "WARNING:root:/build/board/bin/foo: "
866 "Symbol file has INLINE records but no FILE records"
867 ],
868 ),
869 InvalidSymbolFile(
870 "bad_inline_no_origins.sym",
871 [
872 "WARNING:root:/build/board/bin/foo: "
873 "Symbol file has INLINE records but no INLINE_ORIGIN "
874 "records"
875 ],
876 ),
877 InvalidSymbolFile(
878 "blank.sym",
879 [
880 "WARNING:root:/build/board/bin/foo: "
881 "Symbol file has no STACK records",
882 "WARNING:root:/build/board/bin/foo: "
883 "Symbol file has 0 MODULE lines",
884 "WARNING:root:/build/board/bin/foo: "
885 "Symbol file has no FUNC or PUBLIC records",
886 ],
887 ),
888 ]
889
890 for file in INVALID_SYMBOL_FILES:
891 with self.subTest(
892 file=file.filename
893 ), multiprocessing.Manager() as mp_manager:
894 found_files = mp_manager.list()
895 with self.assertLogs(level=logging.WARNING) as cm:
896 self.assertFalse(
897 cros_generate_breakpad_symbols.ValidateSymbolFile(
898 self._GetTestdataFile(file.filename),
899 "/build/board/bin/foo",
900 "/build/board",
901 found_files,
902 )
903 )
904 self.assertEqual(file.expected_errors, cm.output)
905 self.assertFalse(found_files)
906
907 def testInvalidSymbolFilesWhichRaise(self):
908 """Test ValidateSymbolFile raise exceptions on certain files"""
909
910 class InvalidSymbolFile:
911 """The invalid symbol file + the expected exception message"""
912
913 def __init__(self, filename, expected_exception_regex):
914 self.filename = filename
915 self.expected_exception_regex = expected_exception_regex
916
917 INVALID_SYMBOL_FILES = [
918 InvalidSymbolFile(
919 "bad_unknown_line_type.sym",
Ian Barkley-Yeungccfa1b52023-07-07 18:55:46 -0700920 r"symbol file has unknown line type UNKNOWN "
921 r"\(line='UNKNOWN line type\n'\)",
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800922 ),
923 InvalidSymbolFile(
924 "bad_blank_line.sym",
925 "symbol file has unexpected blank line",
926 ),
927 InvalidSymbolFile(
928 "bad_short_func.sym",
929 r"symbol file has FUNC line with 2 words "
Ian Barkley-Yeungccfa1b52023-07-07 18:55:46 -0700930 r"\(expected 5 or more\) \(line='FUNC fb0\n'\)",
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800931 ),
932 InvalidSymbolFile(
933 "bad_short_line_number.sym",
934 r"symbol file has line number line with 3 words "
Ian Barkley-Yeungccfa1b52023-07-07 18:55:46 -0700935 r"\(expected 4 - 4\) \(line='fb0 106 0\n'\)",
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800936 ),
937 InvalidSymbolFile(
938 "bad_long_line_number.sym",
939 r"symbol file has line number line with 5 words "
Ian Barkley-Yeungccfa1b52023-07-07 18:55:46 -0700940 r"\(expected 4 - 4\) \(line='c184 7 59 4 8\n'\)",
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800941 ),
942 ]
943
944 for file in INVALID_SYMBOL_FILES:
945 with self.subTest(
946 file=file.filename
947 ), multiprocessing.Manager() as mp_manager:
948 found_files = mp_manager.list()
949 self.assertRaisesRegex(
950 ValueError,
951 file.expected_exception_regex,
952 cros_generate_breakpad_symbols.ValidateSymbolFile,
953 self._GetTestdataFile(file.filename),
954 "/build/board/bin/foo",
955 "/build/board",
956 found_files,
957 )
958
959 def testAllowlist(self):
960 """Test that ELFs on the allowlist are allowed to pass."""
961 with multiprocessing.Manager() as mp_manager:
962 found_files = mp_manager.list()
963 self.assertTrue(
964 cros_generate_breakpad_symbols.ValidateSymbolFile(
965 self._GetTestdataFile("bad_no_stack.sym"),
966 "/build/board/opt/google/chrome/nacl_helper_bootstrap",
967 "/build/board",
968 found_files,
969 )
970 )
971 self.assertFalse(found_files)
972
973 def testAllowlistRegex(self):
974 """Test that ELFs on the regex-based allowlist are allowed to pass."""
975 with multiprocessing.Manager() as mp_manager:
976 found_files = mp_manager.list()
977 self.assertTrue(
978 cros_generate_breakpad_symbols.ValidateSymbolFile(
979 self._GetTestdataFile("bad_no_stack.sym"),
980 "/build/board/usr/lib/libcros_ml_core.so",
981 "/build/board",
982 found_files,
983 )
984 )
985 self.assertFalse(found_files)
986
987 def _CreateSymbolFile(
988 self,
989 sym_file: pathlib.Path,
990 func_lines: int = 0,
991 public_lines: int = 0,
992 stack_lines: int = 0,
993 line_number_lines: int = 0,
994 ) -> None:
995 """Creates a symbol file.
996
997 Creates a symbol file with the given number of lines (and enough other
998 lines to pass validation) in the temp directory.
999
1000 To pass validation, chrome.sym files must be huge; create them
1001 programmatically during the test instead of checking in a real 800MB+
1002 chrome symbol file.
1003 """
1004 with sym_file.open(mode="w", encoding="utf-8") as f:
1005 f.write("MODULE OS CPU ID NAME\n")
1006 f.write("FILE 0 /path/to/source.cc\n")
1007 for func in range(0, func_lines):
1008 f.write(f"FUNC {func} 1 0 function{func}\n")
1009 for public in range(0, public_lines):
1010 f.write(f"PUBLIC {public} 0 Public{public}\n")
1011 for line in range(0, line_number_lines):
1012 f.write(f"{line} 1 {line} 0\n")
1013 for stack in range(0, stack_lines):
1014 f.write(f"STACK CFI {stack} .cfa: $esp {stack} +\n")
1015
1016 def testValidChromeSymbolFile(self):
1017 """Test that a chrome symbol file can pass the additional checks"""
1018 sym_file = self.tempdir / "chrome.sym"
1019 self._CreateSymbolFile(
1020 sym_file,
1021 func_lines=100000,
1022 public_lines=10,
1023 stack_lines=1000000,
1024 line_number_lines=1000000,
1025 )
1026 with multiprocessing.Manager() as mp_manager:
1027 found_files = mp_manager.list()
1028 self.assertTrue(
1029 cros_generate_breakpad_symbols.ValidateSymbolFile(
1030 str(sym_file),
1031 "/build/board/opt/google/chrome/chrome",
1032 "/build/board",
1033 found_files,
1034 )
1035 )
1036 self.assertEqual(
1037 list(found_files),
1038 [cros_generate_breakpad_symbols.ExpectedFiles.ASH_CHROME],
1039 )
1040
1041 def testInvalidChromeSymbolFile(self):
1042 """Test that a chrome symbol file is held to higher standards."""
1043
1044 class ChromeSymbolFileTest:
1045 """Defines the subtest for an invalid Chrome symbol file."""
1046
1047 def __init__(
1048 self,
1049 name,
1050 expected_error,
1051 func_lines=100000,
1052 stack_lines=1000000,
1053 line_number_lines=1000000,
1054 ):
1055 self.name = name
1056 self.expected_error = expected_error
1057 self.func_lines = func_lines
1058 self.stack_lines = stack_lines
1059 self.line_number_lines = line_number_lines
1060
1061 CHROME_SYMBOL_TESTS = [
1062 ChromeSymbolFileTest(
1063 name="Insufficient FUNC records",
1064 func_lines=10000,
1065 expected_error="chrome should have at least 100,000 FUNC "
1066 "records, found 10000",
1067 ),
1068 ChromeSymbolFileTest(
1069 name="Insufficient STACK records",
1070 stack_lines=100000,
1071 expected_error="chrome should have at least 1,000,000 STACK "
1072 "records, found 100000",
1073 ),
1074 ChromeSymbolFileTest(
1075 name="Insufficient line number records",
1076 line_number_lines=100000,
1077 expected_error="chrome should have at least 1,000,000 "
1078 "line number records, found 100000",
1079 ),
1080 ]
1081 for test in CHROME_SYMBOL_TESTS:
1082 with self.subTest(
1083 name=test.name
1084 ), multiprocessing.Manager() as mp_manager:
1085 sym_file = self.tempdir / "chrome.sym"
1086 self._CreateSymbolFile(
1087 sym_file,
1088 func_lines=test.func_lines,
1089 public_lines=10,
1090 stack_lines=test.stack_lines,
1091 line_number_lines=test.line_number_lines,
1092 )
1093 found_files = mp_manager.list()
1094 with self.assertLogs(level=logging.WARNING) as cm:
1095 self.assertFalse(
1096 cros_generate_breakpad_symbols.ValidateSymbolFile(
1097 str(sym_file),
1098 "/build/board/opt/google/chrome/chrome",
1099 "/build/board",
1100 found_files,
1101 )
1102 )
1103 self.assertIn(test.expected_error, cm.output[0])
1104 self.assertEqual(len(cm.output), 1)
1105
1106 def testValidLibcSymbolFile(self):
1107 """Test that a libc.so symbol file can pass the additional checks."""
1108 with multiprocessing.Manager() as mp_manager:
1109 sym_file = self.tempdir / "libc.so.sym"
1110 self._CreateSymbolFile(
1111 sym_file, public_lines=200, stack_lines=20000
1112 )
1113 found_files = mp_manager.list()
1114 self.assertTrue(
1115 cros_generate_breakpad_symbols.ValidateSymbolFile(
1116 str(sym_file),
1117 "/build/board/lib64/libc.so.6",
1118 "/build/board",
1119 found_files,
1120 )
1121 )
1122 self.assertEqual(
1123 list(found_files),
1124 [cros_generate_breakpad_symbols.ExpectedFiles.LIBC],
1125 )
1126
1127 def testInvalidLibcSymbolFile(self):
1128 """Test that a libc.so symbol file is held to higher standards."""
1129
1130 class LibcSymbolFileTest:
1131 """Defines the subtest for an invalid libc symbol file."""
1132
1133 def __init__(
1134 self,
1135 name,
1136 expected_error,
1137 public_lines=200,
1138 stack_lines=20000,
1139 ):
1140 self.name = name
1141 self.expected_error = expected_error
1142 self.public_lines = public_lines
1143 self.stack_lines = stack_lines
1144
1145 LIBC_SYMBOL_TESTS = [
1146 LibcSymbolFileTest(
1147 name="Insufficient PUBLIC records",
1148 public_lines=50,
1149 expected_error="/build/board/lib64/libc.so.6 should have at "
1150 "least 100 PUBLIC records, found 50",
1151 ),
1152 LibcSymbolFileTest(
1153 name="Insufficient STACK records",
1154 stack_lines=1000,
1155 expected_error="/build/board/lib64/libc.so.6 should have at "
1156 "least 10000 STACK records, found 1000",
1157 ),
1158 ]
1159 for test in LIBC_SYMBOL_TESTS:
1160 with self.subTest(
1161 name=test.name
1162 ), multiprocessing.Manager() as mp_manager:
1163 sym_file = self.tempdir / "libc.so.sym"
1164 self._CreateSymbolFile(
1165 sym_file,
1166 public_lines=test.public_lines,
1167 stack_lines=test.stack_lines,
1168 )
1169 found_files = mp_manager.list()
1170 with self.assertLogs(level=logging.WARNING) as cm:
1171 self.assertFalse(
1172 cros_generate_breakpad_symbols.ValidateSymbolFile(
1173 str(sym_file),
1174 "/build/board/lib64/libc.so.6",
1175 "/build/board",
1176 found_files,
1177 )
1178 )
1179 self.assertIn(test.expected_error, cm.output[0])
1180 self.assertEqual(len(cm.output), 1)
1181
1182 def testValidCrashReporterSymbolFile(self):
1183 """Test a crash_reporter symbol file can pass the additional checks."""
1184 with multiprocessing.Manager() as mp_manager:
1185 sym_file = self.tempdir / "crash_reporter.sym"
1186 self._CreateSymbolFile(
1187 sym_file,
1188 func_lines=2000,
1189 public_lines=10,
1190 stack_lines=2000,
1191 line_number_lines=20000,
1192 )
1193 found_files = mp_manager.list()
1194 self.assertTrue(
1195 cros_generate_breakpad_symbols.ValidateSymbolFile(
1196 str(sym_file),
1197 "/build/board/sbin/crash_reporter",
1198 "/build/board",
1199 found_files,
1200 )
1201 )
1202 self.assertEqual(
1203 list(found_files),
1204 [cros_generate_breakpad_symbols.ExpectedFiles.CRASH_REPORTER],
1205 )
1206
1207 def testInvalidCrashReporterSymbolFile(self):
1208 """Test that a crash_reporter symbol file is held to higher standards"""
1209
1210 class CrashReporterSymbolFileTest:
1211 """Defines the subtest for an invalid crash_reporter symbol file."""
1212
1213 def __init__(
1214 self,
1215 name,
1216 expected_error,
1217 func_lines=2000,
1218 stack_lines=2000,
1219 line_number_lines=20000,
1220 ):
1221 self.name = name
1222 self.expected_error = expected_error
1223 self.func_lines = func_lines
1224 self.stack_lines = stack_lines
1225 self.line_number_lines = line_number_lines
1226
1227 CRASH_REPORTER_SYMBOL_TESTS = [
1228 CrashReporterSymbolFileTest(
1229 name="Insufficient FUNC records",
1230 func_lines=500,
1231 expected_error="crash_reporter should have at least 1000 FUNC "
1232 "records, found 500",
1233 ),
1234 CrashReporterSymbolFileTest(
1235 name="Insufficient STACK records",
1236 stack_lines=100,
1237 expected_error="crash_reporter should have at least 1000 STACK "
1238 "records, found 100",
1239 ),
1240 CrashReporterSymbolFileTest(
1241 name="Insufficient line number records",
1242 line_number_lines=2000,
1243 expected_error="crash_reporter should have at least 10,000 "
1244 "line number records, found 2000",
1245 ),
1246 ]
1247 for test in CRASH_REPORTER_SYMBOL_TESTS:
1248 with self.subTest(
1249 name=test.name
1250 ), multiprocessing.Manager() as mp_manager:
1251 sym_file = self.tempdir / "crash_reporter.sym"
1252 self._CreateSymbolFile(
1253 sym_file,
1254 func_lines=test.func_lines,
1255 stack_lines=test.stack_lines,
1256 line_number_lines=test.line_number_lines,
1257 )
1258 found_files = mp_manager.list()
1259 with self.assertLogs(level=logging.WARNING) as cm:
1260 self.assertFalse(
1261 cros_generate_breakpad_symbols.ValidateSymbolFile(
1262 str(sym_file),
1263 "/build/board/sbin/crash_reporter",
1264 "/build/board",
1265 found_files,
1266 )
1267 )
1268 self.assertIn(test.expected_error, cm.output[0])
1269 self.assertEqual(len(cm.output), 1)
1270
1271 def testValidLibMetricsSymbolFile(self):
1272 """Test a libmetrics.so symbol file can pass the additional checks."""
1273 with multiprocessing.Manager() as mp_manager:
1274 sym_file = self.tempdir / "libmetrics.so.sym"
1275 self._CreateSymbolFile(
1276 sym_file,
1277 func_lines=200,
1278 public_lines=2,
1279 stack_lines=2000,
1280 line_number_lines=10000,
1281 )
1282 found_files = mp_manager.list()
1283 self.assertTrue(
1284 cros_generate_breakpad_symbols.ValidateSymbolFile(
1285 str(sym_file),
1286 "/build/board/usr/lib64/libmetrics.so",
1287 "/build/board",
1288 found_files,
1289 )
1290 )
1291 self.assertEqual(
1292 list(found_files),
1293 [cros_generate_breakpad_symbols.ExpectedFiles.LIBMETRICS],
1294 )
1295
1296 def testInvalidLibMetricsSymbolFile(self):
1297 """Test that a libmetrics.so symbol file is held to higher standards."""
1298
1299 class LibMetricsSymbolFileTest:
1300 """Defines the subtest for an invalid libmetrics.so symbol file."""
1301
1302 def __init__(
1303 self,
1304 name,
1305 expected_error,
1306 func_lines=200,
1307 public_lines=2,
1308 stack_lines=2000,
1309 line_number_lines=10000,
1310 ):
1311 self.name = name
1312 self.expected_error = expected_error
1313 self.func_lines = func_lines
1314 self.public_lines = public_lines
1315 self.stack_lines = stack_lines
1316 self.line_number_lines = line_number_lines
1317
1318 LIBMETRICS_SYMBOL_TESTS = [
1319 LibMetricsSymbolFileTest(
1320 name="Insufficient FUNC records",
1321 func_lines=10,
1322 expected_error="libmetrics should have at least 100 FUNC "
1323 "records, found 10",
1324 ),
1325 LibMetricsSymbolFileTest(
1326 name="Insufficient PUBLIC records",
1327 public_lines=0,
1328 expected_error="libmetrics should have at least 1 PUBLIC "
1329 "record, found 0",
1330 ),
1331 LibMetricsSymbolFileTest(
1332 name="Insufficient STACK records",
1333 stack_lines=500,
1334 expected_error="libmetrics should have at least 1000 STACK "
1335 "records, found 500",
1336 ),
1337 LibMetricsSymbolFileTest(
1338 name="Insufficient line number records",
1339 line_number_lines=2000,
1340 expected_error="libmetrics should have at least 5000 "
1341 "line number records, found 2000",
1342 ),
1343 ]
1344 for test in LIBMETRICS_SYMBOL_TESTS:
1345 with self.subTest(
1346 name=test.name
1347 ), multiprocessing.Manager() as mp_manager:
1348 sym_file = self.tempdir / "libmetrics.so.sym"
1349 self._CreateSymbolFile(
1350 sym_file,
1351 func_lines=test.func_lines,
1352 public_lines=test.public_lines,
1353 stack_lines=test.stack_lines,
1354 line_number_lines=test.line_number_lines,
1355 )
1356 found_files = mp_manager.list()
1357 with self.assertLogs(level=logging.WARNING) as cm:
1358 self.assertFalse(
1359 cros_generate_breakpad_symbols.ValidateSymbolFile(
1360 str(sym_file),
1361 "/build/board/usr/lib64/libmetrics.so",
1362 "/build/board",
1363 found_files,
1364 )
1365 )
1366 self.assertIn(test.expected_error, cm.output[0])
1367 self.assertEqual(len(cm.output), 1)
1368
1369
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001370class UtilsTestDir(cros_test_lib.TempDirTestCase):
Alex Klein1699fab2022-09-08 08:46:06 -06001371 """Tests ReadSymsHeader."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001372
Alex Klein1699fab2022-09-08 08:46:06 -06001373 def testReadSymsHeaderGoodFile(self):
1374 """Make sure ReadSymsHeader can parse sym files"""
1375 sym_file = os.path.join(self.tempdir, "sym")
1376 osutils.WriteFile(sym_file, "MODULE Linux x86 s0m31D chrooome")
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001377 result = cros_generate_breakpad_symbols.ReadSymsHeader(
1378 sym_file, "unused_elfname"
1379 )
Alex Klein1699fab2022-09-08 08:46:06 -06001380 self.assertEqual(result.cpu, "x86")
1381 self.assertEqual(result.id, "s0m31D")
1382 self.assertEqual(result.name, "chrooome")
1383 self.assertEqual(result.os, "Linux")
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001384
1385
1386class UtilsTest(cros_test_lib.TestCase):
Alex Klein1699fab2022-09-08 08:46:06 -06001387 """Tests ReadSymsHeader."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001388
Alex Klein1699fab2022-09-08 08:46:06 -06001389 def testReadSymsHeaderGoodBuffer(self):
1390 """Make sure ReadSymsHeader can parse sym file handles"""
1391 result = cros_generate_breakpad_symbols.ReadSymsHeader(
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001392 io.BytesIO(b"MODULE Linux arm MY-ID-HERE blkid"), "unused_elfname"
Alex Klein1699fab2022-09-08 08:46:06 -06001393 )
1394 self.assertEqual(result.cpu, "arm")
1395 self.assertEqual(result.id, "MY-ID-HERE")
1396 self.assertEqual(result.name, "blkid")
1397 self.assertEqual(result.os, "Linux")
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001398
Alex Klein1699fab2022-09-08 08:46:06 -06001399 def testReadSymsHeaderBadd(self):
1400 """Make sure ReadSymsHeader throws on bad sym files"""
1401 self.assertRaises(
1402 ValueError,
1403 cros_generate_breakpad_symbols.ReadSymsHeader,
1404 io.BytesIO(b"asdf"),
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001405 "unused_elfname",
Alex Klein1699fab2022-09-08 08:46:06 -06001406 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001407
Alex Klein1699fab2022-09-08 08:46:06 -06001408 def testBreakpadDir(self):
1409 """Make sure board->breakpad path expansion works"""
1410 expected = "/build/blah/usr/lib/debug/breakpad"
1411 result = cros_generate_breakpad_symbols.FindBreakpadDir("blah")
1412 self.assertEqual(expected, result)
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001413
Alex Klein1699fab2022-09-08 08:46:06 -06001414 def testDebugDir(self):
1415 """Make sure board->debug path expansion works"""
1416 expected = "/build/blah/usr/lib/debug"
1417 result = cros_generate_breakpad_symbols.FindDebugDir("blah")
1418 self.assertEqual(expected, result)