blob: 9c7e738e5a7d063de4461a27f344e2e4a69cc109 [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
Ian Barkley-Yeung4ed8cee2023-05-05 11:20:52 -0700565 def testOptGoogleCrosContainersLibSkip(self):
566 """Binaries in /opt/google/cros-containers/lib as well.
567
568 Binaries in /opt/google/cros-containers/lib should call
569 _DumpAllowingBasicFallback()
570 """
571 self._testBinaryIsInLocalFallback(
572 "opt/google/cros-containers/lib", "libc.so.6"
573 )
574
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400575
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800576class ValidateSymbolFileTest(cros_test_lib.TempDirTestCase):
577 """Tests ValidateSymbolFile"""
578
579 def _GetTestdataFile(self, filename: str) -> str:
580 """Gets the path to a file in the testdata directory.
581
582 Args:
583 filename: The base filename of the file.
584
585 Returns:
586 A string with the complete path to the file.
587 """
588 return os.path.join(os.path.dirname(__file__), "testdata", filename)
589
590 def testValidSymbolFiles(self):
591 """Make sure ValidateSymbolFile passes on valid files"""
592
593 # All files are in the testdata/ subdirectory.
594 VALID_SYMBOL_FILES = [
595 # A "normal" symbol file from an executable.
596 "basic.sym",
597 # A "normal" symbol file from a shared library.
598 "basic_lib.sym",
599 # A symbol file with PUBLIC records but no FUNC records.
600 "public_only.sym",
601 # A symbol file with FUNC records but no PUBLIC records.
602 "func_only.sym",
603 # A symbol file with at least one of every line type.
604 "all_line_types.sym",
605 ]
606
607 for file in VALID_SYMBOL_FILES:
608 with self.subTest(
609 file=file
610 ), multiprocessing.Manager() as mp_manager:
611 found_files = mp_manager.list()
612 self.assertTrue(
613 cros_generate_breakpad_symbols.ValidateSymbolFile(
614 self._GetTestdataFile(file),
615 "/build/board/bin/foo",
616 "/build/board",
617 found_files,
618 )
619 )
620 self.assertFalse(found_files)
621
622 def testInvalidSymbolFiles(self):
623 """Make sure ValidateSymbolFile fails on invalid files.
624
625 This test only covers cases that return false, not cases that raise
626 exceptions.
627 """
628
629 class InvalidSymbolFile:
630 """The name of an invalid symbol file + the expected error msg."""
631
632 def __init__(self, filename, expected_errors):
633 self.filename = filename
634 self.expected_errors = expected_errors
635
636 INVALID_SYMBOL_FILES = [
637 InvalidSymbolFile(
638 "bad_no_func_or_public.sym",
639 [
640 "WARNING:root:/build/board/bin/foo: "
641 "Symbol file has no FUNC or PUBLIC records"
642 ],
643 ),
644 InvalidSymbolFile(
645 "bad_no_stack.sym",
646 [
647 "WARNING:root:/build/board/bin/foo: "
648 "Symbol file has no STACK records"
649 ],
650 ),
651 InvalidSymbolFile(
652 "bad_no_module.sym",
653 [
654 "WARNING:root:/build/board/bin/foo: "
655 "Symbol file has 0 MODULE lines"
656 ],
657 ),
658 InvalidSymbolFile(
659 "bad_two_modules.sym",
660 [
661 "WARNING:root:/build/board/bin/foo: "
662 "Symbol file has 2 MODULE lines"
663 ],
664 ),
665 InvalidSymbolFile(
666 "bad_func_no_line_numbers.sym",
667 [
668 "WARNING:root:/build/board/bin/foo: "
669 "Symbol file has FUNC records but no line numbers"
670 ],
671 ),
672 InvalidSymbolFile(
673 "bad_line_numbers_no_file.sym",
674 [
675 "WARNING:root:/build/board/bin/foo: "
676 "Symbol file has line number records but no FILE records"
677 ],
678 ),
679 InvalidSymbolFile(
680 "bad_inline_no_files.sym",
681 [
682 "WARNING:root:/build/board/bin/foo: "
683 "Symbol file has INLINE records but no FILE records"
684 ],
685 ),
686 InvalidSymbolFile(
687 "bad_inline_no_origins.sym",
688 [
689 "WARNING:root:/build/board/bin/foo: "
690 "Symbol file has INLINE records but no INLINE_ORIGIN "
691 "records"
692 ],
693 ),
694 InvalidSymbolFile(
695 "blank.sym",
696 [
697 "WARNING:root:/build/board/bin/foo: "
698 "Symbol file has no STACK records",
699 "WARNING:root:/build/board/bin/foo: "
700 "Symbol file has 0 MODULE lines",
701 "WARNING:root:/build/board/bin/foo: "
702 "Symbol file has no FUNC or PUBLIC records",
703 ],
704 ),
705 ]
706
707 for file in INVALID_SYMBOL_FILES:
708 with self.subTest(
709 file=file.filename
710 ), multiprocessing.Manager() as mp_manager:
711 found_files = mp_manager.list()
712 with self.assertLogs(level=logging.WARNING) as cm:
713 self.assertFalse(
714 cros_generate_breakpad_symbols.ValidateSymbolFile(
715 self._GetTestdataFile(file.filename),
716 "/build/board/bin/foo",
717 "/build/board",
718 found_files,
719 )
720 )
721 self.assertEqual(file.expected_errors, cm.output)
722 self.assertFalse(found_files)
723
724 def testInvalidSymbolFilesWhichRaise(self):
725 """Test ValidateSymbolFile raise exceptions on certain files"""
726
727 class InvalidSymbolFile:
728 """The invalid symbol file + the expected exception message"""
729
730 def __init__(self, filename, expected_exception_regex):
731 self.filename = filename
732 self.expected_exception_regex = expected_exception_regex
733
734 INVALID_SYMBOL_FILES = [
735 InvalidSymbolFile(
736 "bad_unknown_line_type.sym",
737 "symbol file has unknown line type UNKNOWN",
738 ),
739 InvalidSymbolFile(
740 "bad_blank_line.sym",
741 "symbol file has unexpected blank line",
742 ),
743 InvalidSymbolFile(
744 "bad_short_func.sym",
745 r"symbol file has FUNC line with 2 words "
746 r"\(expected 5 or more\)",
747 ),
748 InvalidSymbolFile(
749 "bad_short_line_number.sym",
750 r"symbol file has line number line with 3 words "
751 r"\(expected 4 - 4\)",
752 ),
753 InvalidSymbolFile(
754 "bad_long_line_number.sym",
755 r"symbol file has line number line with 5 words "
756 r"\(expected 4 - 4\)",
757 ),
758 ]
759
760 for file in INVALID_SYMBOL_FILES:
761 with self.subTest(
762 file=file.filename
763 ), multiprocessing.Manager() as mp_manager:
764 found_files = mp_manager.list()
765 self.assertRaisesRegex(
766 ValueError,
767 file.expected_exception_regex,
768 cros_generate_breakpad_symbols.ValidateSymbolFile,
769 self._GetTestdataFile(file.filename),
770 "/build/board/bin/foo",
771 "/build/board",
772 found_files,
773 )
774
775 def testAllowlist(self):
776 """Test that ELFs on the allowlist are allowed to pass."""
777 with multiprocessing.Manager() as mp_manager:
778 found_files = mp_manager.list()
779 self.assertTrue(
780 cros_generate_breakpad_symbols.ValidateSymbolFile(
781 self._GetTestdataFile("bad_no_stack.sym"),
782 "/build/board/opt/google/chrome/nacl_helper_bootstrap",
783 "/build/board",
784 found_files,
785 )
786 )
787 self.assertFalse(found_files)
788
789 def testAllowlistRegex(self):
790 """Test that ELFs on the regex-based allowlist are allowed to pass."""
791 with multiprocessing.Manager() as mp_manager:
792 found_files = mp_manager.list()
793 self.assertTrue(
794 cros_generate_breakpad_symbols.ValidateSymbolFile(
795 self._GetTestdataFile("bad_no_stack.sym"),
796 "/build/board/usr/lib/libcros_ml_core.so",
797 "/build/board",
798 found_files,
799 )
800 )
801 self.assertFalse(found_files)
802
803 def _CreateSymbolFile(
804 self,
805 sym_file: pathlib.Path,
806 func_lines: int = 0,
807 public_lines: int = 0,
808 stack_lines: int = 0,
809 line_number_lines: int = 0,
810 ) -> None:
811 """Creates a symbol file.
812
813 Creates a symbol file with the given number of lines (and enough other
814 lines to pass validation) in the temp directory.
815
816 To pass validation, chrome.sym files must be huge; create them
817 programmatically during the test instead of checking in a real 800MB+
818 chrome symbol file.
819 """
820 with sym_file.open(mode="w", encoding="utf-8") as f:
821 f.write("MODULE OS CPU ID NAME\n")
822 f.write("FILE 0 /path/to/source.cc\n")
823 for func in range(0, func_lines):
824 f.write(f"FUNC {func} 1 0 function{func}\n")
825 for public in range(0, public_lines):
826 f.write(f"PUBLIC {public} 0 Public{public}\n")
827 for line in range(0, line_number_lines):
828 f.write(f"{line} 1 {line} 0\n")
829 for stack in range(0, stack_lines):
830 f.write(f"STACK CFI {stack} .cfa: $esp {stack} +\n")
831
832 def testValidChromeSymbolFile(self):
833 """Test that a chrome symbol file can pass the additional checks"""
834 sym_file = self.tempdir / "chrome.sym"
835 self._CreateSymbolFile(
836 sym_file,
837 func_lines=100000,
838 public_lines=10,
839 stack_lines=1000000,
840 line_number_lines=1000000,
841 )
842 with multiprocessing.Manager() as mp_manager:
843 found_files = mp_manager.list()
844 self.assertTrue(
845 cros_generate_breakpad_symbols.ValidateSymbolFile(
846 str(sym_file),
847 "/build/board/opt/google/chrome/chrome",
848 "/build/board",
849 found_files,
850 )
851 )
852 self.assertEqual(
853 list(found_files),
854 [cros_generate_breakpad_symbols.ExpectedFiles.ASH_CHROME],
855 )
856
857 def testInvalidChromeSymbolFile(self):
858 """Test that a chrome symbol file is held to higher standards."""
859
860 class ChromeSymbolFileTest:
861 """Defines the subtest for an invalid Chrome symbol file."""
862
863 def __init__(
864 self,
865 name,
866 expected_error,
867 func_lines=100000,
868 stack_lines=1000000,
869 line_number_lines=1000000,
870 ):
871 self.name = name
872 self.expected_error = expected_error
873 self.func_lines = func_lines
874 self.stack_lines = stack_lines
875 self.line_number_lines = line_number_lines
876
877 CHROME_SYMBOL_TESTS = [
878 ChromeSymbolFileTest(
879 name="Insufficient FUNC records",
880 func_lines=10000,
881 expected_error="chrome should have at least 100,000 FUNC "
882 "records, found 10000",
883 ),
884 ChromeSymbolFileTest(
885 name="Insufficient STACK records",
886 stack_lines=100000,
887 expected_error="chrome should have at least 1,000,000 STACK "
888 "records, found 100000",
889 ),
890 ChromeSymbolFileTest(
891 name="Insufficient line number records",
892 line_number_lines=100000,
893 expected_error="chrome should have at least 1,000,000 "
894 "line number records, found 100000",
895 ),
896 ]
897 for test in CHROME_SYMBOL_TESTS:
898 with self.subTest(
899 name=test.name
900 ), multiprocessing.Manager() as mp_manager:
901 sym_file = self.tempdir / "chrome.sym"
902 self._CreateSymbolFile(
903 sym_file,
904 func_lines=test.func_lines,
905 public_lines=10,
906 stack_lines=test.stack_lines,
907 line_number_lines=test.line_number_lines,
908 )
909 found_files = mp_manager.list()
910 with self.assertLogs(level=logging.WARNING) as cm:
911 self.assertFalse(
912 cros_generate_breakpad_symbols.ValidateSymbolFile(
913 str(sym_file),
914 "/build/board/opt/google/chrome/chrome",
915 "/build/board",
916 found_files,
917 )
918 )
919 self.assertIn(test.expected_error, cm.output[0])
920 self.assertEqual(len(cm.output), 1)
921
922 def testValidLibcSymbolFile(self):
923 """Test that a libc.so symbol file can pass the additional checks."""
924 with multiprocessing.Manager() as mp_manager:
925 sym_file = self.tempdir / "libc.so.sym"
926 self._CreateSymbolFile(
927 sym_file, public_lines=200, stack_lines=20000
928 )
929 found_files = mp_manager.list()
930 self.assertTrue(
931 cros_generate_breakpad_symbols.ValidateSymbolFile(
932 str(sym_file),
933 "/build/board/lib64/libc.so.6",
934 "/build/board",
935 found_files,
936 )
937 )
938 self.assertEqual(
939 list(found_files),
940 [cros_generate_breakpad_symbols.ExpectedFiles.LIBC],
941 )
942
943 def testInvalidLibcSymbolFile(self):
944 """Test that a libc.so symbol file is held to higher standards."""
945
946 class LibcSymbolFileTest:
947 """Defines the subtest for an invalid libc symbol file."""
948
949 def __init__(
950 self,
951 name,
952 expected_error,
953 public_lines=200,
954 stack_lines=20000,
955 ):
956 self.name = name
957 self.expected_error = expected_error
958 self.public_lines = public_lines
959 self.stack_lines = stack_lines
960
961 LIBC_SYMBOL_TESTS = [
962 LibcSymbolFileTest(
963 name="Insufficient PUBLIC records",
964 public_lines=50,
965 expected_error="/build/board/lib64/libc.so.6 should have at "
966 "least 100 PUBLIC records, found 50",
967 ),
968 LibcSymbolFileTest(
969 name="Insufficient STACK records",
970 stack_lines=1000,
971 expected_error="/build/board/lib64/libc.so.6 should have at "
972 "least 10000 STACK records, found 1000",
973 ),
974 ]
975 for test in LIBC_SYMBOL_TESTS:
976 with self.subTest(
977 name=test.name
978 ), multiprocessing.Manager() as mp_manager:
979 sym_file = self.tempdir / "libc.so.sym"
980 self._CreateSymbolFile(
981 sym_file,
982 public_lines=test.public_lines,
983 stack_lines=test.stack_lines,
984 )
985 found_files = mp_manager.list()
986 with self.assertLogs(level=logging.WARNING) as cm:
987 self.assertFalse(
988 cros_generate_breakpad_symbols.ValidateSymbolFile(
989 str(sym_file),
990 "/build/board/lib64/libc.so.6",
991 "/build/board",
992 found_files,
993 )
994 )
995 self.assertIn(test.expected_error, cm.output[0])
996 self.assertEqual(len(cm.output), 1)
997
998 def testValidCrashReporterSymbolFile(self):
999 """Test a crash_reporter symbol file can pass the additional checks."""
1000 with multiprocessing.Manager() as mp_manager:
1001 sym_file = self.tempdir / "crash_reporter.sym"
1002 self._CreateSymbolFile(
1003 sym_file,
1004 func_lines=2000,
1005 public_lines=10,
1006 stack_lines=2000,
1007 line_number_lines=20000,
1008 )
1009 found_files = mp_manager.list()
1010 self.assertTrue(
1011 cros_generate_breakpad_symbols.ValidateSymbolFile(
1012 str(sym_file),
1013 "/build/board/sbin/crash_reporter",
1014 "/build/board",
1015 found_files,
1016 )
1017 )
1018 self.assertEqual(
1019 list(found_files),
1020 [cros_generate_breakpad_symbols.ExpectedFiles.CRASH_REPORTER],
1021 )
1022
1023 def testInvalidCrashReporterSymbolFile(self):
1024 """Test that a crash_reporter symbol file is held to higher standards"""
1025
1026 class CrashReporterSymbolFileTest:
1027 """Defines the subtest for an invalid crash_reporter symbol file."""
1028
1029 def __init__(
1030 self,
1031 name,
1032 expected_error,
1033 func_lines=2000,
1034 stack_lines=2000,
1035 line_number_lines=20000,
1036 ):
1037 self.name = name
1038 self.expected_error = expected_error
1039 self.func_lines = func_lines
1040 self.stack_lines = stack_lines
1041 self.line_number_lines = line_number_lines
1042
1043 CRASH_REPORTER_SYMBOL_TESTS = [
1044 CrashReporterSymbolFileTest(
1045 name="Insufficient FUNC records",
1046 func_lines=500,
1047 expected_error="crash_reporter should have at least 1000 FUNC "
1048 "records, found 500",
1049 ),
1050 CrashReporterSymbolFileTest(
1051 name="Insufficient STACK records",
1052 stack_lines=100,
1053 expected_error="crash_reporter should have at least 1000 STACK "
1054 "records, found 100",
1055 ),
1056 CrashReporterSymbolFileTest(
1057 name="Insufficient line number records",
1058 line_number_lines=2000,
1059 expected_error="crash_reporter should have at least 10,000 "
1060 "line number records, found 2000",
1061 ),
1062 ]
1063 for test in CRASH_REPORTER_SYMBOL_TESTS:
1064 with self.subTest(
1065 name=test.name
1066 ), multiprocessing.Manager() as mp_manager:
1067 sym_file = self.tempdir / "crash_reporter.sym"
1068 self._CreateSymbolFile(
1069 sym_file,
1070 func_lines=test.func_lines,
1071 stack_lines=test.stack_lines,
1072 line_number_lines=test.line_number_lines,
1073 )
1074 found_files = mp_manager.list()
1075 with self.assertLogs(level=logging.WARNING) as cm:
1076 self.assertFalse(
1077 cros_generate_breakpad_symbols.ValidateSymbolFile(
1078 str(sym_file),
1079 "/build/board/sbin/crash_reporter",
1080 "/build/board",
1081 found_files,
1082 )
1083 )
1084 self.assertIn(test.expected_error, cm.output[0])
1085 self.assertEqual(len(cm.output), 1)
1086
1087 def testValidLibMetricsSymbolFile(self):
1088 """Test a libmetrics.so symbol file can pass the additional checks."""
1089 with multiprocessing.Manager() as mp_manager:
1090 sym_file = self.tempdir / "libmetrics.so.sym"
1091 self._CreateSymbolFile(
1092 sym_file,
1093 func_lines=200,
1094 public_lines=2,
1095 stack_lines=2000,
1096 line_number_lines=10000,
1097 )
1098 found_files = mp_manager.list()
1099 self.assertTrue(
1100 cros_generate_breakpad_symbols.ValidateSymbolFile(
1101 str(sym_file),
1102 "/build/board/usr/lib64/libmetrics.so",
1103 "/build/board",
1104 found_files,
1105 )
1106 )
1107 self.assertEqual(
1108 list(found_files),
1109 [cros_generate_breakpad_symbols.ExpectedFiles.LIBMETRICS],
1110 )
1111
1112 def testInvalidLibMetricsSymbolFile(self):
1113 """Test that a libmetrics.so symbol file is held to higher standards."""
1114
1115 class LibMetricsSymbolFileTest:
1116 """Defines the subtest for an invalid libmetrics.so symbol file."""
1117
1118 def __init__(
1119 self,
1120 name,
1121 expected_error,
1122 func_lines=200,
1123 public_lines=2,
1124 stack_lines=2000,
1125 line_number_lines=10000,
1126 ):
1127 self.name = name
1128 self.expected_error = expected_error
1129 self.func_lines = func_lines
1130 self.public_lines = public_lines
1131 self.stack_lines = stack_lines
1132 self.line_number_lines = line_number_lines
1133
1134 LIBMETRICS_SYMBOL_TESTS = [
1135 LibMetricsSymbolFileTest(
1136 name="Insufficient FUNC records",
1137 func_lines=10,
1138 expected_error="libmetrics should have at least 100 FUNC "
1139 "records, found 10",
1140 ),
1141 LibMetricsSymbolFileTest(
1142 name="Insufficient PUBLIC records",
1143 public_lines=0,
1144 expected_error="libmetrics should have at least 1 PUBLIC "
1145 "record, found 0",
1146 ),
1147 LibMetricsSymbolFileTest(
1148 name="Insufficient STACK records",
1149 stack_lines=500,
1150 expected_error="libmetrics should have at least 1000 STACK "
1151 "records, found 500",
1152 ),
1153 LibMetricsSymbolFileTest(
1154 name="Insufficient line number records",
1155 line_number_lines=2000,
1156 expected_error="libmetrics should have at least 5000 "
1157 "line number records, found 2000",
1158 ),
1159 ]
1160 for test in LIBMETRICS_SYMBOL_TESTS:
1161 with self.subTest(
1162 name=test.name
1163 ), multiprocessing.Manager() as mp_manager:
1164 sym_file = self.tempdir / "libmetrics.so.sym"
1165 self._CreateSymbolFile(
1166 sym_file,
1167 func_lines=test.func_lines,
1168 public_lines=test.public_lines,
1169 stack_lines=test.stack_lines,
1170 line_number_lines=test.line_number_lines,
1171 )
1172 found_files = mp_manager.list()
1173 with self.assertLogs(level=logging.WARNING) as cm:
1174 self.assertFalse(
1175 cros_generate_breakpad_symbols.ValidateSymbolFile(
1176 str(sym_file),
1177 "/build/board/usr/lib64/libmetrics.so",
1178 "/build/board",
1179 found_files,
1180 )
1181 )
1182 self.assertIn(test.expected_error, cm.output[0])
1183 self.assertEqual(len(cm.output), 1)
1184
1185
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001186class UtilsTestDir(cros_test_lib.TempDirTestCase):
Alex Klein1699fab2022-09-08 08:46:06 -06001187 """Tests ReadSymsHeader."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001188
Alex Klein1699fab2022-09-08 08:46:06 -06001189 def testReadSymsHeaderGoodFile(self):
1190 """Make sure ReadSymsHeader can parse sym files"""
1191 sym_file = os.path.join(self.tempdir, "sym")
1192 osutils.WriteFile(sym_file, "MODULE Linux x86 s0m31D chrooome")
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001193 result = cros_generate_breakpad_symbols.ReadSymsHeader(
1194 sym_file, "unused_elfname"
1195 )
Alex Klein1699fab2022-09-08 08:46:06 -06001196 self.assertEqual(result.cpu, "x86")
1197 self.assertEqual(result.id, "s0m31D")
1198 self.assertEqual(result.name, "chrooome")
1199 self.assertEqual(result.os, "Linux")
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001200
1201
1202class UtilsTest(cros_test_lib.TestCase):
Alex Klein1699fab2022-09-08 08:46:06 -06001203 """Tests ReadSymsHeader."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001204
Alex Klein1699fab2022-09-08 08:46:06 -06001205 def testReadSymsHeaderGoodBuffer(self):
1206 """Make sure ReadSymsHeader can parse sym file handles"""
1207 result = cros_generate_breakpad_symbols.ReadSymsHeader(
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001208 io.BytesIO(b"MODULE Linux arm MY-ID-HERE blkid"), "unused_elfname"
Alex Klein1699fab2022-09-08 08:46:06 -06001209 )
1210 self.assertEqual(result.cpu, "arm")
1211 self.assertEqual(result.id, "MY-ID-HERE")
1212 self.assertEqual(result.name, "blkid")
1213 self.assertEqual(result.os, "Linux")
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001214
Alex Klein1699fab2022-09-08 08:46:06 -06001215 def testReadSymsHeaderBadd(self):
1216 """Make sure ReadSymsHeader throws on bad sym files"""
1217 self.assertRaises(
1218 ValueError,
1219 cros_generate_breakpad_symbols.ReadSymsHeader,
1220 io.BytesIO(b"asdf"),
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001221 "unused_elfname",
Alex Klein1699fab2022-09-08 08:46:06 -06001222 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001223
Alex Klein1699fab2022-09-08 08:46:06 -06001224 def testBreakpadDir(self):
1225 """Make sure board->breakpad path expansion works"""
1226 expected = "/build/blah/usr/lib/debug/breakpad"
1227 result = cros_generate_breakpad_symbols.FindBreakpadDir("blah")
1228 self.assertEqual(expected, result)
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001229
Alex Klein1699fab2022-09-08 08:46:06 -06001230 def testDebugDir(self):
1231 """Make sure board->debug path expansion works"""
1232 expected = "/build/blah/usr/lib/debug"
1233 result = cros_generate_breakpad_symbols.FindDebugDir("blah")
1234 self.assertEqual(expected, result)