blob: cb49a40f8e96c4edd78af70956b507015bd979a3 [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
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400736
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800737class ValidateSymbolFileTest(cros_test_lib.TempDirTestCase):
738 """Tests ValidateSymbolFile"""
739
740 def _GetTestdataFile(self, filename: str) -> str:
741 """Gets the path to a file in the testdata directory.
742
743 Args:
744 filename: The base filename of the file.
745
746 Returns:
747 A string with the complete path to the file.
748 """
749 return os.path.join(os.path.dirname(__file__), "testdata", filename)
750
751 def testValidSymbolFiles(self):
752 """Make sure ValidateSymbolFile passes on valid files"""
753
754 # All files are in the testdata/ subdirectory.
755 VALID_SYMBOL_FILES = [
756 # A "normal" symbol file from an executable.
757 "basic.sym",
758 # A "normal" symbol file from a shared library.
759 "basic_lib.sym",
760 # A symbol file with PUBLIC records but no FUNC records.
761 "public_only.sym",
762 # A symbol file with FUNC records but no PUBLIC records.
763 "func_only.sym",
764 # A symbol file with at least one of every line type.
765 "all_line_types.sym",
766 ]
767
768 for file in VALID_SYMBOL_FILES:
769 with self.subTest(
770 file=file
771 ), multiprocessing.Manager() as mp_manager:
772 found_files = mp_manager.list()
773 self.assertTrue(
774 cros_generate_breakpad_symbols.ValidateSymbolFile(
775 self._GetTestdataFile(file),
776 "/build/board/bin/foo",
777 "/build/board",
778 found_files,
779 )
780 )
781 self.assertFalse(found_files)
782
783 def testInvalidSymbolFiles(self):
784 """Make sure ValidateSymbolFile fails on invalid files.
785
786 This test only covers cases that return false, not cases that raise
787 exceptions.
788 """
789
790 class InvalidSymbolFile:
791 """The name of an invalid symbol file + the expected error msg."""
792
793 def __init__(self, filename, expected_errors):
794 self.filename = filename
795 self.expected_errors = expected_errors
796
797 INVALID_SYMBOL_FILES = [
798 InvalidSymbolFile(
799 "bad_no_func_or_public.sym",
800 [
801 "WARNING:root:/build/board/bin/foo: "
802 "Symbol file has no FUNC or PUBLIC records"
803 ],
804 ),
805 InvalidSymbolFile(
806 "bad_no_stack.sym",
807 [
808 "WARNING:root:/build/board/bin/foo: "
809 "Symbol file has no STACK records"
810 ],
811 ),
812 InvalidSymbolFile(
813 "bad_no_module.sym",
814 [
815 "WARNING:root:/build/board/bin/foo: "
816 "Symbol file has 0 MODULE lines"
817 ],
818 ),
819 InvalidSymbolFile(
820 "bad_two_modules.sym",
821 [
822 "WARNING:root:/build/board/bin/foo: "
823 "Symbol file has 2 MODULE lines"
824 ],
825 ),
826 InvalidSymbolFile(
827 "bad_func_no_line_numbers.sym",
828 [
829 "WARNING:root:/build/board/bin/foo: "
830 "Symbol file has FUNC records but no line numbers"
831 ],
832 ),
833 InvalidSymbolFile(
834 "bad_line_numbers_no_file.sym",
835 [
836 "WARNING:root:/build/board/bin/foo: "
837 "Symbol file has line number records but no FILE records"
838 ],
839 ),
840 InvalidSymbolFile(
841 "bad_inline_no_files.sym",
842 [
843 "WARNING:root:/build/board/bin/foo: "
844 "Symbol file has INLINE records but no FILE records"
845 ],
846 ),
847 InvalidSymbolFile(
848 "bad_inline_no_origins.sym",
849 [
850 "WARNING:root:/build/board/bin/foo: "
851 "Symbol file has INLINE records but no INLINE_ORIGIN "
852 "records"
853 ],
854 ),
855 InvalidSymbolFile(
856 "blank.sym",
857 [
858 "WARNING:root:/build/board/bin/foo: "
859 "Symbol file has no STACK records",
860 "WARNING:root:/build/board/bin/foo: "
861 "Symbol file has 0 MODULE lines",
862 "WARNING:root:/build/board/bin/foo: "
863 "Symbol file has no FUNC or PUBLIC records",
864 ],
865 ),
866 ]
867
868 for file in INVALID_SYMBOL_FILES:
869 with self.subTest(
870 file=file.filename
871 ), multiprocessing.Manager() as mp_manager:
872 found_files = mp_manager.list()
873 with self.assertLogs(level=logging.WARNING) as cm:
874 self.assertFalse(
875 cros_generate_breakpad_symbols.ValidateSymbolFile(
876 self._GetTestdataFile(file.filename),
877 "/build/board/bin/foo",
878 "/build/board",
879 found_files,
880 )
881 )
882 self.assertEqual(file.expected_errors, cm.output)
883 self.assertFalse(found_files)
884
885 def testInvalidSymbolFilesWhichRaise(self):
886 """Test ValidateSymbolFile raise exceptions on certain files"""
887
888 class InvalidSymbolFile:
889 """The invalid symbol file + the expected exception message"""
890
891 def __init__(self, filename, expected_exception_regex):
892 self.filename = filename
893 self.expected_exception_regex = expected_exception_regex
894
895 INVALID_SYMBOL_FILES = [
896 InvalidSymbolFile(
897 "bad_unknown_line_type.sym",
898 "symbol file has unknown line type UNKNOWN",
899 ),
900 InvalidSymbolFile(
901 "bad_blank_line.sym",
902 "symbol file has unexpected blank line",
903 ),
904 InvalidSymbolFile(
905 "bad_short_func.sym",
906 r"symbol file has FUNC line with 2 words "
907 r"\(expected 5 or more\)",
908 ),
909 InvalidSymbolFile(
910 "bad_short_line_number.sym",
911 r"symbol file has line number line with 3 words "
912 r"\(expected 4 - 4\)",
913 ),
914 InvalidSymbolFile(
915 "bad_long_line_number.sym",
916 r"symbol file has line number line with 5 words "
917 r"\(expected 4 - 4\)",
918 ),
919 ]
920
921 for file in INVALID_SYMBOL_FILES:
922 with self.subTest(
923 file=file.filename
924 ), multiprocessing.Manager() as mp_manager:
925 found_files = mp_manager.list()
926 self.assertRaisesRegex(
927 ValueError,
928 file.expected_exception_regex,
929 cros_generate_breakpad_symbols.ValidateSymbolFile,
930 self._GetTestdataFile(file.filename),
931 "/build/board/bin/foo",
932 "/build/board",
933 found_files,
934 )
935
936 def testAllowlist(self):
937 """Test that ELFs on the allowlist are allowed to pass."""
938 with multiprocessing.Manager() as mp_manager:
939 found_files = mp_manager.list()
940 self.assertTrue(
941 cros_generate_breakpad_symbols.ValidateSymbolFile(
942 self._GetTestdataFile("bad_no_stack.sym"),
943 "/build/board/opt/google/chrome/nacl_helper_bootstrap",
944 "/build/board",
945 found_files,
946 )
947 )
948 self.assertFalse(found_files)
949
950 def testAllowlistRegex(self):
951 """Test that ELFs on the regex-based allowlist are allowed to pass."""
952 with multiprocessing.Manager() as mp_manager:
953 found_files = mp_manager.list()
954 self.assertTrue(
955 cros_generate_breakpad_symbols.ValidateSymbolFile(
956 self._GetTestdataFile("bad_no_stack.sym"),
957 "/build/board/usr/lib/libcros_ml_core.so",
958 "/build/board",
959 found_files,
960 )
961 )
962 self.assertFalse(found_files)
963
964 def _CreateSymbolFile(
965 self,
966 sym_file: pathlib.Path,
967 func_lines: int = 0,
968 public_lines: int = 0,
969 stack_lines: int = 0,
970 line_number_lines: int = 0,
971 ) -> None:
972 """Creates a symbol file.
973
974 Creates a symbol file with the given number of lines (and enough other
975 lines to pass validation) in the temp directory.
976
977 To pass validation, chrome.sym files must be huge; create them
978 programmatically during the test instead of checking in a real 800MB+
979 chrome symbol file.
980 """
981 with sym_file.open(mode="w", encoding="utf-8") as f:
982 f.write("MODULE OS CPU ID NAME\n")
983 f.write("FILE 0 /path/to/source.cc\n")
984 for func in range(0, func_lines):
985 f.write(f"FUNC {func} 1 0 function{func}\n")
986 for public in range(0, public_lines):
987 f.write(f"PUBLIC {public} 0 Public{public}\n")
988 for line in range(0, line_number_lines):
989 f.write(f"{line} 1 {line} 0\n")
990 for stack in range(0, stack_lines):
991 f.write(f"STACK CFI {stack} .cfa: $esp {stack} +\n")
992
993 def testValidChromeSymbolFile(self):
994 """Test that a chrome symbol file can pass the additional checks"""
995 sym_file = self.tempdir / "chrome.sym"
996 self._CreateSymbolFile(
997 sym_file,
998 func_lines=100000,
999 public_lines=10,
1000 stack_lines=1000000,
1001 line_number_lines=1000000,
1002 )
1003 with multiprocessing.Manager() as mp_manager:
1004 found_files = mp_manager.list()
1005 self.assertTrue(
1006 cros_generate_breakpad_symbols.ValidateSymbolFile(
1007 str(sym_file),
1008 "/build/board/opt/google/chrome/chrome",
1009 "/build/board",
1010 found_files,
1011 )
1012 )
1013 self.assertEqual(
1014 list(found_files),
1015 [cros_generate_breakpad_symbols.ExpectedFiles.ASH_CHROME],
1016 )
1017
1018 def testInvalidChromeSymbolFile(self):
1019 """Test that a chrome symbol file is held to higher standards."""
1020
1021 class ChromeSymbolFileTest:
1022 """Defines the subtest for an invalid Chrome symbol file."""
1023
1024 def __init__(
1025 self,
1026 name,
1027 expected_error,
1028 func_lines=100000,
1029 stack_lines=1000000,
1030 line_number_lines=1000000,
1031 ):
1032 self.name = name
1033 self.expected_error = expected_error
1034 self.func_lines = func_lines
1035 self.stack_lines = stack_lines
1036 self.line_number_lines = line_number_lines
1037
1038 CHROME_SYMBOL_TESTS = [
1039 ChromeSymbolFileTest(
1040 name="Insufficient FUNC records",
1041 func_lines=10000,
1042 expected_error="chrome should have at least 100,000 FUNC "
1043 "records, found 10000",
1044 ),
1045 ChromeSymbolFileTest(
1046 name="Insufficient STACK records",
1047 stack_lines=100000,
1048 expected_error="chrome should have at least 1,000,000 STACK "
1049 "records, found 100000",
1050 ),
1051 ChromeSymbolFileTest(
1052 name="Insufficient line number records",
1053 line_number_lines=100000,
1054 expected_error="chrome should have at least 1,000,000 "
1055 "line number records, found 100000",
1056 ),
1057 ]
1058 for test in CHROME_SYMBOL_TESTS:
1059 with self.subTest(
1060 name=test.name
1061 ), multiprocessing.Manager() as mp_manager:
1062 sym_file = self.tempdir / "chrome.sym"
1063 self._CreateSymbolFile(
1064 sym_file,
1065 func_lines=test.func_lines,
1066 public_lines=10,
1067 stack_lines=test.stack_lines,
1068 line_number_lines=test.line_number_lines,
1069 )
1070 found_files = mp_manager.list()
1071 with self.assertLogs(level=logging.WARNING) as cm:
1072 self.assertFalse(
1073 cros_generate_breakpad_symbols.ValidateSymbolFile(
1074 str(sym_file),
1075 "/build/board/opt/google/chrome/chrome",
1076 "/build/board",
1077 found_files,
1078 )
1079 )
1080 self.assertIn(test.expected_error, cm.output[0])
1081 self.assertEqual(len(cm.output), 1)
1082
1083 def testValidLibcSymbolFile(self):
1084 """Test that a libc.so symbol file can pass the additional checks."""
1085 with multiprocessing.Manager() as mp_manager:
1086 sym_file = self.tempdir / "libc.so.sym"
1087 self._CreateSymbolFile(
1088 sym_file, public_lines=200, stack_lines=20000
1089 )
1090 found_files = mp_manager.list()
1091 self.assertTrue(
1092 cros_generate_breakpad_symbols.ValidateSymbolFile(
1093 str(sym_file),
1094 "/build/board/lib64/libc.so.6",
1095 "/build/board",
1096 found_files,
1097 )
1098 )
1099 self.assertEqual(
1100 list(found_files),
1101 [cros_generate_breakpad_symbols.ExpectedFiles.LIBC],
1102 )
1103
1104 def testInvalidLibcSymbolFile(self):
1105 """Test that a libc.so symbol file is held to higher standards."""
1106
1107 class LibcSymbolFileTest:
1108 """Defines the subtest for an invalid libc symbol file."""
1109
1110 def __init__(
1111 self,
1112 name,
1113 expected_error,
1114 public_lines=200,
1115 stack_lines=20000,
1116 ):
1117 self.name = name
1118 self.expected_error = expected_error
1119 self.public_lines = public_lines
1120 self.stack_lines = stack_lines
1121
1122 LIBC_SYMBOL_TESTS = [
1123 LibcSymbolFileTest(
1124 name="Insufficient PUBLIC records",
1125 public_lines=50,
1126 expected_error="/build/board/lib64/libc.so.6 should have at "
1127 "least 100 PUBLIC records, found 50",
1128 ),
1129 LibcSymbolFileTest(
1130 name="Insufficient STACK records",
1131 stack_lines=1000,
1132 expected_error="/build/board/lib64/libc.so.6 should have at "
1133 "least 10000 STACK records, found 1000",
1134 ),
1135 ]
1136 for test in LIBC_SYMBOL_TESTS:
1137 with self.subTest(
1138 name=test.name
1139 ), multiprocessing.Manager() as mp_manager:
1140 sym_file = self.tempdir / "libc.so.sym"
1141 self._CreateSymbolFile(
1142 sym_file,
1143 public_lines=test.public_lines,
1144 stack_lines=test.stack_lines,
1145 )
1146 found_files = mp_manager.list()
1147 with self.assertLogs(level=logging.WARNING) as cm:
1148 self.assertFalse(
1149 cros_generate_breakpad_symbols.ValidateSymbolFile(
1150 str(sym_file),
1151 "/build/board/lib64/libc.so.6",
1152 "/build/board",
1153 found_files,
1154 )
1155 )
1156 self.assertIn(test.expected_error, cm.output[0])
1157 self.assertEqual(len(cm.output), 1)
1158
1159 def testValidCrashReporterSymbolFile(self):
1160 """Test a crash_reporter symbol file can pass the additional checks."""
1161 with multiprocessing.Manager() as mp_manager:
1162 sym_file = self.tempdir / "crash_reporter.sym"
1163 self._CreateSymbolFile(
1164 sym_file,
1165 func_lines=2000,
1166 public_lines=10,
1167 stack_lines=2000,
1168 line_number_lines=20000,
1169 )
1170 found_files = mp_manager.list()
1171 self.assertTrue(
1172 cros_generate_breakpad_symbols.ValidateSymbolFile(
1173 str(sym_file),
1174 "/build/board/sbin/crash_reporter",
1175 "/build/board",
1176 found_files,
1177 )
1178 )
1179 self.assertEqual(
1180 list(found_files),
1181 [cros_generate_breakpad_symbols.ExpectedFiles.CRASH_REPORTER],
1182 )
1183
1184 def testInvalidCrashReporterSymbolFile(self):
1185 """Test that a crash_reporter symbol file is held to higher standards"""
1186
1187 class CrashReporterSymbolFileTest:
1188 """Defines the subtest for an invalid crash_reporter symbol file."""
1189
1190 def __init__(
1191 self,
1192 name,
1193 expected_error,
1194 func_lines=2000,
1195 stack_lines=2000,
1196 line_number_lines=20000,
1197 ):
1198 self.name = name
1199 self.expected_error = expected_error
1200 self.func_lines = func_lines
1201 self.stack_lines = stack_lines
1202 self.line_number_lines = line_number_lines
1203
1204 CRASH_REPORTER_SYMBOL_TESTS = [
1205 CrashReporterSymbolFileTest(
1206 name="Insufficient FUNC records",
1207 func_lines=500,
1208 expected_error="crash_reporter should have at least 1000 FUNC "
1209 "records, found 500",
1210 ),
1211 CrashReporterSymbolFileTest(
1212 name="Insufficient STACK records",
1213 stack_lines=100,
1214 expected_error="crash_reporter should have at least 1000 STACK "
1215 "records, found 100",
1216 ),
1217 CrashReporterSymbolFileTest(
1218 name="Insufficient line number records",
1219 line_number_lines=2000,
1220 expected_error="crash_reporter should have at least 10,000 "
1221 "line number records, found 2000",
1222 ),
1223 ]
1224 for test in CRASH_REPORTER_SYMBOL_TESTS:
1225 with self.subTest(
1226 name=test.name
1227 ), multiprocessing.Manager() as mp_manager:
1228 sym_file = self.tempdir / "crash_reporter.sym"
1229 self._CreateSymbolFile(
1230 sym_file,
1231 func_lines=test.func_lines,
1232 stack_lines=test.stack_lines,
1233 line_number_lines=test.line_number_lines,
1234 )
1235 found_files = mp_manager.list()
1236 with self.assertLogs(level=logging.WARNING) as cm:
1237 self.assertFalse(
1238 cros_generate_breakpad_symbols.ValidateSymbolFile(
1239 str(sym_file),
1240 "/build/board/sbin/crash_reporter",
1241 "/build/board",
1242 found_files,
1243 )
1244 )
1245 self.assertIn(test.expected_error, cm.output[0])
1246 self.assertEqual(len(cm.output), 1)
1247
1248 def testValidLibMetricsSymbolFile(self):
1249 """Test a libmetrics.so symbol file can pass the additional checks."""
1250 with multiprocessing.Manager() as mp_manager:
1251 sym_file = self.tempdir / "libmetrics.so.sym"
1252 self._CreateSymbolFile(
1253 sym_file,
1254 func_lines=200,
1255 public_lines=2,
1256 stack_lines=2000,
1257 line_number_lines=10000,
1258 )
1259 found_files = mp_manager.list()
1260 self.assertTrue(
1261 cros_generate_breakpad_symbols.ValidateSymbolFile(
1262 str(sym_file),
1263 "/build/board/usr/lib64/libmetrics.so",
1264 "/build/board",
1265 found_files,
1266 )
1267 )
1268 self.assertEqual(
1269 list(found_files),
1270 [cros_generate_breakpad_symbols.ExpectedFiles.LIBMETRICS],
1271 )
1272
1273 def testInvalidLibMetricsSymbolFile(self):
1274 """Test that a libmetrics.so symbol file is held to higher standards."""
1275
1276 class LibMetricsSymbolFileTest:
1277 """Defines the subtest for an invalid libmetrics.so symbol file."""
1278
1279 def __init__(
1280 self,
1281 name,
1282 expected_error,
1283 func_lines=200,
1284 public_lines=2,
1285 stack_lines=2000,
1286 line_number_lines=10000,
1287 ):
1288 self.name = name
1289 self.expected_error = expected_error
1290 self.func_lines = func_lines
1291 self.public_lines = public_lines
1292 self.stack_lines = stack_lines
1293 self.line_number_lines = line_number_lines
1294
1295 LIBMETRICS_SYMBOL_TESTS = [
1296 LibMetricsSymbolFileTest(
1297 name="Insufficient FUNC records",
1298 func_lines=10,
1299 expected_error="libmetrics should have at least 100 FUNC "
1300 "records, found 10",
1301 ),
1302 LibMetricsSymbolFileTest(
1303 name="Insufficient PUBLIC records",
1304 public_lines=0,
1305 expected_error="libmetrics should have at least 1 PUBLIC "
1306 "record, found 0",
1307 ),
1308 LibMetricsSymbolFileTest(
1309 name="Insufficient STACK records",
1310 stack_lines=500,
1311 expected_error="libmetrics should have at least 1000 STACK "
1312 "records, found 500",
1313 ),
1314 LibMetricsSymbolFileTest(
1315 name="Insufficient line number records",
1316 line_number_lines=2000,
1317 expected_error="libmetrics should have at least 5000 "
1318 "line number records, found 2000",
1319 ),
1320 ]
1321 for test in LIBMETRICS_SYMBOL_TESTS:
1322 with self.subTest(
1323 name=test.name
1324 ), multiprocessing.Manager() as mp_manager:
1325 sym_file = self.tempdir / "libmetrics.so.sym"
1326 self._CreateSymbolFile(
1327 sym_file,
1328 func_lines=test.func_lines,
1329 public_lines=test.public_lines,
1330 stack_lines=test.stack_lines,
1331 line_number_lines=test.line_number_lines,
1332 )
1333 found_files = mp_manager.list()
1334 with self.assertLogs(level=logging.WARNING) as cm:
1335 self.assertFalse(
1336 cros_generate_breakpad_symbols.ValidateSymbolFile(
1337 str(sym_file),
1338 "/build/board/usr/lib64/libmetrics.so",
1339 "/build/board",
1340 found_files,
1341 )
1342 )
1343 self.assertIn(test.expected_error, cm.output[0])
1344 self.assertEqual(len(cm.output), 1)
1345
1346
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001347class UtilsTestDir(cros_test_lib.TempDirTestCase):
Alex Klein1699fab2022-09-08 08:46:06 -06001348 """Tests ReadSymsHeader."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001349
Alex Klein1699fab2022-09-08 08:46:06 -06001350 def testReadSymsHeaderGoodFile(self):
1351 """Make sure ReadSymsHeader can parse sym files"""
1352 sym_file = os.path.join(self.tempdir, "sym")
1353 osutils.WriteFile(sym_file, "MODULE Linux x86 s0m31D chrooome")
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001354 result = cros_generate_breakpad_symbols.ReadSymsHeader(
1355 sym_file, "unused_elfname"
1356 )
Alex Klein1699fab2022-09-08 08:46:06 -06001357 self.assertEqual(result.cpu, "x86")
1358 self.assertEqual(result.id, "s0m31D")
1359 self.assertEqual(result.name, "chrooome")
1360 self.assertEqual(result.os, "Linux")
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001361
1362
1363class UtilsTest(cros_test_lib.TestCase):
Alex Klein1699fab2022-09-08 08:46:06 -06001364 """Tests ReadSymsHeader."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001365
Alex Klein1699fab2022-09-08 08:46:06 -06001366 def testReadSymsHeaderGoodBuffer(self):
1367 """Make sure ReadSymsHeader can parse sym file handles"""
1368 result = cros_generate_breakpad_symbols.ReadSymsHeader(
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001369 io.BytesIO(b"MODULE Linux arm MY-ID-HERE blkid"), "unused_elfname"
Alex Klein1699fab2022-09-08 08:46:06 -06001370 )
1371 self.assertEqual(result.cpu, "arm")
1372 self.assertEqual(result.id, "MY-ID-HERE")
1373 self.assertEqual(result.name, "blkid")
1374 self.assertEqual(result.os, "Linux")
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001375
Alex Klein1699fab2022-09-08 08:46:06 -06001376 def testReadSymsHeaderBadd(self):
1377 """Make sure ReadSymsHeader throws on bad sym files"""
1378 self.assertRaises(
1379 ValueError,
1380 cros_generate_breakpad_symbols.ReadSymsHeader,
1381 io.BytesIO(b"asdf"),
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001382 "unused_elfname",
Alex Klein1699fab2022-09-08 08:46:06 -06001383 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001384
Alex Klein1699fab2022-09-08 08:46:06 -06001385 def testBreakpadDir(self):
1386 """Make sure board->breakpad path expansion works"""
1387 expected = "/build/blah/usr/lib/debug/breakpad"
1388 result = cros_generate_breakpad_symbols.FindBreakpadDir("blah")
1389 self.assertEqual(expected, result)
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001390
Alex Klein1699fab2022-09-08 08:46:06 -06001391 def testDebugDir(self):
1392 """Make sure board->debug path expansion works"""
1393 expected = "/build/blah/usr/lib/debug"
1394 result = cros_generate_breakpad_symbols.FindDebugDir("blah")
1395 self.assertEqual(expected, result)