blob: d460227c8b4fe22f7ea1eaf75cdf6263d421d3e5 [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
Alex Klein1699fab2022-09-08 08:46:06 -0600431 def setUp(self):
432 self.elf_file = os.path.join(self.tempdir, "elf")
433 osutils.Touch(self.elf_file)
434 self.debug_dir = os.path.join(self.tempdir, "debug")
435 self.debug_file = os.path.join(self.debug_dir, "elf.debug")
436 osutils.Touch(self.debug_file, makedirs=True)
437 # Not needed as the code itself should create it as needed.
438 self.breakpad_dir = os.path.join(self.debug_dir, "breakpad")
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400439
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800440 self.FILE_OUT = (
441 f"{self.elf_file}: ELF 64-bit LSB pie executable, x86-64, "
442 "version 1 (SYSV), dynamically linked, interpreter "
443 "/lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, "
444 "BuildID[sha1]=cf9a21fa6b14bfb2dfcb76effd713c4536014d95, stripped"
445 )
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800446 # A symbol file which would pass validation.
447 MINIMAL_SYMBOL_FILE = (
448 "MODULE OS CPU ID NAME\n"
449 "PUBLIC f10 0 func\n"
450 "STACK CFI INIT f10 22 .cfa: $rsp 8 + .ra: .cfa -8 + ^\n"
451 )
452 self.rc.SetDefaultCmdResult(stdout=MINIMAL_SYMBOL_FILE)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800453 self.rc.AddCmdResult(
454 ["/usr/bin/file", self.elf_file], stdout=self.FILE_OUT
455 )
Alex Klein1699fab2022-09-08 08:46:06 -0600456 self.assertCommandContains = self.rc.assertCommandContains
457 self.sym_file = os.path.join(self.breakpad_dir, "NAME/ID/NAME.sym")
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400458
Alex Klein1699fab2022-09-08 08:46:06 -0600459 self.StartPatcher(FindDebugDirMock(self.debug_dir))
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400460
Alex Klein1699fab2022-09-08 08:46:06 -0600461 def assertCommandArgs(self, i, args):
462 """Helper for looking at the args of the |i|th call"""
463 self.assertEqual(self.rc.call_args_list[i][0][0], args)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400464
Alex Klein1699fab2022-09-08 08:46:06 -0600465 def testNormal(self):
466 """Normal run -- given an ELF and a debug file"""
467 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800468 self.elf_file,
469 self.debug_file,
470 self.breakpad_dir,
Alex Klein1699fab2022-09-08 08:46:06 -0600471 )
472 self.assertEqual(ret, self.sym_file)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800473 self.assertEqual(self.rc.call_count, 2)
Alex Klein1699fab2022-09-08 08:46:06 -0600474 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000475 1, ["dump_syms", "-v", self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600476 )
477 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400478
Alex Klein1699fab2022-09-08 08:46:06 -0600479 def testNormalNoCfi(self):
480 """Normal run w/out CFI"""
481 # Make sure the num_errors flag works too.
Trent Apted1e2e4f32023-05-05 03:50:20 +0000482 num_errors = ctypes.c_int(0)
Alex Klein1699fab2022-09-08 08:46:06 -0600483 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
484 self.elf_file,
485 breakpad_dir=self.breakpad_dir,
486 strip_cfi=True,
487 num_errors=num_errors,
488 )
489 self.assertEqual(ret, self.sym_file)
490 self.assertEqual(num_errors.value, 0)
Manoj Gupta68546b02023-05-01 21:55:19 +0000491 self.assertCommandArgs(1, ["dump_syms", "-v", "-c", self.elf_file])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800492 self.assertEqual(self.rc.call_count, 2)
Alex Klein1699fab2022-09-08 08:46:06 -0600493 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400494
Alex Klein1699fab2022-09-08 08:46:06 -0600495 def testNormalElfOnly(self):
496 """Normal run -- given just an ELF"""
497 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
498 self.elf_file, breakpad_dir=self.breakpad_dir
499 )
500 self.assertEqual(ret, self.sym_file)
Manoj Gupta68546b02023-05-01 21:55:19 +0000501 self.assertCommandArgs(1, ["dump_syms", "-v", self.elf_file])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800502 self.assertEqual(self.rc.call_count, 2)
Alex Klein1699fab2022-09-08 08:46:06 -0600503 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400504
Alex Klein1699fab2022-09-08 08:46:06 -0600505 def testNormalSudo(self):
506 """Normal run where ELF is readable only by root"""
507 with mock.patch.object(os, "access") as mock_access:
508 mock_access.return_value = False
509 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
510 self.elf_file, breakpad_dir=self.breakpad_dir
511 )
512 self.assertEqual(ret, self.sym_file)
513 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000514 1, ["sudo", "--", "dump_syms", "-v", self.elf_file]
Alex Klein1699fab2022-09-08 08:46:06 -0600515 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400516
Alex Klein1699fab2022-09-08 08:46:06 -0600517 def testLargeDebugFail(self):
518 """Running w/large .debug failed, but retry worked"""
519 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000520 ["dump_syms", "-v", self.elf_file, self.debug_dir], returncode=1
Alex Klein1699fab2022-09-08 08:46:06 -0600521 )
522 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
523 self.elf_file, self.debug_file, self.breakpad_dir
524 )
525 self.assertEqual(ret, self.sym_file)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800526 self.assertEqual(self.rc.call_count, 4)
Alex Klein1699fab2022-09-08 08:46:06 -0600527 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000528 1, ["dump_syms", "-v", self.elf_file, self.debug_dir]
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800529 )
530 # The current fallback from _DumpExpectingSymbols() to
531 # _DumpAllowingBasicFallback() causes the first dump_sums command to get
532 # repeated.
533 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000534 2, ["dump_syms", "-v", self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600535 )
536 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000537 3, ["dump_syms", "-v", "-c", "-r", self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600538 )
539 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400540
Ian Barkley-Yeungb5274442023-04-28 16:32:20 -0700541 def testForceBasicFallback(self):
542 """Running with force_basic_fallback
543
544 Test force_basic_fallback goes straight to _DumpAllowingBasicFallback().
545 """
546 self.rc.AddCmdResult(
547 ["dump_syms", "-v", self.elf_file, self.debug_dir], returncode=1
548 )
549 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
550 self.elf_file,
551 self.debug_file,
552 self.breakpad_dir,
553 force_basic_fallback=True,
554 )
555 self.assertEqual(ret, self.sym_file)
556 self.assertEqual(self.rc.call_count, 2)
557 # dump_syms -v should only happen once in _DumpAllowingBasicFallback()
558 # and not in _DumpExpectingSymbols(). We don't call /usr/bin/file in
559 # _ExpectGoodSymbols() either, so there's 2 fewer commands than
560 # in testLargeDebugFail.
561 self.assertCommandArgs(
562 0, ["dump_syms", "-v", self.elf_file, self.debug_dir]
563 )
564 self.assertCommandArgs(
565 1, ["dump_syms", "-v", "-c", "-r", self.elf_file, self.debug_dir]
566 )
567 self.assertExists(self.sym_file)
568
Alex Klein1699fab2022-09-08 08:46:06 -0600569 def testDebugFail(self):
570 """Running w/.debug always failed, but works w/out"""
571 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000572 ["dump_syms", "-v", self.elf_file, self.debug_dir], returncode=1
Alex Klein1699fab2022-09-08 08:46:06 -0600573 )
574 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000575 ["dump_syms", "-v", "-c", "-r", self.elf_file, self.debug_dir],
Alex Klein1699fab2022-09-08 08:46:06 -0600576 returncode=1,
577 )
578 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
579 self.elf_file, self.debug_file, self.breakpad_dir
580 )
581 self.assertEqual(ret, self.sym_file)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800582 self.assertEqual(self.rc.call_count, 5)
Alex Klein1699fab2022-09-08 08:46:06 -0600583 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000584 1, ["dump_syms", "-v", self.elf_file, self.debug_dir]
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800585 )
586 # The current fallback from _DumpExpectingSymbols() to
587 # _DumpAllowingBasicFallback() causes the first dump_sums command to get
588 # repeated.
589 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000590 2, ["dump_syms", "-v", self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600591 )
592 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000593 3, ["dump_syms", "-v", "-c", "-r", self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600594 )
Manoj Gupta68546b02023-05-01 21:55:19 +0000595 self.assertCommandArgs(4, ["dump_syms", "-v", self.elf_file])
Alex Klein1699fab2022-09-08 08:46:06 -0600596 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400597
Alex Klein1699fab2022-09-08 08:46:06 -0600598 def testCompleteFail(self):
599 """Running dump_syms always fails"""
600 self.rc.SetDefaultCmdResult(returncode=1)
601 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
602 self.elf_file, breakpad_dir=self.breakpad_dir
603 )
604 self.assertEqual(ret, 1)
605 # Make sure the num_errors flag works too.
Trent Apted1e2e4f32023-05-05 03:50:20 +0000606 num_errors = ctypes.c_int(0)
Alex Klein1699fab2022-09-08 08:46:06 -0600607 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
608 self.elf_file, breakpad_dir=self.breakpad_dir, num_errors=num_errors
609 )
610 self.assertEqual(ret, 1)
611 self.assertEqual(num_errors.value, 1)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400612
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800613 def testKernelObjects(self):
614 """Kernel object files should call _DumpAllowingBasicFallback()"""
615 ko_file = os.path.join(self.tempdir, "elf.ko")
616 osutils.Touch(ko_file)
617 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000618 ["dump_syms", "-v", ko_file, self.debug_dir], returncode=1
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800619 )
620 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000621 ["dump_syms", "-v", "-c", "-r", ko_file, self.debug_dir],
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800622 returncode=1,
623 )
624 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
625 ko_file, self.debug_file, self.breakpad_dir
626 )
627 self.assertEqual(ret, self.sym_file)
628 self.assertEqual(self.rc.call_count, 3)
629 # Only one call (at the beginning of _DumpAllowingBasicFallback())
630 # to "dump_syms -v"
Manoj Gupta68546b02023-05-01 21:55:19 +0000631 self.assertCommandArgs(0, ["dump_syms", "-v", ko_file, self.debug_dir])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800632 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000633 1, ["dump_syms", "-v", "-c", "-r", ko_file, self.debug_dir]
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800634 )
Manoj Gupta68546b02023-05-01 21:55:19 +0000635 self.assertCommandArgs(2, ["dump_syms", "-v", ko_file])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800636 self.assertExists(self.sym_file)
637
638 def testGoBinary(self):
639 """Go binaries should call _DumpAllowingBasicFallback()
640
641 Also tests that dump_syms failing with 'file contains no debugging
642 information' does not fail the script.
643 """
644 go_binary = os.path.join(self.tempdir, "goprogram")
645 osutils.Touch(go_binary)
646 go_debug_file = os.path.join(self.debug_dir, "goprogram.debug")
647 osutils.Touch(go_debug_file, makedirs=True)
648 FILE_OUT_GO = go_binary + (
649 ": ELF 64-bit LSB executable, x86-64, "
650 "version 1 (SYSV), statically linked, "
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800651 "Go BuildID=KKXVlL66E8Qmngr4qll9/5kOKGZw9I7TmNhoqKLqq/SiYVJam6w5Fo"
652 "39B3BtDo/ba8_ceezZ-3R4qEv6_-K, not stripped"
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800653 )
654 self.rc.AddCmdResult(["/usr/bin/file", go_binary], stdout=FILE_OUT_GO)
655 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000656 ["dump_syms", "-v", go_binary, self.debug_dir], returncode=1
657 )
658 self.rc.AddCmdResult(
659 ["dump_syms", "-v", "-c", "-r", go_binary, self.debug_dir],
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800660 returncode=1,
661 )
662 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000663 ["dump_syms", "-v", go_binary],
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800664 returncode=1,
665 stderr=(
666 f"{go_binary}: file contains no debugging information "
667 '(no ".stab" or ".debug_info" sections)'
668 ),
669 )
Trent Apted1e2e4f32023-05-05 03:50:20 +0000670 num_errors = ctypes.c_int(0)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800671 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
672 go_binary, go_debug_file, self.breakpad_dir
673 )
674 self.assertEqual(ret, 0)
675 self.assertEqual(self.rc.call_count, 4)
676 self.assertCommandArgs(0, ["/usr/bin/file", go_binary])
677 # Only one call (at the beginning of _DumpAllowingBasicFallback())
678 # to "dump_syms -v"
679 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000680 1, ["dump_syms", "-v", go_binary, self.debug_dir]
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800681 )
682 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000683 2, ["dump_syms", "-v", "-c", "-r", go_binary, self.debug_dir]
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800684 )
Manoj Gupta68546b02023-05-01 21:55:19 +0000685 self.assertCommandArgs(3, ["dump_syms", "-v", go_binary])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800686 self.assertNotExists(self.sym_file)
687 self.assertEqual(num_errors.value, 0)
688
Ian Barkley-Yeunge1785762023-03-08 18:32:18 -0800689 def _testBinaryIsInLocalFallback(self, directory, filename):
690 binary = os.path.join(self.tempdir, directory, filename)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800691 osutils.Touch(binary, makedirs=True)
Ian Barkley-Yeunge1785762023-03-08 18:32:18 -0800692 debug_dir = os.path.join(self.debug_dir, directory)
693 debug_file = os.path.join(debug_dir, f"{filename}.debug")
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800694 osutils.Touch(debug_file, makedirs=True)
695 self.rc.AddCmdResult(["/usr/bin/file", binary], stdout=self.FILE_OUT)
696 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000697 ["dump_syms", "-v", binary, debug_dir], returncode=1
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800698 )
699 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000700 ["dump_syms", "-v", "-c", "-r", binary, debug_dir],
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800701 returncode=1,
702 )
703 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000704 ["dump_syms", "-v", binary],
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800705 returncode=1,
706 stderr=(
707 f"{binary}: file contains no debugging information "
708 '(no ".stab" or ".debug_info" sections)'
709 ),
710 )
Trent Apted1e2e4f32023-05-05 03:50:20 +0000711 num_errors = ctypes.c_int(0)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800712 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
713 binary, debug_file, self.breakpad_dir, sysroot=self.tempdir
714 )
715 self.assertEqual(ret, 0)
716 self.assertEqual(self.rc.call_count, 4)
717 self.assertCommandArgs(0, ["/usr/bin/file", binary])
718 # Only one call (at the beginning of _DumpAllowingBasicFallback())
719 # to "dump_syms -v"
Manoj Gupta68546b02023-05-01 21:55:19 +0000720 self.assertCommandArgs(1, ["dump_syms", "-v", binary, debug_dir])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800721 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000722 2, ["dump_syms", "-v", "-c", "-r", binary, debug_dir]
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800723 )
Manoj Gupta68546b02023-05-01 21:55:19 +0000724 self.assertCommandArgs(3, ["dump_syms", "-v", binary])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800725 self.assertNotExists(self.sym_file)
726 self.assertEqual(num_errors.value, 0)
727
Ian Barkley-Yeunge1785762023-03-08 18:32:18 -0800728 def testAllowlist(self):
729 """Binaries in the allowlist should call _DumpAllowingBasicFallback()"""
730 self._testBinaryIsInLocalFallback("usr/bin", "goldctl")
731
732 def testUsrLocalSkip(self):
733 """Binaries in /usr/local should call _DumpAllowingBasicFallback()"""
734 self._testBinaryIsInLocalFallback("usr/local", "minidump_stackwalk")
735
Ian Barkley-Yeung4ed8cee2023-05-05 11:20:52 -0700736 def testOptGoogleCrosContainersLibSkip(self):
737 """Binaries in /opt/google/cros-containers/lib as well.
738
739 Binaries in /opt/google/cros-containers/lib should call
740 _DumpAllowingBasicFallback()
741 """
742 self._testBinaryIsInLocalFallback(
743 "opt/google/cros-containers/lib", "libc.so.6"
744 )
745
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400746
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800747class ValidateSymbolFileTest(cros_test_lib.TempDirTestCase):
748 """Tests ValidateSymbolFile"""
749
750 def _GetTestdataFile(self, filename: str) -> str:
751 """Gets the path to a file in the testdata directory.
752
753 Args:
754 filename: The base filename of the file.
755
756 Returns:
757 A string with the complete path to the file.
758 """
759 return os.path.join(os.path.dirname(__file__), "testdata", filename)
760
761 def testValidSymbolFiles(self):
762 """Make sure ValidateSymbolFile passes on valid files"""
763
764 # All files are in the testdata/ subdirectory.
765 VALID_SYMBOL_FILES = [
766 # A "normal" symbol file from an executable.
767 "basic.sym",
768 # A "normal" symbol file from a shared library.
769 "basic_lib.sym",
770 # A symbol file with PUBLIC records but no FUNC records.
771 "public_only.sym",
772 # A symbol file with FUNC records but no PUBLIC records.
773 "func_only.sym",
774 # A symbol file with at least one of every line type.
775 "all_line_types.sym",
776 ]
777
778 for file in VALID_SYMBOL_FILES:
779 with self.subTest(
780 file=file
781 ), multiprocessing.Manager() as mp_manager:
782 found_files = mp_manager.list()
783 self.assertTrue(
784 cros_generate_breakpad_symbols.ValidateSymbolFile(
785 self._GetTestdataFile(file),
786 "/build/board/bin/foo",
787 "/build/board",
788 found_files,
789 )
790 )
791 self.assertFalse(found_files)
792
793 def testInvalidSymbolFiles(self):
794 """Make sure ValidateSymbolFile fails on invalid files.
795
796 This test only covers cases that return false, not cases that raise
797 exceptions.
798 """
799
800 class InvalidSymbolFile:
801 """The name of an invalid symbol file + the expected error msg."""
802
803 def __init__(self, filename, expected_errors):
804 self.filename = filename
805 self.expected_errors = expected_errors
806
807 INVALID_SYMBOL_FILES = [
808 InvalidSymbolFile(
809 "bad_no_func_or_public.sym",
810 [
811 "WARNING:root:/build/board/bin/foo: "
812 "Symbol file has no FUNC or PUBLIC records"
813 ],
814 ),
815 InvalidSymbolFile(
816 "bad_no_stack.sym",
817 [
818 "WARNING:root:/build/board/bin/foo: "
819 "Symbol file has no STACK records"
820 ],
821 ),
822 InvalidSymbolFile(
823 "bad_no_module.sym",
824 [
825 "WARNING:root:/build/board/bin/foo: "
826 "Symbol file has 0 MODULE lines"
827 ],
828 ),
829 InvalidSymbolFile(
830 "bad_two_modules.sym",
831 [
832 "WARNING:root:/build/board/bin/foo: "
833 "Symbol file has 2 MODULE lines"
834 ],
835 ),
836 InvalidSymbolFile(
837 "bad_func_no_line_numbers.sym",
838 [
839 "WARNING:root:/build/board/bin/foo: "
840 "Symbol file has FUNC records but no line numbers"
841 ],
842 ),
843 InvalidSymbolFile(
844 "bad_line_numbers_no_file.sym",
845 [
846 "WARNING:root:/build/board/bin/foo: "
847 "Symbol file has line number records but no FILE records"
848 ],
849 ),
850 InvalidSymbolFile(
851 "bad_inline_no_files.sym",
852 [
853 "WARNING:root:/build/board/bin/foo: "
854 "Symbol file has INLINE records but no FILE records"
855 ],
856 ),
857 InvalidSymbolFile(
858 "bad_inline_no_origins.sym",
859 [
860 "WARNING:root:/build/board/bin/foo: "
861 "Symbol file has INLINE records but no INLINE_ORIGIN "
862 "records"
863 ],
864 ),
865 InvalidSymbolFile(
866 "blank.sym",
867 [
868 "WARNING:root:/build/board/bin/foo: "
869 "Symbol file has no STACK records",
870 "WARNING:root:/build/board/bin/foo: "
871 "Symbol file has 0 MODULE lines",
872 "WARNING:root:/build/board/bin/foo: "
873 "Symbol file has no FUNC or PUBLIC records",
874 ],
875 ),
876 ]
877
878 for file in INVALID_SYMBOL_FILES:
879 with self.subTest(
880 file=file.filename
881 ), multiprocessing.Manager() as mp_manager:
882 found_files = mp_manager.list()
883 with self.assertLogs(level=logging.WARNING) as cm:
884 self.assertFalse(
885 cros_generate_breakpad_symbols.ValidateSymbolFile(
886 self._GetTestdataFile(file.filename),
887 "/build/board/bin/foo",
888 "/build/board",
889 found_files,
890 )
891 )
892 self.assertEqual(file.expected_errors, cm.output)
893 self.assertFalse(found_files)
894
895 def testInvalidSymbolFilesWhichRaise(self):
896 """Test ValidateSymbolFile raise exceptions on certain files"""
897
898 class InvalidSymbolFile:
899 """The invalid symbol file + the expected exception message"""
900
901 def __init__(self, filename, expected_exception_regex):
902 self.filename = filename
903 self.expected_exception_regex = expected_exception_regex
904
905 INVALID_SYMBOL_FILES = [
906 InvalidSymbolFile(
907 "bad_unknown_line_type.sym",
908 "symbol file has unknown line type UNKNOWN",
909 ),
910 InvalidSymbolFile(
911 "bad_blank_line.sym",
912 "symbol file has unexpected blank line",
913 ),
914 InvalidSymbolFile(
915 "bad_short_func.sym",
916 r"symbol file has FUNC line with 2 words "
917 r"\(expected 5 or more\)",
918 ),
919 InvalidSymbolFile(
920 "bad_short_line_number.sym",
921 r"symbol file has line number line with 3 words "
922 r"\(expected 4 - 4\)",
923 ),
924 InvalidSymbolFile(
925 "bad_long_line_number.sym",
926 r"symbol file has line number line with 5 words "
927 r"\(expected 4 - 4\)",
928 ),
929 ]
930
931 for file in INVALID_SYMBOL_FILES:
932 with self.subTest(
933 file=file.filename
934 ), multiprocessing.Manager() as mp_manager:
935 found_files = mp_manager.list()
936 self.assertRaisesRegex(
937 ValueError,
938 file.expected_exception_regex,
939 cros_generate_breakpad_symbols.ValidateSymbolFile,
940 self._GetTestdataFile(file.filename),
941 "/build/board/bin/foo",
942 "/build/board",
943 found_files,
944 )
945
946 def testAllowlist(self):
947 """Test that ELFs on the allowlist are allowed to pass."""
948 with multiprocessing.Manager() as mp_manager:
949 found_files = mp_manager.list()
950 self.assertTrue(
951 cros_generate_breakpad_symbols.ValidateSymbolFile(
952 self._GetTestdataFile("bad_no_stack.sym"),
953 "/build/board/opt/google/chrome/nacl_helper_bootstrap",
954 "/build/board",
955 found_files,
956 )
957 )
958 self.assertFalse(found_files)
959
960 def testAllowlistRegex(self):
961 """Test that ELFs on the regex-based allowlist are allowed to pass."""
962 with multiprocessing.Manager() as mp_manager:
963 found_files = mp_manager.list()
964 self.assertTrue(
965 cros_generate_breakpad_symbols.ValidateSymbolFile(
966 self._GetTestdataFile("bad_no_stack.sym"),
967 "/build/board/usr/lib/libcros_ml_core.so",
968 "/build/board",
969 found_files,
970 )
971 )
972 self.assertFalse(found_files)
973
974 def _CreateSymbolFile(
975 self,
976 sym_file: pathlib.Path,
977 func_lines: int = 0,
978 public_lines: int = 0,
979 stack_lines: int = 0,
980 line_number_lines: int = 0,
981 ) -> None:
982 """Creates a symbol file.
983
984 Creates a symbol file with the given number of lines (and enough other
985 lines to pass validation) in the temp directory.
986
987 To pass validation, chrome.sym files must be huge; create them
988 programmatically during the test instead of checking in a real 800MB+
989 chrome symbol file.
990 """
991 with sym_file.open(mode="w", encoding="utf-8") as f:
992 f.write("MODULE OS CPU ID NAME\n")
993 f.write("FILE 0 /path/to/source.cc\n")
994 for func in range(0, func_lines):
995 f.write(f"FUNC {func} 1 0 function{func}\n")
996 for public in range(0, public_lines):
997 f.write(f"PUBLIC {public} 0 Public{public}\n")
998 for line in range(0, line_number_lines):
999 f.write(f"{line} 1 {line} 0\n")
1000 for stack in range(0, stack_lines):
1001 f.write(f"STACK CFI {stack} .cfa: $esp {stack} +\n")
1002
1003 def testValidChromeSymbolFile(self):
1004 """Test that a chrome symbol file can pass the additional checks"""
1005 sym_file = self.tempdir / "chrome.sym"
1006 self._CreateSymbolFile(
1007 sym_file,
1008 func_lines=100000,
1009 public_lines=10,
1010 stack_lines=1000000,
1011 line_number_lines=1000000,
1012 )
1013 with multiprocessing.Manager() as mp_manager:
1014 found_files = mp_manager.list()
1015 self.assertTrue(
1016 cros_generate_breakpad_symbols.ValidateSymbolFile(
1017 str(sym_file),
1018 "/build/board/opt/google/chrome/chrome",
1019 "/build/board",
1020 found_files,
1021 )
1022 )
1023 self.assertEqual(
1024 list(found_files),
1025 [cros_generate_breakpad_symbols.ExpectedFiles.ASH_CHROME],
1026 )
1027
1028 def testInvalidChromeSymbolFile(self):
1029 """Test that a chrome symbol file is held to higher standards."""
1030
1031 class ChromeSymbolFileTest:
1032 """Defines the subtest for an invalid Chrome symbol file."""
1033
1034 def __init__(
1035 self,
1036 name,
1037 expected_error,
1038 func_lines=100000,
1039 stack_lines=1000000,
1040 line_number_lines=1000000,
1041 ):
1042 self.name = name
1043 self.expected_error = expected_error
1044 self.func_lines = func_lines
1045 self.stack_lines = stack_lines
1046 self.line_number_lines = line_number_lines
1047
1048 CHROME_SYMBOL_TESTS = [
1049 ChromeSymbolFileTest(
1050 name="Insufficient FUNC records",
1051 func_lines=10000,
1052 expected_error="chrome should have at least 100,000 FUNC "
1053 "records, found 10000",
1054 ),
1055 ChromeSymbolFileTest(
1056 name="Insufficient STACK records",
1057 stack_lines=100000,
1058 expected_error="chrome should have at least 1,000,000 STACK "
1059 "records, found 100000",
1060 ),
1061 ChromeSymbolFileTest(
1062 name="Insufficient line number records",
1063 line_number_lines=100000,
1064 expected_error="chrome should have at least 1,000,000 "
1065 "line number records, found 100000",
1066 ),
1067 ]
1068 for test in CHROME_SYMBOL_TESTS:
1069 with self.subTest(
1070 name=test.name
1071 ), multiprocessing.Manager() as mp_manager:
1072 sym_file = self.tempdir / "chrome.sym"
1073 self._CreateSymbolFile(
1074 sym_file,
1075 func_lines=test.func_lines,
1076 public_lines=10,
1077 stack_lines=test.stack_lines,
1078 line_number_lines=test.line_number_lines,
1079 )
1080 found_files = mp_manager.list()
1081 with self.assertLogs(level=logging.WARNING) as cm:
1082 self.assertFalse(
1083 cros_generate_breakpad_symbols.ValidateSymbolFile(
1084 str(sym_file),
1085 "/build/board/opt/google/chrome/chrome",
1086 "/build/board",
1087 found_files,
1088 )
1089 )
1090 self.assertIn(test.expected_error, cm.output[0])
1091 self.assertEqual(len(cm.output), 1)
1092
1093 def testValidLibcSymbolFile(self):
1094 """Test that a libc.so symbol file can pass the additional checks."""
1095 with multiprocessing.Manager() as mp_manager:
1096 sym_file = self.tempdir / "libc.so.sym"
1097 self._CreateSymbolFile(
1098 sym_file, public_lines=200, stack_lines=20000
1099 )
1100 found_files = mp_manager.list()
1101 self.assertTrue(
1102 cros_generate_breakpad_symbols.ValidateSymbolFile(
1103 str(sym_file),
1104 "/build/board/lib64/libc.so.6",
1105 "/build/board",
1106 found_files,
1107 )
1108 )
1109 self.assertEqual(
1110 list(found_files),
1111 [cros_generate_breakpad_symbols.ExpectedFiles.LIBC],
1112 )
1113
1114 def testInvalidLibcSymbolFile(self):
1115 """Test that a libc.so symbol file is held to higher standards."""
1116
1117 class LibcSymbolFileTest:
1118 """Defines the subtest for an invalid libc symbol file."""
1119
1120 def __init__(
1121 self,
1122 name,
1123 expected_error,
1124 public_lines=200,
1125 stack_lines=20000,
1126 ):
1127 self.name = name
1128 self.expected_error = expected_error
1129 self.public_lines = public_lines
1130 self.stack_lines = stack_lines
1131
1132 LIBC_SYMBOL_TESTS = [
1133 LibcSymbolFileTest(
1134 name="Insufficient PUBLIC records",
1135 public_lines=50,
1136 expected_error="/build/board/lib64/libc.so.6 should have at "
1137 "least 100 PUBLIC records, found 50",
1138 ),
1139 LibcSymbolFileTest(
1140 name="Insufficient STACK records",
1141 stack_lines=1000,
1142 expected_error="/build/board/lib64/libc.so.6 should have at "
1143 "least 10000 STACK records, found 1000",
1144 ),
1145 ]
1146 for test in LIBC_SYMBOL_TESTS:
1147 with self.subTest(
1148 name=test.name
1149 ), multiprocessing.Manager() as mp_manager:
1150 sym_file = self.tempdir / "libc.so.sym"
1151 self._CreateSymbolFile(
1152 sym_file,
1153 public_lines=test.public_lines,
1154 stack_lines=test.stack_lines,
1155 )
1156 found_files = mp_manager.list()
1157 with self.assertLogs(level=logging.WARNING) as cm:
1158 self.assertFalse(
1159 cros_generate_breakpad_symbols.ValidateSymbolFile(
1160 str(sym_file),
1161 "/build/board/lib64/libc.so.6",
1162 "/build/board",
1163 found_files,
1164 )
1165 )
1166 self.assertIn(test.expected_error, cm.output[0])
1167 self.assertEqual(len(cm.output), 1)
1168
1169 def testValidCrashReporterSymbolFile(self):
1170 """Test a crash_reporter symbol file can pass the additional checks."""
1171 with multiprocessing.Manager() as mp_manager:
1172 sym_file = self.tempdir / "crash_reporter.sym"
1173 self._CreateSymbolFile(
1174 sym_file,
1175 func_lines=2000,
1176 public_lines=10,
1177 stack_lines=2000,
1178 line_number_lines=20000,
1179 )
1180 found_files = mp_manager.list()
1181 self.assertTrue(
1182 cros_generate_breakpad_symbols.ValidateSymbolFile(
1183 str(sym_file),
1184 "/build/board/sbin/crash_reporter",
1185 "/build/board",
1186 found_files,
1187 )
1188 )
1189 self.assertEqual(
1190 list(found_files),
1191 [cros_generate_breakpad_symbols.ExpectedFiles.CRASH_REPORTER],
1192 )
1193
1194 def testInvalidCrashReporterSymbolFile(self):
1195 """Test that a crash_reporter symbol file is held to higher standards"""
1196
1197 class CrashReporterSymbolFileTest:
1198 """Defines the subtest for an invalid crash_reporter symbol file."""
1199
1200 def __init__(
1201 self,
1202 name,
1203 expected_error,
1204 func_lines=2000,
1205 stack_lines=2000,
1206 line_number_lines=20000,
1207 ):
1208 self.name = name
1209 self.expected_error = expected_error
1210 self.func_lines = func_lines
1211 self.stack_lines = stack_lines
1212 self.line_number_lines = line_number_lines
1213
1214 CRASH_REPORTER_SYMBOL_TESTS = [
1215 CrashReporterSymbolFileTest(
1216 name="Insufficient FUNC records",
1217 func_lines=500,
1218 expected_error="crash_reporter should have at least 1000 FUNC "
1219 "records, found 500",
1220 ),
1221 CrashReporterSymbolFileTest(
1222 name="Insufficient STACK records",
1223 stack_lines=100,
1224 expected_error="crash_reporter should have at least 1000 STACK "
1225 "records, found 100",
1226 ),
1227 CrashReporterSymbolFileTest(
1228 name="Insufficient line number records",
1229 line_number_lines=2000,
1230 expected_error="crash_reporter should have at least 10,000 "
1231 "line number records, found 2000",
1232 ),
1233 ]
1234 for test in CRASH_REPORTER_SYMBOL_TESTS:
1235 with self.subTest(
1236 name=test.name
1237 ), multiprocessing.Manager() as mp_manager:
1238 sym_file = self.tempdir / "crash_reporter.sym"
1239 self._CreateSymbolFile(
1240 sym_file,
1241 func_lines=test.func_lines,
1242 stack_lines=test.stack_lines,
1243 line_number_lines=test.line_number_lines,
1244 )
1245 found_files = mp_manager.list()
1246 with self.assertLogs(level=logging.WARNING) as cm:
1247 self.assertFalse(
1248 cros_generate_breakpad_symbols.ValidateSymbolFile(
1249 str(sym_file),
1250 "/build/board/sbin/crash_reporter",
1251 "/build/board",
1252 found_files,
1253 )
1254 )
1255 self.assertIn(test.expected_error, cm.output[0])
1256 self.assertEqual(len(cm.output), 1)
1257
1258 def testValidLibMetricsSymbolFile(self):
1259 """Test a libmetrics.so symbol file can pass the additional checks."""
1260 with multiprocessing.Manager() as mp_manager:
1261 sym_file = self.tempdir / "libmetrics.so.sym"
1262 self._CreateSymbolFile(
1263 sym_file,
1264 func_lines=200,
1265 public_lines=2,
1266 stack_lines=2000,
1267 line_number_lines=10000,
1268 )
1269 found_files = mp_manager.list()
1270 self.assertTrue(
1271 cros_generate_breakpad_symbols.ValidateSymbolFile(
1272 str(sym_file),
1273 "/build/board/usr/lib64/libmetrics.so",
1274 "/build/board",
1275 found_files,
1276 )
1277 )
1278 self.assertEqual(
1279 list(found_files),
1280 [cros_generate_breakpad_symbols.ExpectedFiles.LIBMETRICS],
1281 )
1282
1283 def testInvalidLibMetricsSymbolFile(self):
1284 """Test that a libmetrics.so symbol file is held to higher standards."""
1285
1286 class LibMetricsSymbolFileTest:
1287 """Defines the subtest for an invalid libmetrics.so symbol file."""
1288
1289 def __init__(
1290 self,
1291 name,
1292 expected_error,
1293 func_lines=200,
1294 public_lines=2,
1295 stack_lines=2000,
1296 line_number_lines=10000,
1297 ):
1298 self.name = name
1299 self.expected_error = expected_error
1300 self.func_lines = func_lines
1301 self.public_lines = public_lines
1302 self.stack_lines = stack_lines
1303 self.line_number_lines = line_number_lines
1304
1305 LIBMETRICS_SYMBOL_TESTS = [
1306 LibMetricsSymbolFileTest(
1307 name="Insufficient FUNC records",
1308 func_lines=10,
1309 expected_error="libmetrics should have at least 100 FUNC "
1310 "records, found 10",
1311 ),
1312 LibMetricsSymbolFileTest(
1313 name="Insufficient PUBLIC records",
1314 public_lines=0,
1315 expected_error="libmetrics should have at least 1 PUBLIC "
1316 "record, found 0",
1317 ),
1318 LibMetricsSymbolFileTest(
1319 name="Insufficient STACK records",
1320 stack_lines=500,
1321 expected_error="libmetrics should have at least 1000 STACK "
1322 "records, found 500",
1323 ),
1324 LibMetricsSymbolFileTest(
1325 name="Insufficient line number records",
1326 line_number_lines=2000,
1327 expected_error="libmetrics should have at least 5000 "
1328 "line number records, found 2000",
1329 ),
1330 ]
1331 for test in LIBMETRICS_SYMBOL_TESTS:
1332 with self.subTest(
1333 name=test.name
1334 ), multiprocessing.Manager() as mp_manager:
1335 sym_file = self.tempdir / "libmetrics.so.sym"
1336 self._CreateSymbolFile(
1337 sym_file,
1338 func_lines=test.func_lines,
1339 public_lines=test.public_lines,
1340 stack_lines=test.stack_lines,
1341 line_number_lines=test.line_number_lines,
1342 )
1343 found_files = mp_manager.list()
1344 with self.assertLogs(level=logging.WARNING) as cm:
1345 self.assertFalse(
1346 cros_generate_breakpad_symbols.ValidateSymbolFile(
1347 str(sym_file),
1348 "/build/board/usr/lib64/libmetrics.so",
1349 "/build/board",
1350 found_files,
1351 )
1352 )
1353 self.assertIn(test.expected_error, cm.output[0])
1354 self.assertEqual(len(cm.output), 1)
1355
1356
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001357class UtilsTestDir(cros_test_lib.TempDirTestCase):
Alex Klein1699fab2022-09-08 08:46:06 -06001358 """Tests ReadSymsHeader."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001359
Alex Klein1699fab2022-09-08 08:46:06 -06001360 def testReadSymsHeaderGoodFile(self):
1361 """Make sure ReadSymsHeader can parse sym files"""
1362 sym_file = os.path.join(self.tempdir, "sym")
1363 osutils.WriteFile(sym_file, "MODULE Linux x86 s0m31D chrooome")
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001364 result = cros_generate_breakpad_symbols.ReadSymsHeader(
1365 sym_file, "unused_elfname"
1366 )
Alex Klein1699fab2022-09-08 08:46:06 -06001367 self.assertEqual(result.cpu, "x86")
1368 self.assertEqual(result.id, "s0m31D")
1369 self.assertEqual(result.name, "chrooome")
1370 self.assertEqual(result.os, "Linux")
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001371
1372
1373class UtilsTest(cros_test_lib.TestCase):
Alex Klein1699fab2022-09-08 08:46:06 -06001374 """Tests ReadSymsHeader."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001375
Alex Klein1699fab2022-09-08 08:46:06 -06001376 def testReadSymsHeaderGoodBuffer(self):
1377 """Make sure ReadSymsHeader can parse sym file handles"""
1378 result = cros_generate_breakpad_symbols.ReadSymsHeader(
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001379 io.BytesIO(b"MODULE Linux arm MY-ID-HERE blkid"), "unused_elfname"
Alex Klein1699fab2022-09-08 08:46:06 -06001380 )
1381 self.assertEqual(result.cpu, "arm")
1382 self.assertEqual(result.id, "MY-ID-HERE")
1383 self.assertEqual(result.name, "blkid")
1384 self.assertEqual(result.os, "Linux")
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001385
Alex Klein1699fab2022-09-08 08:46:06 -06001386 def testReadSymsHeaderBadd(self):
1387 """Make sure ReadSymsHeader throws on bad sym files"""
1388 self.assertRaises(
1389 ValueError,
1390 cros_generate_breakpad_symbols.ReadSymsHeader,
1391 io.BytesIO(b"asdf"),
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001392 "unused_elfname",
Alex Klein1699fab2022-09-08 08:46:06 -06001393 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001394
Alex Klein1699fab2022-09-08 08:46:06 -06001395 def testBreakpadDir(self):
1396 """Make sure board->breakpad path expansion works"""
1397 expected = "/build/blah/usr/lib/debug/breakpad"
1398 result = cros_generate_breakpad_symbols.FindBreakpadDir("blah")
1399 self.assertEqual(expected, result)
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001400
Alex Klein1699fab2022-09-08 08:46:06 -06001401 def testDebugDir(self):
1402 """Make sure board->debug path expansion works"""
1403 expected = "/build/blah/usr/lib/debug"
1404 result = cros_generate_breakpad_symbols.FindDebugDir("blah")
1405 self.assertEqual(expected, result)