blob: aee67db5f4810216ed6b4d6415abdccfbcc45743 [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
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400284
Benjamin Gordon121a2aa2018-05-04 16:24:45 -0600285class GenerateSymbolTest(cros_test_lib.RunCommandTempDirTestCase):
Alex Klein1699fab2022-09-08 08:46:06 -0600286 """Test GenerateBreakpadSymbol."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400287
Alex Klein1699fab2022-09-08 08:46:06 -0600288 def setUp(self):
289 self.elf_file = os.path.join(self.tempdir, "elf")
290 osutils.Touch(self.elf_file)
291 self.debug_dir = os.path.join(self.tempdir, "debug")
292 self.debug_file = os.path.join(self.debug_dir, "elf.debug")
293 osutils.Touch(self.debug_file, makedirs=True)
294 # Not needed as the code itself should create it as needed.
295 self.breakpad_dir = os.path.join(self.debug_dir, "breakpad")
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400296
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800297 self.FILE_OUT = (
298 f"{self.elf_file}: ELF 64-bit LSB pie executable, x86-64, "
299 "version 1 (SYSV), dynamically linked, interpreter "
300 "/lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, "
301 "BuildID[sha1]=cf9a21fa6b14bfb2dfcb76effd713c4536014d95, stripped"
302 )
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800303 # A symbol file which would pass validation.
304 MINIMAL_SYMBOL_FILE = (
305 "MODULE OS CPU ID NAME\n"
306 "PUBLIC f10 0 func\n"
307 "STACK CFI INIT f10 22 .cfa: $rsp 8 + .ra: .cfa -8 + ^\n"
308 )
309 self.rc.SetDefaultCmdResult(stdout=MINIMAL_SYMBOL_FILE)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800310 self.rc.AddCmdResult(
311 ["/usr/bin/file", self.elf_file], stdout=self.FILE_OUT
312 )
Alex Klein1699fab2022-09-08 08:46:06 -0600313 self.assertCommandContains = self.rc.assertCommandContains
314 self.sym_file = os.path.join(self.breakpad_dir, "NAME/ID/NAME.sym")
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400315
Alex Klein1699fab2022-09-08 08:46:06 -0600316 self.StartPatcher(FindDebugDirMock(self.debug_dir))
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400317
Alex Klein1699fab2022-09-08 08:46:06 -0600318 def assertCommandArgs(self, i, args):
319 """Helper for looking at the args of the |i|th call"""
320 self.assertEqual(self.rc.call_args_list[i][0][0], args)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400321
Alex Klein1699fab2022-09-08 08:46:06 -0600322 def testNormal(self):
323 """Normal run -- given an ELF and a debug file"""
324 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800325 self.elf_file,
326 self.debug_file,
327 self.breakpad_dir,
Alex Klein1699fab2022-09-08 08:46:06 -0600328 )
329 self.assertEqual(ret, self.sym_file)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800330 self.assertEqual(self.rc.call_count, 2)
Alex Klein1699fab2022-09-08 08:46:06 -0600331 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000332 1, ["dump_syms", "-v", self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600333 )
334 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400335
Alex Klein1699fab2022-09-08 08:46:06 -0600336 def testNormalNoCfi(self):
337 """Normal run w/out CFI"""
338 # Make sure the num_errors flag works too.
Trent Apted1e2e4f32023-05-05 03:50:20 +0000339 num_errors = ctypes.c_int(0)
Alex Klein1699fab2022-09-08 08:46:06 -0600340 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
341 self.elf_file,
342 breakpad_dir=self.breakpad_dir,
343 strip_cfi=True,
344 num_errors=num_errors,
345 )
346 self.assertEqual(ret, self.sym_file)
347 self.assertEqual(num_errors.value, 0)
Manoj Gupta68546b02023-05-01 21:55:19 +0000348 self.assertCommandArgs(1, ["dump_syms", "-v", "-c", self.elf_file])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800349 self.assertEqual(self.rc.call_count, 2)
Alex Klein1699fab2022-09-08 08:46:06 -0600350 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400351
Alex Klein1699fab2022-09-08 08:46:06 -0600352 def testNormalElfOnly(self):
353 """Normal run -- given just an ELF"""
354 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
355 self.elf_file, breakpad_dir=self.breakpad_dir
356 )
357 self.assertEqual(ret, self.sym_file)
Manoj Gupta68546b02023-05-01 21:55:19 +0000358 self.assertCommandArgs(1, ["dump_syms", "-v", self.elf_file])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800359 self.assertEqual(self.rc.call_count, 2)
Alex Klein1699fab2022-09-08 08:46:06 -0600360 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400361
Alex Klein1699fab2022-09-08 08:46:06 -0600362 def testNormalSudo(self):
363 """Normal run where ELF is readable only by root"""
364 with mock.patch.object(os, "access") as mock_access:
365 mock_access.return_value = False
366 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
367 self.elf_file, breakpad_dir=self.breakpad_dir
368 )
369 self.assertEqual(ret, self.sym_file)
370 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000371 1, ["sudo", "--", "dump_syms", "-v", self.elf_file]
Alex Klein1699fab2022-09-08 08:46:06 -0600372 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400373
Alex Klein1699fab2022-09-08 08:46:06 -0600374 def testLargeDebugFail(self):
375 """Running w/large .debug failed, but retry worked"""
376 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000377 ["dump_syms", "-v", self.elf_file, self.debug_dir], returncode=1
Alex Klein1699fab2022-09-08 08:46:06 -0600378 )
379 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
380 self.elf_file, self.debug_file, self.breakpad_dir
381 )
382 self.assertEqual(ret, self.sym_file)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800383 self.assertEqual(self.rc.call_count, 4)
Alex Klein1699fab2022-09-08 08:46:06 -0600384 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000385 1, ["dump_syms", "-v", self.elf_file, self.debug_dir]
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800386 )
387 # The current fallback from _DumpExpectingSymbols() to
388 # _DumpAllowingBasicFallback() causes the first dump_sums command to get
389 # repeated.
390 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000391 2, ["dump_syms", "-v", self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600392 )
393 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000394 3, ["dump_syms", "-v", "-c", "-r", self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600395 )
396 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400397
Alex Klein1699fab2022-09-08 08:46:06 -0600398 def testDebugFail(self):
399 """Running w/.debug always failed, but works w/out"""
400 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000401 ["dump_syms", "-v", self.elf_file, self.debug_dir], returncode=1
Alex Klein1699fab2022-09-08 08:46:06 -0600402 )
403 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000404 ["dump_syms", "-v", "-c", "-r", self.elf_file, self.debug_dir],
Alex Klein1699fab2022-09-08 08:46:06 -0600405 returncode=1,
406 )
407 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
408 self.elf_file, self.debug_file, self.breakpad_dir
409 )
410 self.assertEqual(ret, self.sym_file)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800411 self.assertEqual(self.rc.call_count, 5)
Alex Klein1699fab2022-09-08 08:46:06 -0600412 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000413 1, ["dump_syms", "-v", self.elf_file, self.debug_dir]
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800414 )
415 # The current fallback from _DumpExpectingSymbols() to
416 # _DumpAllowingBasicFallback() causes the first dump_sums command to get
417 # repeated.
418 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000419 2, ["dump_syms", "-v", self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600420 )
421 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000422 3, ["dump_syms", "-v", "-c", "-r", self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600423 )
Manoj Gupta68546b02023-05-01 21:55:19 +0000424 self.assertCommandArgs(4, ["dump_syms", "-v", self.elf_file])
Alex Klein1699fab2022-09-08 08:46:06 -0600425 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400426
Alex Klein1699fab2022-09-08 08:46:06 -0600427 def testCompleteFail(self):
428 """Running dump_syms always fails"""
429 self.rc.SetDefaultCmdResult(returncode=1)
430 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
431 self.elf_file, breakpad_dir=self.breakpad_dir
432 )
433 self.assertEqual(ret, 1)
434 # Make sure the num_errors flag works too.
Trent Apted1e2e4f32023-05-05 03:50:20 +0000435 num_errors = ctypes.c_int(0)
Alex Klein1699fab2022-09-08 08:46:06 -0600436 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
437 self.elf_file, breakpad_dir=self.breakpad_dir, num_errors=num_errors
438 )
439 self.assertEqual(ret, 1)
440 self.assertEqual(num_errors.value, 1)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400441
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800442 def testKernelObjects(self):
443 """Kernel object files should call _DumpAllowingBasicFallback()"""
444 ko_file = os.path.join(self.tempdir, "elf.ko")
445 osutils.Touch(ko_file)
446 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000447 ["dump_syms", "-v", ko_file, self.debug_dir], returncode=1
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800448 )
449 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000450 ["dump_syms", "-v", "-c", "-r", ko_file, self.debug_dir],
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800451 returncode=1,
452 )
453 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
454 ko_file, self.debug_file, self.breakpad_dir
455 )
456 self.assertEqual(ret, self.sym_file)
457 self.assertEqual(self.rc.call_count, 3)
458 # Only one call (at the beginning of _DumpAllowingBasicFallback())
459 # to "dump_syms -v"
Manoj Gupta68546b02023-05-01 21:55:19 +0000460 self.assertCommandArgs(0, ["dump_syms", "-v", ko_file, self.debug_dir])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800461 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000462 1, ["dump_syms", "-v", "-c", "-r", ko_file, self.debug_dir]
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800463 )
Manoj Gupta68546b02023-05-01 21:55:19 +0000464 self.assertCommandArgs(2, ["dump_syms", "-v", ko_file])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800465 self.assertExists(self.sym_file)
466
467 def testGoBinary(self):
468 """Go binaries should call _DumpAllowingBasicFallback()
469
470 Also tests that dump_syms failing with 'file contains no debugging
471 information' does not fail the script.
472 """
473 go_binary = os.path.join(self.tempdir, "goprogram")
474 osutils.Touch(go_binary)
475 go_debug_file = os.path.join(self.debug_dir, "goprogram.debug")
476 osutils.Touch(go_debug_file, makedirs=True)
477 FILE_OUT_GO = go_binary + (
478 ": ELF 64-bit LSB executable, x86-64, "
479 "version 1 (SYSV), statically linked, "
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800480 "Go BuildID=KKXVlL66E8Qmngr4qll9/5kOKGZw9I7TmNhoqKLqq/SiYVJam6w5Fo"
481 "39B3BtDo/ba8_ceezZ-3R4qEv6_-K, not stripped"
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800482 )
483 self.rc.AddCmdResult(["/usr/bin/file", go_binary], stdout=FILE_OUT_GO)
484 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000485 ["dump_syms", "-v", go_binary, self.debug_dir], returncode=1
486 )
487 self.rc.AddCmdResult(
488 ["dump_syms", "-v", "-c", "-r", go_binary, self.debug_dir],
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800489 returncode=1,
490 )
491 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000492 ["dump_syms", "-v", go_binary],
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800493 returncode=1,
494 stderr=(
495 f"{go_binary}: file contains no debugging information "
496 '(no ".stab" or ".debug_info" sections)'
497 ),
498 )
Trent Apted1e2e4f32023-05-05 03:50:20 +0000499 num_errors = ctypes.c_int(0)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800500 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
501 go_binary, go_debug_file, self.breakpad_dir
502 )
503 self.assertEqual(ret, 0)
504 self.assertEqual(self.rc.call_count, 4)
505 self.assertCommandArgs(0, ["/usr/bin/file", go_binary])
506 # Only one call (at the beginning of _DumpAllowingBasicFallback())
507 # to "dump_syms -v"
508 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000509 1, ["dump_syms", "-v", go_binary, self.debug_dir]
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800510 )
511 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000512 2, ["dump_syms", "-v", "-c", "-r", go_binary, self.debug_dir]
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800513 )
Manoj Gupta68546b02023-05-01 21:55:19 +0000514 self.assertCommandArgs(3, ["dump_syms", "-v", go_binary])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800515 self.assertNotExists(self.sym_file)
516 self.assertEqual(num_errors.value, 0)
517
Ian Barkley-Yeunge1785762023-03-08 18:32:18 -0800518 def _testBinaryIsInLocalFallback(self, directory, filename):
519 binary = os.path.join(self.tempdir, directory, filename)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800520 osutils.Touch(binary, makedirs=True)
Ian Barkley-Yeunge1785762023-03-08 18:32:18 -0800521 debug_dir = os.path.join(self.debug_dir, directory)
522 debug_file = os.path.join(debug_dir, f"{filename}.debug")
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800523 osutils.Touch(debug_file, makedirs=True)
524 self.rc.AddCmdResult(["/usr/bin/file", binary], stdout=self.FILE_OUT)
525 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000526 ["dump_syms", "-v", binary, debug_dir], returncode=1
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800527 )
528 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000529 ["dump_syms", "-v", "-c", "-r", binary, debug_dir],
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800530 returncode=1,
531 )
532 self.rc.AddCmdResult(
Manoj Gupta68546b02023-05-01 21:55:19 +0000533 ["dump_syms", "-v", binary],
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800534 returncode=1,
535 stderr=(
536 f"{binary}: file contains no debugging information "
537 '(no ".stab" or ".debug_info" sections)'
538 ),
539 )
Trent Apted1e2e4f32023-05-05 03:50:20 +0000540 num_errors = ctypes.c_int(0)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800541 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
542 binary, debug_file, self.breakpad_dir, sysroot=self.tempdir
543 )
544 self.assertEqual(ret, 0)
545 self.assertEqual(self.rc.call_count, 4)
546 self.assertCommandArgs(0, ["/usr/bin/file", binary])
547 # Only one call (at the beginning of _DumpAllowingBasicFallback())
548 # to "dump_syms -v"
Manoj Gupta68546b02023-05-01 21:55:19 +0000549 self.assertCommandArgs(1, ["dump_syms", "-v", binary, debug_dir])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800550 self.assertCommandArgs(
Manoj Gupta68546b02023-05-01 21:55:19 +0000551 2, ["dump_syms", "-v", "-c", "-r", binary, debug_dir]
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800552 )
Manoj Gupta68546b02023-05-01 21:55:19 +0000553 self.assertCommandArgs(3, ["dump_syms", "-v", binary])
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800554 self.assertNotExists(self.sym_file)
555 self.assertEqual(num_errors.value, 0)
556
Ian Barkley-Yeunge1785762023-03-08 18:32:18 -0800557 def testAllowlist(self):
558 """Binaries in the allowlist should call _DumpAllowingBasicFallback()"""
559 self._testBinaryIsInLocalFallback("usr/bin", "goldctl")
560
561 def testUsrLocalSkip(self):
562 """Binaries in /usr/local should call _DumpAllowingBasicFallback()"""
563 self._testBinaryIsInLocalFallback("usr/local", "minidump_stackwalk")
564
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400565
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800566class ValidateSymbolFileTest(cros_test_lib.TempDirTestCase):
567 """Tests ValidateSymbolFile"""
568
569 def _GetTestdataFile(self, filename: str) -> str:
570 """Gets the path to a file in the testdata directory.
571
572 Args:
573 filename: The base filename of the file.
574
575 Returns:
576 A string with the complete path to the file.
577 """
578 return os.path.join(os.path.dirname(__file__), "testdata", filename)
579
580 def testValidSymbolFiles(self):
581 """Make sure ValidateSymbolFile passes on valid files"""
582
583 # All files are in the testdata/ subdirectory.
584 VALID_SYMBOL_FILES = [
585 # A "normal" symbol file from an executable.
586 "basic.sym",
587 # A "normal" symbol file from a shared library.
588 "basic_lib.sym",
589 # A symbol file with PUBLIC records but no FUNC records.
590 "public_only.sym",
591 # A symbol file with FUNC records but no PUBLIC records.
592 "func_only.sym",
593 # A symbol file with at least one of every line type.
594 "all_line_types.sym",
595 ]
596
597 for file in VALID_SYMBOL_FILES:
598 with self.subTest(
599 file=file
600 ), multiprocessing.Manager() as mp_manager:
601 found_files = mp_manager.list()
602 self.assertTrue(
603 cros_generate_breakpad_symbols.ValidateSymbolFile(
604 self._GetTestdataFile(file),
605 "/build/board/bin/foo",
606 "/build/board",
607 found_files,
608 )
609 )
610 self.assertFalse(found_files)
611
612 def testInvalidSymbolFiles(self):
613 """Make sure ValidateSymbolFile fails on invalid files.
614
615 This test only covers cases that return false, not cases that raise
616 exceptions.
617 """
618
619 class InvalidSymbolFile:
620 """The name of an invalid symbol file + the expected error msg."""
621
622 def __init__(self, filename, expected_errors):
623 self.filename = filename
624 self.expected_errors = expected_errors
625
626 INVALID_SYMBOL_FILES = [
627 InvalidSymbolFile(
628 "bad_no_func_or_public.sym",
629 [
630 "WARNING:root:/build/board/bin/foo: "
631 "Symbol file has no FUNC or PUBLIC records"
632 ],
633 ),
634 InvalidSymbolFile(
635 "bad_no_stack.sym",
636 [
637 "WARNING:root:/build/board/bin/foo: "
638 "Symbol file has no STACK records"
639 ],
640 ),
641 InvalidSymbolFile(
642 "bad_no_module.sym",
643 [
644 "WARNING:root:/build/board/bin/foo: "
645 "Symbol file has 0 MODULE lines"
646 ],
647 ),
648 InvalidSymbolFile(
649 "bad_two_modules.sym",
650 [
651 "WARNING:root:/build/board/bin/foo: "
652 "Symbol file has 2 MODULE lines"
653 ],
654 ),
655 InvalidSymbolFile(
656 "bad_func_no_line_numbers.sym",
657 [
658 "WARNING:root:/build/board/bin/foo: "
659 "Symbol file has FUNC records but no line numbers"
660 ],
661 ),
662 InvalidSymbolFile(
663 "bad_line_numbers_no_file.sym",
664 [
665 "WARNING:root:/build/board/bin/foo: "
666 "Symbol file has line number records but no FILE records"
667 ],
668 ),
669 InvalidSymbolFile(
670 "bad_inline_no_files.sym",
671 [
672 "WARNING:root:/build/board/bin/foo: "
673 "Symbol file has INLINE records but no FILE records"
674 ],
675 ),
676 InvalidSymbolFile(
677 "bad_inline_no_origins.sym",
678 [
679 "WARNING:root:/build/board/bin/foo: "
680 "Symbol file has INLINE records but no INLINE_ORIGIN "
681 "records"
682 ],
683 ),
684 InvalidSymbolFile(
685 "blank.sym",
686 [
687 "WARNING:root:/build/board/bin/foo: "
688 "Symbol file has no STACK records",
689 "WARNING:root:/build/board/bin/foo: "
690 "Symbol file has 0 MODULE lines",
691 "WARNING:root:/build/board/bin/foo: "
692 "Symbol file has no FUNC or PUBLIC records",
693 ],
694 ),
695 ]
696
697 for file in INVALID_SYMBOL_FILES:
698 with self.subTest(
699 file=file.filename
700 ), multiprocessing.Manager() as mp_manager:
701 found_files = mp_manager.list()
702 with self.assertLogs(level=logging.WARNING) as cm:
703 self.assertFalse(
704 cros_generate_breakpad_symbols.ValidateSymbolFile(
705 self._GetTestdataFile(file.filename),
706 "/build/board/bin/foo",
707 "/build/board",
708 found_files,
709 )
710 )
711 self.assertEqual(file.expected_errors, cm.output)
712 self.assertFalse(found_files)
713
714 def testInvalidSymbolFilesWhichRaise(self):
715 """Test ValidateSymbolFile raise exceptions on certain files"""
716
717 class InvalidSymbolFile:
718 """The invalid symbol file + the expected exception message"""
719
720 def __init__(self, filename, expected_exception_regex):
721 self.filename = filename
722 self.expected_exception_regex = expected_exception_regex
723
724 INVALID_SYMBOL_FILES = [
725 InvalidSymbolFile(
726 "bad_unknown_line_type.sym",
727 "symbol file has unknown line type UNKNOWN",
728 ),
729 InvalidSymbolFile(
730 "bad_blank_line.sym",
731 "symbol file has unexpected blank line",
732 ),
733 InvalidSymbolFile(
734 "bad_short_func.sym",
735 r"symbol file has FUNC line with 2 words "
736 r"\(expected 5 or more\)",
737 ),
738 InvalidSymbolFile(
739 "bad_short_line_number.sym",
740 r"symbol file has line number line with 3 words "
741 r"\(expected 4 - 4\)",
742 ),
743 InvalidSymbolFile(
744 "bad_long_line_number.sym",
745 r"symbol file has line number line with 5 words "
746 r"\(expected 4 - 4\)",
747 ),
748 ]
749
750 for file in INVALID_SYMBOL_FILES:
751 with self.subTest(
752 file=file.filename
753 ), multiprocessing.Manager() as mp_manager:
754 found_files = mp_manager.list()
755 self.assertRaisesRegex(
756 ValueError,
757 file.expected_exception_regex,
758 cros_generate_breakpad_symbols.ValidateSymbolFile,
759 self._GetTestdataFile(file.filename),
760 "/build/board/bin/foo",
761 "/build/board",
762 found_files,
763 )
764
765 def testAllowlist(self):
766 """Test that ELFs on the allowlist are allowed to pass."""
767 with multiprocessing.Manager() as mp_manager:
768 found_files = mp_manager.list()
769 self.assertTrue(
770 cros_generate_breakpad_symbols.ValidateSymbolFile(
771 self._GetTestdataFile("bad_no_stack.sym"),
772 "/build/board/opt/google/chrome/nacl_helper_bootstrap",
773 "/build/board",
774 found_files,
775 )
776 )
777 self.assertFalse(found_files)
778
779 def testAllowlistRegex(self):
780 """Test that ELFs on the regex-based allowlist are allowed to pass."""
781 with multiprocessing.Manager() as mp_manager:
782 found_files = mp_manager.list()
783 self.assertTrue(
784 cros_generate_breakpad_symbols.ValidateSymbolFile(
785 self._GetTestdataFile("bad_no_stack.sym"),
786 "/build/board/usr/lib/libcros_ml_core.so",
787 "/build/board",
788 found_files,
789 )
790 )
791 self.assertFalse(found_files)
792
793 def _CreateSymbolFile(
794 self,
795 sym_file: pathlib.Path,
796 func_lines: int = 0,
797 public_lines: int = 0,
798 stack_lines: int = 0,
799 line_number_lines: int = 0,
800 ) -> None:
801 """Creates a symbol file.
802
803 Creates a symbol file with the given number of lines (and enough other
804 lines to pass validation) in the temp directory.
805
806 To pass validation, chrome.sym files must be huge; create them
807 programmatically during the test instead of checking in a real 800MB+
808 chrome symbol file.
809 """
810 with sym_file.open(mode="w", encoding="utf-8") as f:
811 f.write("MODULE OS CPU ID NAME\n")
812 f.write("FILE 0 /path/to/source.cc\n")
813 for func in range(0, func_lines):
814 f.write(f"FUNC {func} 1 0 function{func}\n")
815 for public in range(0, public_lines):
816 f.write(f"PUBLIC {public} 0 Public{public}\n")
817 for line in range(0, line_number_lines):
818 f.write(f"{line} 1 {line} 0\n")
819 for stack in range(0, stack_lines):
820 f.write(f"STACK CFI {stack} .cfa: $esp {stack} +\n")
821
822 def testValidChromeSymbolFile(self):
823 """Test that a chrome symbol file can pass the additional checks"""
824 sym_file = self.tempdir / "chrome.sym"
825 self._CreateSymbolFile(
826 sym_file,
827 func_lines=100000,
828 public_lines=10,
829 stack_lines=1000000,
830 line_number_lines=1000000,
831 )
832 with multiprocessing.Manager() as mp_manager:
833 found_files = mp_manager.list()
834 self.assertTrue(
835 cros_generate_breakpad_symbols.ValidateSymbolFile(
836 str(sym_file),
837 "/build/board/opt/google/chrome/chrome",
838 "/build/board",
839 found_files,
840 )
841 )
842 self.assertEqual(
843 list(found_files),
844 [cros_generate_breakpad_symbols.ExpectedFiles.ASH_CHROME],
845 )
846
847 def testInvalidChromeSymbolFile(self):
848 """Test that a chrome symbol file is held to higher standards."""
849
850 class ChromeSymbolFileTest:
851 """Defines the subtest for an invalid Chrome symbol file."""
852
853 def __init__(
854 self,
855 name,
856 expected_error,
857 func_lines=100000,
858 stack_lines=1000000,
859 line_number_lines=1000000,
860 ):
861 self.name = name
862 self.expected_error = expected_error
863 self.func_lines = func_lines
864 self.stack_lines = stack_lines
865 self.line_number_lines = line_number_lines
866
867 CHROME_SYMBOL_TESTS = [
868 ChromeSymbolFileTest(
869 name="Insufficient FUNC records",
870 func_lines=10000,
871 expected_error="chrome should have at least 100,000 FUNC "
872 "records, found 10000",
873 ),
874 ChromeSymbolFileTest(
875 name="Insufficient STACK records",
876 stack_lines=100000,
877 expected_error="chrome should have at least 1,000,000 STACK "
878 "records, found 100000",
879 ),
880 ChromeSymbolFileTest(
881 name="Insufficient line number records",
882 line_number_lines=100000,
883 expected_error="chrome should have at least 1,000,000 "
884 "line number records, found 100000",
885 ),
886 ]
887 for test in CHROME_SYMBOL_TESTS:
888 with self.subTest(
889 name=test.name
890 ), multiprocessing.Manager() as mp_manager:
891 sym_file = self.tempdir / "chrome.sym"
892 self._CreateSymbolFile(
893 sym_file,
894 func_lines=test.func_lines,
895 public_lines=10,
896 stack_lines=test.stack_lines,
897 line_number_lines=test.line_number_lines,
898 )
899 found_files = mp_manager.list()
900 with self.assertLogs(level=logging.WARNING) as cm:
901 self.assertFalse(
902 cros_generate_breakpad_symbols.ValidateSymbolFile(
903 str(sym_file),
904 "/build/board/opt/google/chrome/chrome",
905 "/build/board",
906 found_files,
907 )
908 )
909 self.assertIn(test.expected_error, cm.output[0])
910 self.assertEqual(len(cm.output), 1)
911
912 def testValidLibcSymbolFile(self):
913 """Test that a libc.so symbol file can pass the additional checks."""
914 with multiprocessing.Manager() as mp_manager:
915 sym_file = self.tempdir / "libc.so.sym"
916 self._CreateSymbolFile(
917 sym_file, public_lines=200, stack_lines=20000
918 )
919 found_files = mp_manager.list()
920 self.assertTrue(
921 cros_generate_breakpad_symbols.ValidateSymbolFile(
922 str(sym_file),
923 "/build/board/lib64/libc.so.6",
924 "/build/board",
925 found_files,
926 )
927 )
928 self.assertEqual(
929 list(found_files),
930 [cros_generate_breakpad_symbols.ExpectedFiles.LIBC],
931 )
932
933 def testInvalidLibcSymbolFile(self):
934 """Test that a libc.so symbol file is held to higher standards."""
935
936 class LibcSymbolFileTest:
937 """Defines the subtest for an invalid libc symbol file."""
938
939 def __init__(
940 self,
941 name,
942 expected_error,
943 public_lines=200,
944 stack_lines=20000,
945 ):
946 self.name = name
947 self.expected_error = expected_error
948 self.public_lines = public_lines
949 self.stack_lines = stack_lines
950
951 LIBC_SYMBOL_TESTS = [
952 LibcSymbolFileTest(
953 name="Insufficient PUBLIC records",
954 public_lines=50,
955 expected_error="/build/board/lib64/libc.so.6 should have at "
956 "least 100 PUBLIC records, found 50",
957 ),
958 LibcSymbolFileTest(
959 name="Insufficient STACK records",
960 stack_lines=1000,
961 expected_error="/build/board/lib64/libc.so.6 should have at "
962 "least 10000 STACK records, found 1000",
963 ),
964 ]
965 for test in LIBC_SYMBOL_TESTS:
966 with self.subTest(
967 name=test.name
968 ), multiprocessing.Manager() as mp_manager:
969 sym_file = self.tempdir / "libc.so.sym"
970 self._CreateSymbolFile(
971 sym_file,
972 public_lines=test.public_lines,
973 stack_lines=test.stack_lines,
974 )
975 found_files = mp_manager.list()
976 with self.assertLogs(level=logging.WARNING) as cm:
977 self.assertFalse(
978 cros_generate_breakpad_symbols.ValidateSymbolFile(
979 str(sym_file),
980 "/build/board/lib64/libc.so.6",
981 "/build/board",
982 found_files,
983 )
984 )
985 self.assertIn(test.expected_error, cm.output[0])
986 self.assertEqual(len(cm.output), 1)
987
988 def testValidCrashReporterSymbolFile(self):
989 """Test a crash_reporter symbol file can pass the additional checks."""
990 with multiprocessing.Manager() as mp_manager:
991 sym_file = self.tempdir / "crash_reporter.sym"
992 self._CreateSymbolFile(
993 sym_file,
994 func_lines=2000,
995 public_lines=10,
996 stack_lines=2000,
997 line_number_lines=20000,
998 )
999 found_files = mp_manager.list()
1000 self.assertTrue(
1001 cros_generate_breakpad_symbols.ValidateSymbolFile(
1002 str(sym_file),
1003 "/build/board/sbin/crash_reporter",
1004 "/build/board",
1005 found_files,
1006 )
1007 )
1008 self.assertEqual(
1009 list(found_files),
1010 [cros_generate_breakpad_symbols.ExpectedFiles.CRASH_REPORTER],
1011 )
1012
1013 def testInvalidCrashReporterSymbolFile(self):
1014 """Test that a crash_reporter symbol file is held to higher standards"""
1015
1016 class CrashReporterSymbolFileTest:
1017 """Defines the subtest for an invalid crash_reporter symbol file."""
1018
1019 def __init__(
1020 self,
1021 name,
1022 expected_error,
1023 func_lines=2000,
1024 stack_lines=2000,
1025 line_number_lines=20000,
1026 ):
1027 self.name = name
1028 self.expected_error = expected_error
1029 self.func_lines = func_lines
1030 self.stack_lines = stack_lines
1031 self.line_number_lines = line_number_lines
1032
1033 CRASH_REPORTER_SYMBOL_TESTS = [
1034 CrashReporterSymbolFileTest(
1035 name="Insufficient FUNC records",
1036 func_lines=500,
1037 expected_error="crash_reporter should have at least 1000 FUNC "
1038 "records, found 500",
1039 ),
1040 CrashReporterSymbolFileTest(
1041 name="Insufficient STACK records",
1042 stack_lines=100,
1043 expected_error="crash_reporter should have at least 1000 STACK "
1044 "records, found 100",
1045 ),
1046 CrashReporterSymbolFileTest(
1047 name="Insufficient line number records",
1048 line_number_lines=2000,
1049 expected_error="crash_reporter should have at least 10,000 "
1050 "line number records, found 2000",
1051 ),
1052 ]
1053 for test in CRASH_REPORTER_SYMBOL_TESTS:
1054 with self.subTest(
1055 name=test.name
1056 ), multiprocessing.Manager() as mp_manager:
1057 sym_file = self.tempdir / "crash_reporter.sym"
1058 self._CreateSymbolFile(
1059 sym_file,
1060 func_lines=test.func_lines,
1061 stack_lines=test.stack_lines,
1062 line_number_lines=test.line_number_lines,
1063 )
1064 found_files = mp_manager.list()
1065 with self.assertLogs(level=logging.WARNING) as cm:
1066 self.assertFalse(
1067 cros_generate_breakpad_symbols.ValidateSymbolFile(
1068 str(sym_file),
1069 "/build/board/sbin/crash_reporter",
1070 "/build/board",
1071 found_files,
1072 )
1073 )
1074 self.assertIn(test.expected_error, cm.output[0])
1075 self.assertEqual(len(cm.output), 1)
1076
1077 def testValidLibMetricsSymbolFile(self):
1078 """Test a libmetrics.so symbol file can pass the additional checks."""
1079 with multiprocessing.Manager() as mp_manager:
1080 sym_file = self.tempdir / "libmetrics.so.sym"
1081 self._CreateSymbolFile(
1082 sym_file,
1083 func_lines=200,
1084 public_lines=2,
1085 stack_lines=2000,
1086 line_number_lines=10000,
1087 )
1088 found_files = mp_manager.list()
1089 self.assertTrue(
1090 cros_generate_breakpad_symbols.ValidateSymbolFile(
1091 str(sym_file),
1092 "/build/board/usr/lib64/libmetrics.so",
1093 "/build/board",
1094 found_files,
1095 )
1096 )
1097 self.assertEqual(
1098 list(found_files),
1099 [cros_generate_breakpad_symbols.ExpectedFiles.LIBMETRICS],
1100 )
1101
1102 def testInvalidLibMetricsSymbolFile(self):
1103 """Test that a libmetrics.so symbol file is held to higher standards."""
1104
1105 class LibMetricsSymbolFileTest:
1106 """Defines the subtest for an invalid libmetrics.so symbol file."""
1107
1108 def __init__(
1109 self,
1110 name,
1111 expected_error,
1112 func_lines=200,
1113 public_lines=2,
1114 stack_lines=2000,
1115 line_number_lines=10000,
1116 ):
1117 self.name = name
1118 self.expected_error = expected_error
1119 self.func_lines = func_lines
1120 self.public_lines = public_lines
1121 self.stack_lines = stack_lines
1122 self.line_number_lines = line_number_lines
1123
1124 LIBMETRICS_SYMBOL_TESTS = [
1125 LibMetricsSymbolFileTest(
1126 name="Insufficient FUNC records",
1127 func_lines=10,
1128 expected_error="libmetrics should have at least 100 FUNC "
1129 "records, found 10",
1130 ),
1131 LibMetricsSymbolFileTest(
1132 name="Insufficient PUBLIC records",
1133 public_lines=0,
1134 expected_error="libmetrics should have at least 1 PUBLIC "
1135 "record, found 0",
1136 ),
1137 LibMetricsSymbolFileTest(
1138 name="Insufficient STACK records",
1139 stack_lines=500,
1140 expected_error="libmetrics should have at least 1000 STACK "
1141 "records, found 500",
1142 ),
1143 LibMetricsSymbolFileTest(
1144 name="Insufficient line number records",
1145 line_number_lines=2000,
1146 expected_error="libmetrics should have at least 5000 "
1147 "line number records, found 2000",
1148 ),
1149 ]
1150 for test in LIBMETRICS_SYMBOL_TESTS:
1151 with self.subTest(
1152 name=test.name
1153 ), multiprocessing.Manager() as mp_manager:
1154 sym_file = self.tempdir / "libmetrics.so.sym"
1155 self._CreateSymbolFile(
1156 sym_file,
1157 func_lines=test.func_lines,
1158 public_lines=test.public_lines,
1159 stack_lines=test.stack_lines,
1160 line_number_lines=test.line_number_lines,
1161 )
1162 found_files = mp_manager.list()
1163 with self.assertLogs(level=logging.WARNING) as cm:
1164 self.assertFalse(
1165 cros_generate_breakpad_symbols.ValidateSymbolFile(
1166 str(sym_file),
1167 "/build/board/usr/lib64/libmetrics.so",
1168 "/build/board",
1169 found_files,
1170 )
1171 )
1172 self.assertIn(test.expected_error, cm.output[0])
1173 self.assertEqual(len(cm.output), 1)
1174
1175
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001176class UtilsTestDir(cros_test_lib.TempDirTestCase):
Alex Klein1699fab2022-09-08 08:46:06 -06001177 """Tests ReadSymsHeader."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001178
Alex Klein1699fab2022-09-08 08:46:06 -06001179 def testReadSymsHeaderGoodFile(self):
1180 """Make sure ReadSymsHeader can parse sym files"""
1181 sym_file = os.path.join(self.tempdir, "sym")
1182 osutils.WriteFile(sym_file, "MODULE Linux x86 s0m31D chrooome")
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001183 result = cros_generate_breakpad_symbols.ReadSymsHeader(
1184 sym_file, "unused_elfname"
1185 )
Alex Klein1699fab2022-09-08 08:46:06 -06001186 self.assertEqual(result.cpu, "x86")
1187 self.assertEqual(result.id, "s0m31D")
1188 self.assertEqual(result.name, "chrooome")
1189 self.assertEqual(result.os, "Linux")
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001190
1191
1192class UtilsTest(cros_test_lib.TestCase):
Alex Klein1699fab2022-09-08 08:46:06 -06001193 """Tests ReadSymsHeader."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001194
Alex Klein1699fab2022-09-08 08:46:06 -06001195 def testReadSymsHeaderGoodBuffer(self):
1196 """Make sure ReadSymsHeader can parse sym file handles"""
1197 result = cros_generate_breakpad_symbols.ReadSymsHeader(
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001198 io.BytesIO(b"MODULE Linux arm MY-ID-HERE blkid"), "unused_elfname"
Alex Klein1699fab2022-09-08 08:46:06 -06001199 )
1200 self.assertEqual(result.cpu, "arm")
1201 self.assertEqual(result.id, "MY-ID-HERE")
1202 self.assertEqual(result.name, "blkid")
1203 self.assertEqual(result.os, "Linux")
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001204
Alex Klein1699fab2022-09-08 08:46:06 -06001205 def testReadSymsHeaderBadd(self):
1206 """Make sure ReadSymsHeader throws on bad sym files"""
1207 self.assertRaises(
1208 ValueError,
1209 cros_generate_breakpad_symbols.ReadSymsHeader,
1210 io.BytesIO(b"asdf"),
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001211 "unused_elfname",
Alex Klein1699fab2022-09-08 08:46:06 -06001212 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001213
Alex Klein1699fab2022-09-08 08:46:06 -06001214 def testBreakpadDir(self):
1215 """Make sure board->breakpad path expansion works"""
1216 expected = "/build/blah/usr/lib/debug/breakpad"
1217 result = cros_generate_breakpad_symbols.FindBreakpadDir("blah")
1218 self.assertEqual(expected, result)
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001219
Alex Klein1699fab2022-09-08 08:46:06 -06001220 def testDebugDir(self):
1221 """Make sure board->debug path expansion works"""
1222 expected = "/build/blah/usr/lib/debug"
1223 result = cros_generate_breakpad_symbols.FindDebugDir("blah")
1224 self.assertEqual(expected, result)