blob: 47b0353c3189287834b5300d35eb5860ebc86eb6 [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
17from chromite.lib import parallel
18from chromite.lib import parallel_unittest
19from chromite.lib import partial_mock
20from chromite.scripts import cros_generate_breakpad_symbols
21
Mike Frysinger69cb41d2013-08-11 20:08:19 -040022
23class FindDebugDirMock(partial_mock.PartialMock):
Alex Klein1699fab2022-09-08 08:46:06 -060024 """Mock out the DebugDir helper so we can point it to a tempdir."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -040025
Alex Klein1699fab2022-09-08 08:46:06 -060026 TARGET = "chromite.scripts.cros_generate_breakpad_symbols"
27 ATTRS = ("FindDebugDir",)
28 DEFAULT_ATTR = "FindDebugDir"
Mike Frysinger69cb41d2013-08-11 20:08:19 -040029
Alex Klein1699fab2022-09-08 08:46:06 -060030 def __init__(self, path, *args, **kwargs):
31 self.path = path
32 super().__init__(*args, **kwargs)
Mike Frysinger69cb41d2013-08-11 20:08:19 -040033
Alex Klein1699fab2022-09-08 08:46:06 -060034 # pylint: disable=unused-argument
35 def FindDebugDir(self, _board, sysroot=None):
36 return self.path
Mike Frysinger69cb41d2013-08-11 20:08:19 -040037
38
Mike Frysinger3ef6d972019-08-24 20:07:42 -040039# This long decorator triggers a false positive in the docstring test.
40# https://github.com/PyCQA/pylint/issues/3077
41# pylint: disable=bad-docstring-quotes
Alex Klein1699fab2022-09-08 08:46:06 -060042@mock.patch(
43 "chromite.scripts.cros_generate_breakpad_symbols." "GenerateBreakpadSymbol"
44)
Mike Frysinger69cb41d2013-08-11 20:08:19 -040045class GenerateSymbolsTest(cros_test_lib.MockTempDirTestCase):
Alex Klein1699fab2022-09-08 08:46:06 -060046 """Test GenerateBreakpadSymbols."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -040047
Alex Klein1699fab2022-09-08 08:46:06 -060048 def setUp(self):
49 self.board = "monkey-board"
50 self.board_dir = os.path.join(self.tempdir, "build", self.board)
51 self.debug_dir = os.path.join(self.board_dir, "usr", "lib", "debug")
52 self.breakpad_dir = os.path.join(self.debug_dir, "breakpad")
Mike Frysinger69cb41d2013-08-11 20:08:19 -040053
Alex Klein1699fab2022-09-08 08:46:06 -060054 # Generate a tree of files which we'll scan through.
55 elf_files = [
56 "bin/elf",
57 "iii/large-elf",
58 # Need some kernel modules (with & without matching .debug).
59 "lib/modules/3.10/module.ko",
60 "lib/modules/3.10/module-no-debug.ko",
61 # Need a file which has an ELF only, but not a .debug.
62 "usr/bin/elf-only",
63 "usr/sbin/elf",
64 ]
65 debug_files = [
66 "bin/bad-file",
67 "bin/elf.debug",
68 "iii/large-elf.debug",
69 "boot/vmlinux.debug",
70 "lib/modules/3.10/module.ko.debug",
71 # Need a file which has a .debug only, but not an ELF.
72 "sbin/debug-only.debug",
73 "usr/sbin/elf.debug",
74 ]
75 for f in [os.path.join(self.board_dir, x) for x in elf_files] + [
76 os.path.join(self.debug_dir, x) for x in debug_files
77 ]:
78 osutils.Touch(f, makedirs=True)
Mike Frysinger69cb41d2013-08-11 20:08:19 -040079
Alex Klein1699fab2022-09-08 08:46:06 -060080 # Set up random build dirs and symlinks.
81 buildid = os.path.join(self.debug_dir, ".build-id", "00")
82 osutils.SafeMakedirs(buildid)
83 os.symlink("/asdf", os.path.join(buildid, "foo"))
84 os.symlink("/bin/sh", os.path.join(buildid, "foo.debug"))
85 os.symlink("/bin/sh", os.path.join(self.debug_dir, "file.debug"))
86 osutils.WriteFile(
87 os.path.join(self.debug_dir, "iii", "large-elf.debug"),
88 "just some content",
89 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -040090
Alex Klein1699fab2022-09-08 08:46:06 -060091 self.StartPatcher(FindDebugDirMock(self.debug_dir))
Mike Frysinger69cb41d2013-08-11 20:08:19 -040092
Alex Klein1699fab2022-09-08 08:46:06 -060093 def testNormal(self, gen_mock):
94 """Verify all the files we expect to get generated do"""
95 with parallel_unittest.ParallelMock():
96 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
97 self.board, sysroot=self.board_dir
98 )
99 self.assertEqual(ret, 0)
100 self.assertEqual(gen_mock.call_count, 5)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400101
Alex Klein1699fab2022-09-08 08:46:06 -0600102 # The largest ELF should be processed first.
103 call1 = (
104 os.path.join(self.board_dir, "iii/large-elf"),
105 os.path.join(self.debug_dir, "iii/large-elf.debug"),
106 )
107 self.assertEqual(gen_mock.call_args_list[0][0], call1)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400108
Alex Klein1699fab2022-09-08 08:46:06 -0600109 # The other ELFs can be called in any order.
110 call2 = (
111 os.path.join(self.board_dir, "bin/elf"),
112 os.path.join(self.debug_dir, "bin/elf.debug"),
113 )
114 call3 = (
115 os.path.join(self.board_dir, "usr/sbin/elf"),
116 os.path.join(self.debug_dir, "usr/sbin/elf.debug"),
117 )
118 call4 = (
119 os.path.join(self.board_dir, "lib/modules/3.10/module.ko"),
120 os.path.join(
121 self.debug_dir, "lib/modules/3.10/module.ko.debug"
122 ),
123 )
124 call5 = (
125 os.path.join(self.board_dir, "boot/vmlinux"),
126 os.path.join(self.debug_dir, "boot/vmlinux.debug"),
127 )
128 exp_calls = set((call2, call3, call4, call5))
129 actual_calls = set(
130 (
131 gen_mock.call_args_list[1][0],
132 gen_mock.call_args_list[2][0],
133 gen_mock.call_args_list[3][0],
134 gen_mock.call_args_list[4][0],
135 )
136 )
137 self.assertEqual(exp_calls, actual_calls)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400138
Alex Klein1699fab2022-09-08 08:46:06 -0600139 def testFileList(self, gen_mock):
140 """Verify that file_list restricts the symbols generated"""
141 with parallel_unittest.ParallelMock():
142 call1 = (
143 os.path.join(self.board_dir, "usr/sbin/elf"),
144 os.path.join(self.debug_dir, "usr/sbin/elf.debug"),
145 )
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700146
Alex Klein1699fab2022-09-08 08:46:06 -0600147 # Filter with elf path.
148 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
149 self.board,
150 sysroot=self.board_dir,
151 breakpad_dir=self.breakpad_dir,
152 file_list=[os.path.join(self.board_dir, "usr", "sbin", "elf")],
153 )
154 self.assertEqual(ret, 0)
155 self.assertEqual(gen_mock.call_count, 1)
156 self.assertEqual(gen_mock.call_args_list[0][0], call1)
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700157
Alex Klein1699fab2022-09-08 08:46:06 -0600158 # Filter with debug symbols file path.
159 gen_mock.reset_mock()
160 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
161 self.board,
162 sysroot=self.board_dir,
163 breakpad_dir=self.breakpad_dir,
164 file_list=[
165 os.path.join(self.debug_dir, "usr", "sbin", "elf.debug")
166 ],
167 )
168 self.assertEqual(ret, 0)
169 self.assertEqual(gen_mock.call_count, 1)
170 self.assertEqual(gen_mock.call_args_list[0][0], call1)
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700171
Alex Klein1699fab2022-09-08 08:46:06 -0600172 def testGenLimit(self, gen_mock):
173 """Verify generate_count arg works"""
174 with parallel_unittest.ParallelMock():
175 # Generate nothing!
176 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
177 self.board,
178 sysroot=self.board_dir,
179 breakpad_dir=self.breakpad_dir,
180 generate_count=0,
181 )
182 self.assertEqual(ret, 0)
183 self.assertEqual(gen_mock.call_count, 0)
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700184
Alex Klein1699fab2022-09-08 08:46:06 -0600185 # Generate just one.
186 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
187 self.board,
188 sysroot=self.board_dir,
189 breakpad_dir=self.breakpad_dir,
190 generate_count=1,
191 )
192 self.assertEqual(ret, 0)
193 self.assertEqual(gen_mock.call_count, 1)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400194
Alex Klein1699fab2022-09-08 08:46:06 -0600195 # The largest ELF should be processed first.
196 call1 = (
197 os.path.join(self.board_dir, "iii/large-elf"),
198 os.path.join(self.debug_dir, "iii/large-elf.debug"),
199 )
200 self.assertEqual(gen_mock.call_args_list[0][0], call1)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400201
Alex Klein1699fab2022-09-08 08:46:06 -0600202 def testGenErrors(self, gen_mock):
203 """Verify we handle errors from generation correctly"""
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400204
Alex Klein1699fab2022-09-08 08:46:06 -0600205 def _SetError(*_args, **kwargs):
206 kwargs["num_errors"].value += 1
207 return 1
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400208
Alex Klein1699fab2022-09-08 08:46:06 -0600209 gen_mock.side_effect = _SetError
210 with parallel_unittest.ParallelMock():
211 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
212 self.board, sysroot=self.board_dir
213 )
214 self.assertEqual(ret, 5)
215 self.assertEqual(gen_mock.call_count, 5)
Mike Frysinger9a628bb2013-10-24 15:51:37 -0400216
Alex Klein1699fab2022-09-08 08:46:06 -0600217 def testCleaningTrue(self, gen_mock):
218 """Verify behavior of clean_breakpad=True"""
219 with parallel_unittest.ParallelMock():
220 # Dir does not exist, and then does.
221 self.assertNotExists(self.breakpad_dir)
222 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
223 self.board,
224 sysroot=self.board_dir,
225 generate_count=1,
226 clean_breakpad=True,
227 )
228 self.assertEqual(ret, 0)
229 self.assertEqual(gen_mock.call_count, 1)
230 self.assertExists(self.breakpad_dir)
Mike Frysinger9a628bb2013-10-24 15:51:37 -0400231
Alex Klein1699fab2022-09-08 08:46:06 -0600232 # Dir exists before & after.
233 # File exists, but then doesn't.
234 stub_file = os.path.join(self.breakpad_dir, "fooooooooo")
235 osutils.Touch(stub_file)
236 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
237 self.board,
238 sysroot=self.board_dir,
239 generate_count=1,
240 clean_breakpad=True,
241 )
242 self.assertEqual(ret, 0)
243 self.assertEqual(gen_mock.call_count, 2)
244 self.assertNotExists(stub_file)
Mike Frysinger9a628bb2013-10-24 15:51:37 -0400245
Alex Klein1699fab2022-09-08 08:46:06 -0600246 def testCleaningFalse(self, gen_mock):
247 """Verify behavior of clean_breakpad=False"""
248 with parallel_unittest.ParallelMock():
249 # Dir does not exist, and then does.
250 self.assertNotExists(self.breakpad_dir)
251 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
252 self.board,
253 sysroot=self.board_dir,
254 generate_count=1,
255 clean_breakpad=False,
256 )
257 self.assertEqual(ret, 0)
258 self.assertEqual(gen_mock.call_count, 1)
259 self.assertExists(self.breakpad_dir)
Mike Frysinger9a628bb2013-10-24 15:51:37 -0400260
Alex Klein1699fab2022-09-08 08:46:06 -0600261 # Dir exists before & after.
262 # File exists before & after.
263 stub_file = os.path.join(self.breakpad_dir, "fooooooooo")
264 osutils.Touch(stub_file)
265 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
266 self.board,
267 sysroot=self.board_dir,
268 generate_count=1,
269 clean_breakpad=False,
270 )
271 self.assertEqual(ret, 0)
272 self.assertEqual(gen_mock.call_count, 2)
273 self.assertExists(stub_file)
274
275 def testExclusionList(self, gen_mock):
276 """Verify files in directories of the exclusion list are excluded"""
277 exclude_dirs = ["bin", "usr", "fake/dir/fake"]
278 with parallel_unittest.ParallelMock():
279 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
280 self.board, sysroot=self.board_dir, exclude_dirs=exclude_dirs
281 )
282 self.assertEqual(ret, 0)
283 self.assertEqual(gen_mock.call_count, 3)
284
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400285
Benjamin Gordon121a2aa2018-05-04 16:24:45 -0600286class GenerateSymbolTest(cros_test_lib.RunCommandTempDirTestCase):
Alex Klein1699fab2022-09-08 08:46:06 -0600287 """Test GenerateBreakpadSymbol."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400288
Alex Klein1699fab2022-09-08 08:46:06 -0600289 def setUp(self):
290 self.elf_file = os.path.join(self.tempdir, "elf")
291 osutils.Touch(self.elf_file)
292 self.debug_dir = os.path.join(self.tempdir, "debug")
293 self.debug_file = os.path.join(self.debug_dir, "elf.debug")
294 osutils.Touch(self.debug_file, makedirs=True)
295 # Not needed as the code itself should create it as needed.
296 self.breakpad_dir = os.path.join(self.debug_dir, "breakpad")
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400297
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800298 self.FILE_OUT = (
299 f"{self.elf_file}: ELF 64-bit LSB pie executable, x86-64, "
300 "version 1 (SYSV), dynamically linked, interpreter "
301 "/lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, "
302 "BuildID[sha1]=cf9a21fa6b14bfb2dfcb76effd713c4536014d95, stripped"
303 )
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800304 # A symbol file which would pass validation.
305 MINIMAL_SYMBOL_FILE = (
306 "MODULE OS CPU ID NAME\n"
307 "PUBLIC f10 0 func\n"
308 "STACK CFI INIT f10 22 .cfa: $rsp 8 + .ra: .cfa -8 + ^\n"
309 )
310 self.rc.SetDefaultCmdResult(stdout=MINIMAL_SYMBOL_FILE)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800311 self.rc.AddCmdResult(
312 ["/usr/bin/file", self.elf_file], stdout=self.FILE_OUT
313 )
Alex Klein1699fab2022-09-08 08:46:06 -0600314 self.assertCommandContains = self.rc.assertCommandContains
315 self.sym_file = os.path.join(self.breakpad_dir, "NAME/ID/NAME.sym")
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400316
Alex Klein1699fab2022-09-08 08:46:06 -0600317 self.StartPatcher(FindDebugDirMock(self.debug_dir))
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400318
Alex Klein1699fab2022-09-08 08:46:06 -0600319 def assertCommandArgs(self, i, args):
320 """Helper for looking at the args of the |i|th call"""
321 self.assertEqual(self.rc.call_args_list[i][0][0], args)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400322
Alex Klein1699fab2022-09-08 08:46:06 -0600323 def testNormal(self):
324 """Normal run -- given an ELF and a debug file"""
325 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800326 self.elf_file,
327 self.debug_file,
328 self.breakpad_dir,
Alex Klein1699fab2022-09-08 08:46:06 -0600329 )
330 self.assertEqual(ret, self.sym_file)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800331 self.assertEqual(self.rc.call_count, 2)
Alex Klein1699fab2022-09-08 08:46:06 -0600332 self.assertCommandArgs(
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800333 1, ["dump_syms", "-v", self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600334 )
335 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400336
Alex Klein1699fab2022-09-08 08:46:06 -0600337 def testNormalNoCfi(self):
338 """Normal run w/out CFI"""
339 # Make sure the num_errors flag works too.
340 num_errors = ctypes.c_int()
341 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
342 self.elf_file,
343 breakpad_dir=self.breakpad_dir,
344 strip_cfi=True,
345 num_errors=num_errors,
346 )
347 self.assertEqual(ret, self.sym_file)
348 self.assertEqual(num_errors.value, 0)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800349 self.assertCommandArgs(1, ["dump_syms", "-v", "-c", self.elf_file])
350 self.assertEqual(self.rc.call_count, 2)
Alex Klein1699fab2022-09-08 08:46:06 -0600351 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400352
Alex Klein1699fab2022-09-08 08:46:06 -0600353 def testNormalElfOnly(self):
354 """Normal run -- given just an ELF"""
355 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
356 self.elf_file, breakpad_dir=self.breakpad_dir
357 )
358 self.assertEqual(ret, self.sym_file)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800359 self.assertCommandArgs(1, ["dump_syms", "-v", self.elf_file])
360 self.assertEqual(self.rc.call_count, 2)
Alex Klein1699fab2022-09-08 08:46:06 -0600361 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400362
Alex Klein1699fab2022-09-08 08:46:06 -0600363 def testNormalSudo(self):
364 """Normal run where ELF is readable only by root"""
365 with mock.patch.object(os, "access") as mock_access:
366 mock_access.return_value = False
367 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
368 self.elf_file, breakpad_dir=self.breakpad_dir
369 )
370 self.assertEqual(ret, self.sym_file)
371 self.assertCommandArgs(
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800372 1, ["sudo", "--", "dump_syms", "-v", self.elf_file]
Alex Klein1699fab2022-09-08 08:46:06 -0600373 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400374
Alex Klein1699fab2022-09-08 08:46:06 -0600375 def testLargeDebugFail(self):
376 """Running w/large .debug failed, but retry worked"""
377 self.rc.AddCmdResult(
378 ["dump_syms", "-v", self.elf_file, self.debug_dir], returncode=1
379 )
380 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
381 self.elf_file, self.debug_file, self.breakpad_dir
382 )
383 self.assertEqual(ret, self.sym_file)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800384 self.assertEqual(self.rc.call_count, 4)
Alex Klein1699fab2022-09-08 08:46:06 -0600385 self.assertCommandArgs(
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800386 1, ["dump_syms", "-v", self.elf_file, self.debug_dir]
387 )
388 # The current fallback from _DumpExpectingSymbols() to
389 # _DumpAllowingBasicFallback() causes the first dump_sums command to get
390 # repeated.
391 self.assertCommandArgs(
392 2, ["dump_syms", "-v", self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600393 )
394 self.assertCommandArgs(
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800395 3, ["dump_syms", "-v", "-c", "-r", self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600396 )
397 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400398
Alex Klein1699fab2022-09-08 08:46:06 -0600399 def testDebugFail(self):
400 """Running w/.debug always failed, but works w/out"""
401 self.rc.AddCmdResult(
402 ["dump_syms", "-v", self.elf_file, self.debug_dir], returncode=1
403 )
404 self.rc.AddCmdResult(
405 ["dump_syms", "-v", "-c", "-r", self.elf_file, self.debug_dir],
406 returncode=1,
407 )
408 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
409 self.elf_file, self.debug_file, self.breakpad_dir
410 )
411 self.assertEqual(ret, self.sym_file)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800412 self.assertEqual(self.rc.call_count, 5)
Alex Klein1699fab2022-09-08 08:46:06 -0600413 self.assertCommandArgs(
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800414 1, ["dump_syms", "-v", self.elf_file, self.debug_dir]
415 )
416 # The current fallback from _DumpExpectingSymbols() to
417 # _DumpAllowingBasicFallback() causes the first dump_sums command to get
418 # repeated.
419 self.assertCommandArgs(
420 2, ["dump_syms", "-v", self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600421 )
422 self.assertCommandArgs(
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800423 3, ["dump_syms", "-v", "-c", "-r", self.elf_file, self.debug_dir]
Alex Klein1699fab2022-09-08 08:46:06 -0600424 )
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800425 self.assertCommandArgs(4, ["dump_syms", "-v", self.elf_file])
Alex Klein1699fab2022-09-08 08:46:06 -0600426 self.assertExists(self.sym_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400427
Alex Klein1699fab2022-09-08 08:46:06 -0600428 def testCompleteFail(self):
429 """Running dump_syms always fails"""
430 self.rc.SetDefaultCmdResult(returncode=1)
431 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
432 self.elf_file, breakpad_dir=self.breakpad_dir
433 )
434 self.assertEqual(ret, 1)
435 # Make sure the num_errors flag works too.
436 num_errors = ctypes.c_int()
437 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
438 self.elf_file, breakpad_dir=self.breakpad_dir, num_errors=num_errors
439 )
440 self.assertEqual(ret, 1)
441 self.assertEqual(num_errors.value, 1)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400442
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800443 def testKernelObjects(self):
444 """Kernel object files should call _DumpAllowingBasicFallback()"""
445 ko_file = os.path.join(self.tempdir, "elf.ko")
446 osutils.Touch(ko_file)
447 self.rc.AddCmdResult(
448 ["dump_syms", "-v", ko_file, self.debug_dir], returncode=1
449 )
450 self.rc.AddCmdResult(
451 ["dump_syms", "-v", "-c", "-r", ko_file, self.debug_dir],
452 returncode=1,
453 )
454 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
455 ko_file, self.debug_file, self.breakpad_dir
456 )
457 self.assertEqual(ret, self.sym_file)
458 self.assertEqual(self.rc.call_count, 3)
459 # Only one call (at the beginning of _DumpAllowingBasicFallback())
460 # to "dump_syms -v"
461 self.assertCommandArgs(0, ["dump_syms", "-v", ko_file, self.debug_dir])
462 self.assertCommandArgs(
463 1, ["dump_syms", "-v", "-c", "-r", ko_file, self.debug_dir]
464 )
465 self.assertCommandArgs(2, ["dump_syms", "-v", ko_file])
466 self.assertExists(self.sym_file)
467
468 def testGoBinary(self):
469 """Go binaries should call _DumpAllowingBasicFallback()
470
471 Also tests that dump_syms failing with 'file contains no debugging
472 information' does not fail the script.
473 """
474 go_binary = os.path.join(self.tempdir, "goprogram")
475 osutils.Touch(go_binary)
476 go_debug_file = os.path.join(self.debug_dir, "goprogram.debug")
477 osutils.Touch(go_debug_file, makedirs=True)
478 FILE_OUT_GO = go_binary + (
479 ": ELF 64-bit LSB executable, x86-64, "
480 "version 1 (SYSV), statically linked, "
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800481 "Go BuildID=KKXVlL66E8Qmngr4qll9/5kOKGZw9I7TmNhoqKLqq/SiYVJam6w5Fo"
482 "39B3BtDo/ba8_ceezZ-3R4qEv6_-K, not stripped"
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800483 )
484 self.rc.AddCmdResult(["/usr/bin/file", go_binary], stdout=FILE_OUT_GO)
485 self.rc.AddCmdResult(
486 ["dump_syms", "-v", go_binary, self.debug_dir], returncode=1
487 )
488 self.rc.AddCmdResult(
489 ["dump_syms", "-v", "-c", "-r", go_binary, self.debug_dir],
490 returncode=1,
491 )
492 self.rc.AddCmdResult(
493 ["dump_syms", "-v", go_binary],
494 returncode=1,
495 stderr=(
496 f"{go_binary}: file contains no debugging information "
497 '(no ".stab" or ".debug_info" sections)'
498 ),
499 )
500 num_errors = ctypes.c_int()
501 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
502 go_binary, go_debug_file, self.breakpad_dir
503 )
504 self.assertEqual(ret, 0)
505 self.assertEqual(self.rc.call_count, 4)
506 self.assertCommandArgs(0, ["/usr/bin/file", go_binary])
507 # Only one call (at the beginning of _DumpAllowingBasicFallback())
508 # to "dump_syms -v"
509 self.assertCommandArgs(
510 1, ["dump_syms", "-v", go_binary, self.debug_dir]
511 )
512 self.assertCommandArgs(
513 2, ["dump_syms", "-v", "-c", "-r", go_binary, self.debug_dir]
514 )
515 self.assertCommandArgs(3, ["dump_syms", "-v", go_binary])
516 self.assertNotExists(self.sym_file)
517 self.assertEqual(num_errors.value, 0)
518
Ian Barkley-Yeunge1785762023-03-08 18:32:18 -0800519 def _testBinaryIsInLocalFallback(self, directory, filename):
520 binary = os.path.join(self.tempdir, directory, filename)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800521 osutils.Touch(binary, makedirs=True)
Ian Barkley-Yeunge1785762023-03-08 18:32:18 -0800522 debug_dir = os.path.join(self.debug_dir, directory)
523 debug_file = os.path.join(debug_dir, f"{filename}.debug")
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800524 osutils.Touch(debug_file, makedirs=True)
525 self.rc.AddCmdResult(["/usr/bin/file", binary], stdout=self.FILE_OUT)
526 self.rc.AddCmdResult(
527 ["dump_syms", "-v", binary, debug_dir], returncode=1
528 )
529 self.rc.AddCmdResult(
530 ["dump_syms", "-v", "-c", "-r", binary, debug_dir],
531 returncode=1,
532 )
533 self.rc.AddCmdResult(
534 ["dump_syms", "-v", binary],
535 returncode=1,
536 stderr=(
537 f"{binary}: file contains no debugging information "
538 '(no ".stab" or ".debug_info" sections)'
539 ),
540 )
541 num_errors = ctypes.c_int()
542 ret = cros_generate_breakpad_symbols.GenerateBreakpadSymbol(
543 binary, debug_file, self.breakpad_dir, sysroot=self.tempdir
544 )
545 self.assertEqual(ret, 0)
546 self.assertEqual(self.rc.call_count, 4)
547 self.assertCommandArgs(0, ["/usr/bin/file", binary])
548 # Only one call (at the beginning of _DumpAllowingBasicFallback())
549 # to "dump_syms -v"
550 self.assertCommandArgs(1, ["dump_syms", "-v", binary, debug_dir])
551 self.assertCommandArgs(
552 2, ["dump_syms", "-v", "-c", "-r", binary, debug_dir]
553 )
554 self.assertCommandArgs(3, ["dump_syms", "-v", binary])
555 self.assertNotExists(self.sym_file)
556 self.assertEqual(num_errors.value, 0)
557
Ian Barkley-Yeunge1785762023-03-08 18:32:18 -0800558 def testAllowlist(self):
559 """Binaries in the allowlist should call _DumpAllowingBasicFallback()"""
560 self._testBinaryIsInLocalFallback("usr/bin", "goldctl")
561
562 def testUsrLocalSkip(self):
563 """Binaries in /usr/local should call _DumpAllowingBasicFallback()"""
564 self._testBinaryIsInLocalFallback("usr/local", "minidump_stackwalk")
565
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400566
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800567class ValidateSymbolFileTest(cros_test_lib.TempDirTestCase):
568 """Tests ValidateSymbolFile"""
569
570 def _GetTestdataFile(self, filename: str) -> str:
571 """Gets the path to a file in the testdata directory.
572
573 Args:
574 filename: The base filename of the file.
575
576 Returns:
577 A string with the complete path to the file.
578 """
579 return os.path.join(os.path.dirname(__file__), "testdata", filename)
580
581 def testValidSymbolFiles(self):
582 """Make sure ValidateSymbolFile passes on valid files"""
583
584 # All files are in the testdata/ subdirectory.
585 VALID_SYMBOL_FILES = [
586 # A "normal" symbol file from an executable.
587 "basic.sym",
588 # A "normal" symbol file from a shared library.
589 "basic_lib.sym",
590 # A symbol file with PUBLIC records but no FUNC records.
591 "public_only.sym",
592 # A symbol file with FUNC records but no PUBLIC records.
593 "func_only.sym",
594 # A symbol file with at least one of every line type.
595 "all_line_types.sym",
596 ]
597
598 for file in VALID_SYMBOL_FILES:
599 with self.subTest(
600 file=file
601 ), multiprocessing.Manager() as mp_manager:
602 found_files = mp_manager.list()
603 self.assertTrue(
604 cros_generate_breakpad_symbols.ValidateSymbolFile(
605 self._GetTestdataFile(file),
606 "/build/board/bin/foo",
607 "/build/board",
608 found_files,
609 )
610 )
611 self.assertFalse(found_files)
612
613 def testInvalidSymbolFiles(self):
614 """Make sure ValidateSymbolFile fails on invalid files.
615
616 This test only covers cases that return false, not cases that raise
617 exceptions.
618 """
619
620 class InvalidSymbolFile:
621 """The name of an invalid symbol file + the expected error msg."""
622
623 def __init__(self, filename, expected_errors):
624 self.filename = filename
625 self.expected_errors = expected_errors
626
627 INVALID_SYMBOL_FILES = [
628 InvalidSymbolFile(
629 "bad_no_func_or_public.sym",
630 [
631 "WARNING:root:/build/board/bin/foo: "
632 "Symbol file has no FUNC or PUBLIC records"
633 ],
634 ),
635 InvalidSymbolFile(
636 "bad_no_stack.sym",
637 [
638 "WARNING:root:/build/board/bin/foo: "
639 "Symbol file has no STACK records"
640 ],
641 ),
642 InvalidSymbolFile(
643 "bad_no_module.sym",
644 [
645 "WARNING:root:/build/board/bin/foo: "
646 "Symbol file has 0 MODULE lines"
647 ],
648 ),
649 InvalidSymbolFile(
650 "bad_two_modules.sym",
651 [
652 "WARNING:root:/build/board/bin/foo: "
653 "Symbol file has 2 MODULE lines"
654 ],
655 ),
656 InvalidSymbolFile(
657 "bad_func_no_line_numbers.sym",
658 [
659 "WARNING:root:/build/board/bin/foo: "
660 "Symbol file has FUNC records but no line numbers"
661 ],
662 ),
663 InvalidSymbolFile(
664 "bad_line_numbers_no_file.sym",
665 [
666 "WARNING:root:/build/board/bin/foo: "
667 "Symbol file has line number records but no FILE records"
668 ],
669 ),
670 InvalidSymbolFile(
671 "bad_inline_no_files.sym",
672 [
673 "WARNING:root:/build/board/bin/foo: "
674 "Symbol file has INLINE records but no FILE records"
675 ],
676 ),
677 InvalidSymbolFile(
678 "bad_inline_no_origins.sym",
679 [
680 "WARNING:root:/build/board/bin/foo: "
681 "Symbol file has INLINE records but no INLINE_ORIGIN "
682 "records"
683 ],
684 ),
685 InvalidSymbolFile(
686 "blank.sym",
687 [
688 "WARNING:root:/build/board/bin/foo: "
689 "Symbol file has no STACK records",
690 "WARNING:root:/build/board/bin/foo: "
691 "Symbol file has 0 MODULE lines",
692 "WARNING:root:/build/board/bin/foo: "
693 "Symbol file has no FUNC or PUBLIC records",
694 ],
695 ),
696 ]
697
698 for file in INVALID_SYMBOL_FILES:
699 with self.subTest(
700 file=file.filename
701 ), multiprocessing.Manager() as mp_manager:
702 found_files = mp_manager.list()
703 with self.assertLogs(level=logging.WARNING) as cm:
704 self.assertFalse(
705 cros_generate_breakpad_symbols.ValidateSymbolFile(
706 self._GetTestdataFile(file.filename),
707 "/build/board/bin/foo",
708 "/build/board",
709 found_files,
710 )
711 )
712 self.assertEqual(file.expected_errors, cm.output)
713 self.assertFalse(found_files)
714
715 def testInvalidSymbolFilesWhichRaise(self):
716 """Test ValidateSymbolFile raise exceptions on certain files"""
717
718 class InvalidSymbolFile:
719 """The invalid symbol file + the expected exception message"""
720
721 def __init__(self, filename, expected_exception_regex):
722 self.filename = filename
723 self.expected_exception_regex = expected_exception_regex
724
725 INVALID_SYMBOL_FILES = [
726 InvalidSymbolFile(
727 "bad_unknown_line_type.sym",
728 "symbol file has unknown line type UNKNOWN",
729 ),
730 InvalidSymbolFile(
731 "bad_blank_line.sym",
732 "symbol file has unexpected blank line",
733 ),
734 InvalidSymbolFile(
735 "bad_short_func.sym",
736 r"symbol file has FUNC line with 2 words "
737 r"\(expected 5 or more\)",
738 ),
739 InvalidSymbolFile(
740 "bad_short_line_number.sym",
741 r"symbol file has line number line with 3 words "
742 r"\(expected 4 - 4\)",
743 ),
744 InvalidSymbolFile(
745 "bad_long_line_number.sym",
746 r"symbol file has line number line with 5 words "
747 r"\(expected 4 - 4\)",
748 ),
749 ]
750
751 for file in INVALID_SYMBOL_FILES:
752 with self.subTest(
753 file=file.filename
754 ), multiprocessing.Manager() as mp_manager:
755 found_files = mp_manager.list()
756 self.assertRaisesRegex(
757 ValueError,
758 file.expected_exception_regex,
759 cros_generate_breakpad_symbols.ValidateSymbolFile,
760 self._GetTestdataFile(file.filename),
761 "/build/board/bin/foo",
762 "/build/board",
763 found_files,
764 )
765
766 def testAllowlist(self):
767 """Test that ELFs on the allowlist are allowed to pass."""
768 with multiprocessing.Manager() as mp_manager:
769 found_files = mp_manager.list()
770 self.assertTrue(
771 cros_generate_breakpad_symbols.ValidateSymbolFile(
772 self._GetTestdataFile("bad_no_stack.sym"),
773 "/build/board/opt/google/chrome/nacl_helper_bootstrap",
774 "/build/board",
775 found_files,
776 )
777 )
778 self.assertFalse(found_files)
779
780 def testAllowlistRegex(self):
781 """Test that ELFs on the regex-based allowlist are allowed to pass."""
782 with multiprocessing.Manager() as mp_manager:
783 found_files = mp_manager.list()
784 self.assertTrue(
785 cros_generate_breakpad_symbols.ValidateSymbolFile(
786 self._GetTestdataFile("bad_no_stack.sym"),
787 "/build/board/usr/lib/libcros_ml_core.so",
788 "/build/board",
789 found_files,
790 )
791 )
792 self.assertFalse(found_files)
793
794 def _CreateSymbolFile(
795 self,
796 sym_file: pathlib.Path,
797 func_lines: int = 0,
798 public_lines: int = 0,
799 stack_lines: int = 0,
800 line_number_lines: int = 0,
801 ) -> None:
802 """Creates a symbol file.
803
804 Creates a symbol file with the given number of lines (and enough other
805 lines to pass validation) in the temp directory.
806
807 To pass validation, chrome.sym files must be huge; create them
808 programmatically during the test instead of checking in a real 800MB+
809 chrome symbol file.
810 """
811 with sym_file.open(mode="w", encoding="utf-8") as f:
812 f.write("MODULE OS CPU ID NAME\n")
813 f.write("FILE 0 /path/to/source.cc\n")
814 for func in range(0, func_lines):
815 f.write(f"FUNC {func} 1 0 function{func}\n")
816 for public in range(0, public_lines):
817 f.write(f"PUBLIC {public} 0 Public{public}\n")
818 for line in range(0, line_number_lines):
819 f.write(f"{line} 1 {line} 0\n")
820 for stack in range(0, stack_lines):
821 f.write(f"STACK CFI {stack} .cfa: $esp {stack} +\n")
822
823 def testValidChromeSymbolFile(self):
824 """Test that a chrome symbol file can pass the additional checks"""
825 sym_file = self.tempdir / "chrome.sym"
826 self._CreateSymbolFile(
827 sym_file,
828 func_lines=100000,
829 public_lines=10,
830 stack_lines=1000000,
831 line_number_lines=1000000,
832 )
833 with multiprocessing.Manager() as mp_manager:
834 found_files = mp_manager.list()
835 self.assertTrue(
836 cros_generate_breakpad_symbols.ValidateSymbolFile(
837 str(sym_file),
838 "/build/board/opt/google/chrome/chrome",
839 "/build/board",
840 found_files,
841 )
842 )
843 self.assertEqual(
844 list(found_files),
845 [cros_generate_breakpad_symbols.ExpectedFiles.ASH_CHROME],
846 )
847
848 def testInvalidChromeSymbolFile(self):
849 """Test that a chrome symbol file is held to higher standards."""
850
851 class ChromeSymbolFileTest:
852 """Defines the subtest for an invalid Chrome symbol file."""
853
854 def __init__(
855 self,
856 name,
857 expected_error,
858 func_lines=100000,
859 stack_lines=1000000,
860 line_number_lines=1000000,
861 ):
862 self.name = name
863 self.expected_error = expected_error
864 self.func_lines = func_lines
865 self.stack_lines = stack_lines
866 self.line_number_lines = line_number_lines
867
868 CHROME_SYMBOL_TESTS = [
869 ChromeSymbolFileTest(
870 name="Insufficient FUNC records",
871 func_lines=10000,
872 expected_error="chrome should have at least 100,000 FUNC "
873 "records, found 10000",
874 ),
875 ChromeSymbolFileTest(
876 name="Insufficient STACK records",
877 stack_lines=100000,
878 expected_error="chrome should have at least 1,000,000 STACK "
879 "records, found 100000",
880 ),
881 ChromeSymbolFileTest(
882 name="Insufficient line number records",
883 line_number_lines=100000,
884 expected_error="chrome should have at least 1,000,000 "
885 "line number records, found 100000",
886 ),
887 ]
888 for test in CHROME_SYMBOL_TESTS:
889 with self.subTest(
890 name=test.name
891 ), multiprocessing.Manager() as mp_manager:
892 sym_file = self.tempdir / "chrome.sym"
893 self._CreateSymbolFile(
894 sym_file,
895 func_lines=test.func_lines,
896 public_lines=10,
897 stack_lines=test.stack_lines,
898 line_number_lines=test.line_number_lines,
899 )
900 found_files = mp_manager.list()
901 with self.assertLogs(level=logging.WARNING) as cm:
902 self.assertFalse(
903 cros_generate_breakpad_symbols.ValidateSymbolFile(
904 str(sym_file),
905 "/build/board/opt/google/chrome/chrome",
906 "/build/board",
907 found_files,
908 )
909 )
910 self.assertIn(test.expected_error, cm.output[0])
911 self.assertEqual(len(cm.output), 1)
912
913 def testValidLibcSymbolFile(self):
914 """Test that a libc.so symbol file can pass the additional checks."""
915 with multiprocessing.Manager() as mp_manager:
916 sym_file = self.tempdir / "libc.so.sym"
917 self._CreateSymbolFile(
918 sym_file, public_lines=200, stack_lines=20000
919 )
920 found_files = mp_manager.list()
921 self.assertTrue(
922 cros_generate_breakpad_symbols.ValidateSymbolFile(
923 str(sym_file),
924 "/build/board/lib64/libc.so.6",
925 "/build/board",
926 found_files,
927 )
928 )
929 self.assertEqual(
930 list(found_files),
931 [cros_generate_breakpad_symbols.ExpectedFiles.LIBC],
932 )
933
934 def testInvalidLibcSymbolFile(self):
935 """Test that a libc.so symbol file is held to higher standards."""
936
937 class LibcSymbolFileTest:
938 """Defines the subtest for an invalid libc symbol file."""
939
940 def __init__(
941 self,
942 name,
943 expected_error,
944 public_lines=200,
945 stack_lines=20000,
946 ):
947 self.name = name
948 self.expected_error = expected_error
949 self.public_lines = public_lines
950 self.stack_lines = stack_lines
951
952 LIBC_SYMBOL_TESTS = [
953 LibcSymbolFileTest(
954 name="Insufficient PUBLIC records",
955 public_lines=50,
956 expected_error="/build/board/lib64/libc.so.6 should have at "
957 "least 100 PUBLIC records, found 50",
958 ),
959 LibcSymbolFileTest(
960 name="Insufficient STACK records",
961 stack_lines=1000,
962 expected_error="/build/board/lib64/libc.so.6 should have at "
963 "least 10000 STACK records, found 1000",
964 ),
965 ]
966 for test in LIBC_SYMBOL_TESTS:
967 with self.subTest(
968 name=test.name
969 ), multiprocessing.Manager() as mp_manager:
970 sym_file = self.tempdir / "libc.so.sym"
971 self._CreateSymbolFile(
972 sym_file,
973 public_lines=test.public_lines,
974 stack_lines=test.stack_lines,
975 )
976 found_files = mp_manager.list()
977 with self.assertLogs(level=logging.WARNING) as cm:
978 self.assertFalse(
979 cros_generate_breakpad_symbols.ValidateSymbolFile(
980 str(sym_file),
981 "/build/board/lib64/libc.so.6",
982 "/build/board",
983 found_files,
984 )
985 )
986 self.assertIn(test.expected_error, cm.output[0])
987 self.assertEqual(len(cm.output), 1)
988
989 def testValidCrashReporterSymbolFile(self):
990 """Test a crash_reporter symbol file can pass the additional checks."""
991 with multiprocessing.Manager() as mp_manager:
992 sym_file = self.tempdir / "crash_reporter.sym"
993 self._CreateSymbolFile(
994 sym_file,
995 func_lines=2000,
996 public_lines=10,
997 stack_lines=2000,
998 line_number_lines=20000,
999 )
1000 found_files = mp_manager.list()
1001 self.assertTrue(
1002 cros_generate_breakpad_symbols.ValidateSymbolFile(
1003 str(sym_file),
1004 "/build/board/sbin/crash_reporter",
1005 "/build/board",
1006 found_files,
1007 )
1008 )
1009 self.assertEqual(
1010 list(found_files),
1011 [cros_generate_breakpad_symbols.ExpectedFiles.CRASH_REPORTER],
1012 )
1013
1014 def testInvalidCrashReporterSymbolFile(self):
1015 """Test that a crash_reporter symbol file is held to higher standards"""
1016
1017 class CrashReporterSymbolFileTest:
1018 """Defines the subtest for an invalid crash_reporter symbol file."""
1019
1020 def __init__(
1021 self,
1022 name,
1023 expected_error,
1024 func_lines=2000,
1025 stack_lines=2000,
1026 line_number_lines=20000,
1027 ):
1028 self.name = name
1029 self.expected_error = expected_error
1030 self.func_lines = func_lines
1031 self.stack_lines = stack_lines
1032 self.line_number_lines = line_number_lines
1033
1034 CRASH_REPORTER_SYMBOL_TESTS = [
1035 CrashReporterSymbolFileTest(
1036 name="Insufficient FUNC records",
1037 func_lines=500,
1038 expected_error="crash_reporter should have at least 1000 FUNC "
1039 "records, found 500",
1040 ),
1041 CrashReporterSymbolFileTest(
1042 name="Insufficient STACK records",
1043 stack_lines=100,
1044 expected_error="crash_reporter should have at least 1000 STACK "
1045 "records, found 100",
1046 ),
1047 CrashReporterSymbolFileTest(
1048 name="Insufficient line number records",
1049 line_number_lines=2000,
1050 expected_error="crash_reporter should have at least 10,000 "
1051 "line number records, found 2000",
1052 ),
1053 ]
1054 for test in CRASH_REPORTER_SYMBOL_TESTS:
1055 with self.subTest(
1056 name=test.name
1057 ), multiprocessing.Manager() as mp_manager:
1058 sym_file = self.tempdir / "crash_reporter.sym"
1059 self._CreateSymbolFile(
1060 sym_file,
1061 func_lines=test.func_lines,
1062 stack_lines=test.stack_lines,
1063 line_number_lines=test.line_number_lines,
1064 )
1065 found_files = mp_manager.list()
1066 with self.assertLogs(level=logging.WARNING) as cm:
1067 self.assertFalse(
1068 cros_generate_breakpad_symbols.ValidateSymbolFile(
1069 str(sym_file),
1070 "/build/board/sbin/crash_reporter",
1071 "/build/board",
1072 found_files,
1073 )
1074 )
1075 self.assertIn(test.expected_error, cm.output[0])
1076 self.assertEqual(len(cm.output), 1)
1077
1078 def testValidLibMetricsSymbolFile(self):
1079 """Test a libmetrics.so symbol file can pass the additional checks."""
1080 with multiprocessing.Manager() as mp_manager:
1081 sym_file = self.tempdir / "libmetrics.so.sym"
1082 self._CreateSymbolFile(
1083 sym_file,
1084 func_lines=200,
1085 public_lines=2,
1086 stack_lines=2000,
1087 line_number_lines=10000,
1088 )
1089 found_files = mp_manager.list()
1090 self.assertTrue(
1091 cros_generate_breakpad_symbols.ValidateSymbolFile(
1092 str(sym_file),
1093 "/build/board/usr/lib64/libmetrics.so",
1094 "/build/board",
1095 found_files,
1096 )
1097 )
1098 self.assertEqual(
1099 list(found_files),
1100 [cros_generate_breakpad_symbols.ExpectedFiles.LIBMETRICS],
1101 )
1102
1103 def testInvalidLibMetricsSymbolFile(self):
1104 """Test that a libmetrics.so symbol file is held to higher standards."""
1105
1106 class LibMetricsSymbolFileTest:
1107 """Defines the subtest for an invalid libmetrics.so symbol file."""
1108
1109 def __init__(
1110 self,
1111 name,
1112 expected_error,
1113 func_lines=200,
1114 public_lines=2,
1115 stack_lines=2000,
1116 line_number_lines=10000,
1117 ):
1118 self.name = name
1119 self.expected_error = expected_error
1120 self.func_lines = func_lines
1121 self.public_lines = public_lines
1122 self.stack_lines = stack_lines
1123 self.line_number_lines = line_number_lines
1124
1125 LIBMETRICS_SYMBOL_TESTS = [
1126 LibMetricsSymbolFileTest(
1127 name="Insufficient FUNC records",
1128 func_lines=10,
1129 expected_error="libmetrics should have at least 100 FUNC "
1130 "records, found 10",
1131 ),
1132 LibMetricsSymbolFileTest(
1133 name="Insufficient PUBLIC records",
1134 public_lines=0,
1135 expected_error="libmetrics should have at least 1 PUBLIC "
1136 "record, found 0",
1137 ),
1138 LibMetricsSymbolFileTest(
1139 name="Insufficient STACK records",
1140 stack_lines=500,
1141 expected_error="libmetrics should have at least 1000 STACK "
1142 "records, found 500",
1143 ),
1144 LibMetricsSymbolFileTest(
1145 name="Insufficient line number records",
1146 line_number_lines=2000,
1147 expected_error="libmetrics should have at least 5000 "
1148 "line number records, found 2000",
1149 ),
1150 ]
1151 for test in LIBMETRICS_SYMBOL_TESTS:
1152 with self.subTest(
1153 name=test.name
1154 ), multiprocessing.Manager() as mp_manager:
1155 sym_file = self.tempdir / "libmetrics.so.sym"
1156 self._CreateSymbolFile(
1157 sym_file,
1158 func_lines=test.func_lines,
1159 public_lines=test.public_lines,
1160 stack_lines=test.stack_lines,
1161 line_number_lines=test.line_number_lines,
1162 )
1163 found_files = mp_manager.list()
1164 with self.assertLogs(level=logging.WARNING) as cm:
1165 self.assertFalse(
1166 cros_generate_breakpad_symbols.ValidateSymbolFile(
1167 str(sym_file),
1168 "/build/board/usr/lib64/libmetrics.so",
1169 "/build/board",
1170 found_files,
1171 )
1172 )
1173 self.assertIn(test.expected_error, cm.output[0])
1174 self.assertEqual(len(cm.output), 1)
1175
1176
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001177class UtilsTestDir(cros_test_lib.TempDirTestCase):
Alex Klein1699fab2022-09-08 08:46:06 -06001178 """Tests ReadSymsHeader."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001179
Alex Klein1699fab2022-09-08 08:46:06 -06001180 def testReadSymsHeaderGoodFile(self):
1181 """Make sure ReadSymsHeader can parse sym files"""
1182 sym_file = os.path.join(self.tempdir, "sym")
1183 osutils.WriteFile(sym_file, "MODULE Linux x86 s0m31D chrooome")
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001184 result = cros_generate_breakpad_symbols.ReadSymsHeader(
1185 sym_file, "unused_elfname"
1186 )
Alex Klein1699fab2022-09-08 08:46:06 -06001187 self.assertEqual(result.cpu, "x86")
1188 self.assertEqual(result.id, "s0m31D")
1189 self.assertEqual(result.name, "chrooome")
1190 self.assertEqual(result.os, "Linux")
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001191
1192
1193class UtilsTest(cros_test_lib.TestCase):
Alex Klein1699fab2022-09-08 08:46:06 -06001194 """Tests ReadSymsHeader."""
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001195
Alex Klein1699fab2022-09-08 08:46:06 -06001196 def testReadSymsHeaderGoodBuffer(self):
1197 """Make sure ReadSymsHeader can parse sym file handles"""
1198 result = cros_generate_breakpad_symbols.ReadSymsHeader(
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001199 io.BytesIO(b"MODULE Linux arm MY-ID-HERE blkid"), "unused_elfname"
Alex Klein1699fab2022-09-08 08:46:06 -06001200 )
1201 self.assertEqual(result.cpu, "arm")
1202 self.assertEqual(result.id, "MY-ID-HERE")
1203 self.assertEqual(result.name, "blkid")
1204 self.assertEqual(result.os, "Linux")
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001205
Alex Klein1699fab2022-09-08 08:46:06 -06001206 def testReadSymsHeaderBadd(self):
1207 """Make sure ReadSymsHeader throws on bad sym files"""
1208 self.assertRaises(
1209 ValueError,
1210 cros_generate_breakpad_symbols.ReadSymsHeader,
1211 io.BytesIO(b"asdf"),
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -08001212 "unused_elfname",
Alex Klein1699fab2022-09-08 08:46:06 -06001213 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001214
Alex Klein1699fab2022-09-08 08:46:06 -06001215 def testBreakpadDir(self):
1216 """Make sure board->breakpad path expansion works"""
1217 expected = "/build/blah/usr/lib/debug/breakpad"
1218 result = cros_generate_breakpad_symbols.FindBreakpadDir("blah")
1219 self.assertEqual(expected, result)
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001220
Alex Klein1699fab2022-09-08 08:46:06 -06001221 def testDebugDir(self):
1222 """Make sure board->debug path expansion works"""
1223 expected = "/build/blah/usr/lib/debug"
1224 result = cros_generate_breakpad_symbols.FindDebugDir("blah")
1225 self.assertEqual(expected, result)
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001226
1227
Mike Frysingerea838d12014-12-08 11:55:32 -05001228def main(_argv):
Alex Klein1699fab2022-09-08 08:46:06 -06001229 # pylint: disable=protected-access
1230 # Set timeouts small so that if the unit test hangs, it won't hang for long.
1231 parallel._BackgroundTask.STARTUP_TIMEOUT = 5
1232 parallel._BackgroundTask.EXIT_TIMEOUT = 5
Mike Frysinger69cb41d2013-08-11 20:08:19 -04001233
Alex Klein1699fab2022-09-08 08:46:06 -06001234 # Run the tests.
1235 cros_test_lib.main(level="info", module=__name__)