blob: 810dada8b3e1d9ced40e3651df58a18b7118e148 [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",
920 "symbol file has unknown line type UNKNOWN",
921 ),
922 InvalidSymbolFile(
923 "bad_blank_line.sym",
924 "symbol file has unexpected blank line",
925 ),
926 InvalidSymbolFile(
927 "bad_short_func.sym",
928 r"symbol file has FUNC line with 2 words "
929 r"\(expected 5 or more\)",
930 ),
931 InvalidSymbolFile(
932 "bad_short_line_number.sym",
933 r"symbol file has line number line with 3 words "
934 r"\(expected 4 - 4\)",
935 ),
936 InvalidSymbolFile(
937 "bad_long_line_number.sym",
938 r"symbol file has line number line with 5 words "
939 r"\(expected 4 - 4\)",
940 ),
941 ]
942
943 for file in INVALID_SYMBOL_FILES:
944 with self.subTest(
945 file=file.filename
946 ), multiprocessing.Manager() as mp_manager:
947 found_files = mp_manager.list()
948 self.assertRaisesRegex(
949 ValueError,
950 file.expected_exception_regex,
951 cros_generate_breakpad_symbols.ValidateSymbolFile,
952 self._GetTestdataFile(file.filename),
953 "/build/board/bin/foo",
954 "/build/board",
955 found_files,
956 )
957
958 def testAllowlist(self):
959 """Test that ELFs on the allowlist are allowed to pass."""
960 with multiprocessing.Manager() as mp_manager:
961 found_files = mp_manager.list()
962 self.assertTrue(
963 cros_generate_breakpad_symbols.ValidateSymbolFile(
964 self._GetTestdataFile("bad_no_stack.sym"),
965 "/build/board/opt/google/chrome/nacl_helper_bootstrap",
966 "/build/board",
967 found_files,
968 )
969 )
970 self.assertFalse(found_files)
971
972 def testAllowlistRegex(self):
973 """Test that ELFs on the regex-based allowlist are allowed to pass."""
974 with multiprocessing.Manager() as mp_manager:
975 found_files = mp_manager.list()
976 self.assertTrue(
977 cros_generate_breakpad_symbols.ValidateSymbolFile(
978 self._GetTestdataFile("bad_no_stack.sym"),
979 "/build/board/usr/lib/libcros_ml_core.so",
980 "/build/board",
981 found_files,
982 )
983 )
984 self.assertFalse(found_files)
985
986 def _CreateSymbolFile(
987 self,
988 sym_file: pathlib.Path,
989 func_lines: int = 0,
990 public_lines: int = 0,
991 stack_lines: int = 0,
992 line_number_lines: int = 0,
993 ) -> None:
994 """Creates a symbol file.
995
996 Creates a symbol file with the given number of lines (and enough other
997 lines to pass validation) in the temp directory.
998
999 To pass validation, chrome.sym files must be huge; create them
1000 programmatically during the test instead of checking in a real 800MB+
1001 chrome symbol file.
1002 """
1003 with sym_file.open(mode="w", encoding="utf-8") as f:
1004 f.write("MODULE OS CPU ID NAME\n")
1005 f.write("FILE 0 /path/to/source.cc\n")
1006 for func in range(0, func_lines):
1007 f.write(f"FUNC {func} 1 0 function{func}\n")
1008 for public in range(0, public_lines):
1009 f.write(f"PUBLIC {public} 0 Public{public}\n")
1010 for line in range(0, line_number_lines):
1011 f.write(f"{line} 1 {line} 0\n")
1012 for stack in range(0, stack_lines):
1013 f.write(f"STACK CFI {stack} .cfa: $esp {stack} +\n")
1014
1015 def testValidChromeSymbolFile(self):
1016 """Test that a chrome symbol file can pass the additional checks"""
1017 sym_file = self.tempdir / "chrome.sym"
1018 self._CreateSymbolFile(
1019 sym_file,
1020 func_lines=100000,
1021 public_lines=10,
1022 stack_lines=1000000,
1023 line_number_lines=1000000,
1024 )
1025 with multiprocessing.Manager() as mp_manager:
1026 found_files = mp_manager.list()
1027 self.assertTrue(
1028 cros_generate_breakpad_symbols.ValidateSymbolFile(
1029 str(sym_file),
1030 "/build/board/opt/google/chrome/chrome",
1031 "/build/board",
1032 found_files,
1033 )
1034 )
1035 self.assertEqual(
1036 list(found_files),
1037 [cros_generate_breakpad_symbols.ExpectedFiles.ASH_CHROME],
1038 )
1039
1040 def testInvalidChromeSymbolFile(self):
1041 """Test that a chrome symbol file is held to higher standards."""
1042
1043 class ChromeSymbolFileTest:
1044 """Defines the subtest for an invalid Chrome symbol file."""
1045
1046 def __init__(
1047 self,
1048 name,
1049 expected_error,
1050 func_lines=100000,
1051 stack_lines=1000000,
1052 line_number_lines=1000000,
1053 ):
1054 self.name = name
1055 self.expected_error = expected_error
1056 self.func_lines = func_lines
1057 self.stack_lines = stack_lines
1058 self.line_number_lines = line_number_lines
1059
1060 CHROME_SYMBOL_TESTS = [
1061 ChromeSymbolFileTest(
1062 name="Insufficient FUNC records",
1063 func_lines=10000,
1064 expected_error="chrome should have at least 100,000 FUNC "
1065 "records, found 10000",
1066 ),
1067 ChromeSymbolFileTest(
1068 name="Insufficient STACK records",
1069 stack_lines=100000,
1070 expected_error="chrome should have at least 1,000,000 STACK "
1071 "records, found 100000",
1072 ),
1073 ChromeSymbolFileTest(
1074 name="Insufficient line number records",
1075 line_number_lines=100000,
1076 expected_error="chrome should have at least 1,000,000 "
1077 "line number records, found 100000",
1078 ),
1079 ]
1080 for test in CHROME_SYMBOL_TESTS:
1081 with self.subTest(
1082 name=test.name
1083 ), multiprocessing.Manager() as mp_manager:
1084 sym_file = self.tempdir / "chrome.sym"
1085 self._CreateSymbolFile(
1086 sym_file,
1087 func_lines=test.func_lines,
1088 public_lines=10,
1089 stack_lines=test.stack_lines,
1090 line_number_lines=test.line_number_lines,
1091 )
1092 found_files = mp_manager.list()
1093 with self.assertLogs(level=logging.WARNING) as cm:
1094 self.assertFalse(
1095 cros_generate_breakpad_symbols.ValidateSymbolFile(
1096 str(sym_file),
1097 "/build/board/opt/google/chrome/chrome",
1098 "/build/board",
1099 found_files,
1100 )
1101 )
1102 self.assertIn(test.expected_error, cm.output[0])
1103 self.assertEqual(len(cm.output), 1)
1104
1105 def testValidLibcSymbolFile(self):
1106 """Test that a libc.so symbol file can pass the additional checks."""
1107 with multiprocessing.Manager() as mp_manager:
1108 sym_file = self.tempdir / "libc.so.sym"
1109 self._CreateSymbolFile(
1110 sym_file, public_lines=200, stack_lines=20000
1111 )
1112 found_files = mp_manager.list()
1113 self.assertTrue(
1114 cros_generate_breakpad_symbols.ValidateSymbolFile(
1115 str(sym_file),
1116 "/build/board/lib64/libc.so.6",
1117 "/build/board",
1118 found_files,
1119 )
1120 )
1121 self.assertEqual(
1122 list(found_files),
1123 [cros_generate_breakpad_symbols.ExpectedFiles.LIBC],
1124 )
1125
1126 def testInvalidLibcSymbolFile(self):
1127 """Test that a libc.so symbol file is held to higher standards."""
1128
1129 class LibcSymbolFileTest:
1130 """Defines the subtest for an invalid libc symbol file."""
1131
1132 def __init__(
1133 self,
1134 name,
1135 expected_error,
1136 public_lines=200,
1137 stack_lines=20000,
1138 ):
1139 self.name = name
1140 self.expected_error = expected_error
1141 self.public_lines = public_lines
1142 self.stack_lines = stack_lines
1143
1144 LIBC_SYMBOL_TESTS = [
1145 LibcSymbolFileTest(
1146 name="Insufficient PUBLIC records",
1147 public_lines=50,
1148 expected_error="/build/board/lib64/libc.so.6 should have at "
1149 "least 100 PUBLIC records, found 50",
1150 ),
1151 LibcSymbolFileTest(
1152 name="Insufficient STACK records",
1153 stack_lines=1000,
1154 expected_error="/build/board/lib64/libc.so.6 should have at "
1155 "least 10000 STACK records, found 1000",
1156 ),
1157 ]
1158 for test in LIBC_SYMBOL_TESTS:
1159 with self.subTest(
1160 name=test.name
1161 ), multiprocessing.Manager() as mp_manager:
1162 sym_file = self.tempdir / "libc.so.sym"
1163 self._CreateSymbolFile(
1164 sym_file,
1165 public_lines=test.public_lines,
1166 stack_lines=test.stack_lines,
1167 )
1168 found_files = mp_manager.list()
1169 with self.assertLogs(level=logging.WARNING) as cm:
1170 self.assertFalse(
1171 cros_generate_breakpad_symbols.ValidateSymbolFile(
1172 str(sym_file),
1173 "/build/board/lib64/libc.so.6",
1174 "/build/board",
1175 found_files,
1176 )
1177 )
1178 self.assertIn(test.expected_error, cm.output[0])
1179 self.assertEqual(len(cm.output), 1)
1180
1181 def testValidCrashReporterSymbolFile(self):
1182 """Test a crash_reporter symbol file can pass the additional checks."""
1183 with multiprocessing.Manager() as mp_manager:
1184 sym_file = self.tempdir / "crash_reporter.sym"
1185 self._CreateSymbolFile(
1186 sym_file,
1187 func_lines=2000,
1188 public_lines=10,
1189 stack_lines=2000,
1190 line_number_lines=20000,
1191 )
1192 found_files = mp_manager.list()
1193 self.assertTrue(
1194 cros_generate_breakpad_symbols.ValidateSymbolFile(
1195 str(sym_file),
1196 "/build/board/sbin/crash_reporter",
1197 "/build/board",
1198 found_files,
1199 )
1200 )
1201 self.assertEqual(
1202 list(found_files),
1203 [cros_generate_breakpad_symbols.ExpectedFiles.CRASH_REPORTER],
1204 )
1205
1206 def testInvalidCrashReporterSymbolFile(self):
1207 """Test that a crash_reporter symbol file is held to higher standards"""
1208
1209 class CrashReporterSymbolFileTest:
1210 """Defines the subtest for an invalid crash_reporter symbol file."""
1211
1212 def __init__(
1213 self,
1214 name,
1215 expected_error,
1216 func_lines=2000,
1217 stack_lines=2000,
1218 line_number_lines=20000,
1219 ):
1220 self.name = name
1221 self.expected_error = expected_error
1222 self.func_lines = func_lines
1223 self.stack_lines = stack_lines
1224 self.line_number_lines = line_number_lines
1225
1226 CRASH_REPORTER_SYMBOL_TESTS = [
1227 CrashReporterSymbolFileTest(
1228 name="Insufficient FUNC records",
1229 func_lines=500,
1230 expected_error="crash_reporter should have at least 1000 FUNC "
1231 "records, found 500",
1232 ),
1233 CrashReporterSymbolFileTest(
1234 name="Insufficient STACK records",
1235 stack_lines=100,
1236 expected_error="crash_reporter should have at least 1000 STACK "
1237 "records, found 100",
1238 ),
1239 CrashReporterSymbolFileTest(
1240 name="Insufficient line number records",
1241 line_number_lines=2000,
1242 expected_error="crash_reporter should have at least 10,000 "
1243 "line number records, found 2000",
1244 ),
1245 ]
1246 for test in CRASH_REPORTER_SYMBOL_TESTS:
1247 with self.subTest(
1248 name=test.name
1249 ), multiprocessing.Manager() as mp_manager:
1250 sym_file = self.tempdir / "crash_reporter.sym"
1251 self._CreateSymbolFile(
1252 sym_file,
1253 func_lines=test.func_lines,
1254 stack_lines=test.stack_lines,
1255 line_number_lines=test.line_number_lines,
1256 )
1257 found_files = mp_manager.list()
1258 with self.assertLogs(level=logging.WARNING) as cm:
1259 self.assertFalse(
1260 cros_generate_breakpad_symbols.ValidateSymbolFile(
1261 str(sym_file),
1262 "/build/board/sbin/crash_reporter",
1263 "/build/board",
1264 found_files,
1265 )
1266 )
1267 self.assertIn(test.expected_error, cm.output[0])
1268 self.assertEqual(len(cm.output), 1)
1269
1270 def testValidLibMetricsSymbolFile(self):
1271 """Test a libmetrics.so symbol file can pass the additional checks."""
1272 with multiprocessing.Manager() as mp_manager:
1273 sym_file = self.tempdir / "libmetrics.so.sym"
1274 self._CreateSymbolFile(
1275 sym_file,
1276 func_lines=200,
1277 public_lines=2,
1278 stack_lines=2000,
1279 line_number_lines=10000,
1280 )
1281 found_files = mp_manager.list()
1282 self.assertTrue(
1283 cros_generate_breakpad_symbols.ValidateSymbolFile(
1284 str(sym_file),
1285 "/build/board/usr/lib64/libmetrics.so",
1286 "/build/board",
1287 found_files,
1288 )
1289 )
1290 self.assertEqual(
1291 list(found_files),
1292 [cros_generate_breakpad_symbols.ExpectedFiles.LIBMETRICS],
1293 )
1294
1295 def testInvalidLibMetricsSymbolFile(self):
1296 """Test that a libmetrics.so symbol file is held to higher standards."""
1297
1298 class LibMetricsSymbolFileTest:
1299 """Defines the subtest for an invalid libmetrics.so symbol file."""
1300
1301 def __init__(
1302 self,
1303 name,
1304 expected_error,
1305 func_lines=200,
1306 public_lines=2,
1307 stack_lines=2000,
1308 line_number_lines=10000,
1309 ):
1310 self.name = name
1311 self.expected_error = expected_error
1312 self.func_lines = func_lines
1313 self.public_lines = public_lines
1314 self.stack_lines = stack_lines
1315 self.line_number_lines = line_number_lines
1316
1317 LIBMETRICS_SYMBOL_TESTS = [
1318 LibMetricsSymbolFileTest(
1319 name="Insufficient FUNC records",
1320 func_lines=10,
1321 expected_error="libmetrics should have at least 100 FUNC "
1322 "records, found 10",
1323 ),
1324 LibMetricsSymbolFileTest(
1325 name="Insufficient PUBLIC records",
1326 public_lines=0,
1327 expected_error="libmetrics should have at least 1 PUBLIC "
1328 "record, found 0",
1329 ),
1330 LibMetricsSymbolFileTest(
1331 name="Insufficient STACK records",
1332 stack_lines=500,
1333 expected_error="libmetrics should have at least 1000 STACK "
1334 "records, found 500",
1335 ),
1336 LibMetricsSymbolFileTest(
1337 name="Insufficient line number records",
1338 line_number_lines=2000,
1339 expected_error="libmetrics should have at least 5000 "
1340 "line number records, found 2000",
1341 ),
1342 ]
1343 for test in LIBMETRICS_SYMBOL_TESTS:
1344 with self.subTest(
1345 name=test.name
1346 ), multiprocessing.Manager() as mp_manager:
1347 sym_file = self.tempdir / "libmetrics.so.sym"
1348 self._CreateSymbolFile(
1349 sym_file,
1350 func_lines=test.func_lines,
1351 public_lines=test.public_lines,
1352 stack_lines=test.stack_lines,
1353 line_number_lines=test.line_number_lines,
1354 )
1355 found_files = mp_manager.list()
1356 with self.assertLogs(level=logging.WARNING) as cm:
1357 self.assertFalse(
1358 cros_generate_breakpad_symbols.ValidateSymbolFile(
1359 str(sym_file),
1360 "/build/board/usr/lib64/libmetrics.so",
1361 "/build/board",
1362 found_files,
1363 )
1364 )
1365 self.assertIn(test.expected_error, cm.output[0])
1366 self.assertEqual(len(cm.output), 1)
1367
1368
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001369class UtilsTestDir(cros_test_lib.TempDirTestCase):
Alex Klein1699fab2022-09-08 08:46:06 -06001370 """Tests ReadSymsHeader."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001371
Alex Klein1699fab2022-09-08 08:46:06 -06001372 def testReadSymsHeaderGoodFile(self):
1373 """Make sure ReadSymsHeader can parse sym files"""
1374 sym_file = os.path.join(self.tempdir, "sym")
1375 osutils.WriteFile(sym_file, "MODULE Linux x86 s0m31D chrooome")
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001376 result = cros_generate_breakpad_symbols.ReadSymsHeader(
1377 sym_file, "unused_elfname"
1378 )
Alex Klein1699fab2022-09-08 08:46:06 -06001379 self.assertEqual(result.cpu, "x86")
1380 self.assertEqual(result.id, "s0m31D")
1381 self.assertEqual(result.name, "chrooome")
1382 self.assertEqual(result.os, "Linux")
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001383
1384
1385class UtilsTest(cros_test_lib.TestCase):
Alex Klein1699fab2022-09-08 08:46:06 -06001386 """Tests ReadSymsHeader."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001387
Alex Klein1699fab2022-09-08 08:46:06 -06001388 def testReadSymsHeaderGoodBuffer(self):
1389 """Make sure ReadSymsHeader can parse sym file handles"""
1390 result = cros_generate_breakpad_symbols.ReadSymsHeader(
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001391 io.BytesIO(b"MODULE Linux arm MY-ID-HERE blkid"), "unused_elfname"
Alex Klein1699fab2022-09-08 08:46:06 -06001392 )
1393 self.assertEqual(result.cpu, "arm")
1394 self.assertEqual(result.id, "MY-ID-HERE")
1395 self.assertEqual(result.name, "blkid")
1396 self.assertEqual(result.os, "Linux")
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001397
Alex Klein1699fab2022-09-08 08:46:06 -06001398 def testReadSymsHeaderBadd(self):
1399 """Make sure ReadSymsHeader throws on bad sym files"""
1400 self.assertRaises(
1401 ValueError,
1402 cros_generate_breakpad_symbols.ReadSymsHeader,
1403 io.BytesIO(b"asdf"),
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001404 "unused_elfname",
Alex Klein1699fab2022-09-08 08:46:06 -06001405 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001406
Alex Klein1699fab2022-09-08 08:46:06 -06001407 def testBreakpadDir(self):
1408 """Make sure board->breakpad path expansion works"""
1409 expected = "/build/blah/usr/lib/debug/breakpad"
1410 result = cros_generate_breakpad_symbols.FindBreakpadDir("blah")
1411 self.assertEqual(expected, result)
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001412
Alex Klein1699fab2022-09-08 08:46:06 -06001413 def testDebugDir(self):
1414 """Make sure board->debug path expansion works"""
1415 expected = "/build/blah/usr/lib/debug"
1416 result = cros_generate_breakpad_symbols.FindDebugDir("blah")
1417 self.assertEqual(expected, result)