diff options
Diffstat (limited to 'meta/lib/oeqa/selftest/cases/debuginfod.py')
-rw-r--r-- | meta/lib/oeqa/selftest/cases/debuginfod.py | 158 |
1 files changed, 158 insertions, 0 deletions
diff --git a/meta/lib/oeqa/selftest/cases/debuginfod.py b/meta/lib/oeqa/selftest/cases/debuginfod.py new file mode 100644 index 0000000000..505b4be837 --- /dev/null +++ b/meta/lib/oeqa/selftest/cases/debuginfod.py | |||
@@ -0,0 +1,158 @@ | |||
1 | # | ||
2 | # Copyright OpenEmbedded Contributors | ||
3 | # | ||
4 | # SPDX-License-Identifier: MIT | ||
5 | # | ||
6 | import os | ||
7 | import socketserver | ||
8 | import subprocess | ||
9 | import time | ||
10 | import urllib | ||
11 | import pathlib | ||
12 | |||
13 | from oeqa.core.decorator import OETestTag | ||
14 | from oeqa.selftest.case import OESelftestTestCase | ||
15 | from oeqa.utils.commands import bitbake, get_bb_var, runqemu | ||
16 | |||
17 | |||
18 | class Debuginfod(OESelftestTestCase): | ||
19 | |||
20 | def wait_for_debuginfod(self, port): | ||
21 | """ | ||
22 | debuginfod takes time to scan the packages and requesting too early may | ||
23 | result in a test failure if the right packages haven't been scanned yet. | ||
24 | |||
25 | Request the metrics endpoint periodically and wait for there to be no | ||
26 | busy scanning threads. | ||
27 | |||
28 | Returns if debuginfod is ready, raises an exception if not within the | ||
29 | timeout. | ||
30 | """ | ||
31 | |||
32 | # Wait two minutes | ||
33 | countdown = 24 | ||
34 | delay = 5 | ||
35 | latest = None | ||
36 | |||
37 | while countdown: | ||
38 | self.logger.info("waiting...") | ||
39 | time.sleep(delay) | ||
40 | |||
41 | self.logger.info("polling server") | ||
42 | if self.debuginfod.poll(): | ||
43 | self.logger.info("server dead") | ||
44 | self.debuginfod.communicate() | ||
45 | self.fail("debuginfod terminated unexpectedly") | ||
46 | self.logger.info("server alive") | ||
47 | |||
48 | try: | ||
49 | with urllib.request.urlopen("http://localhost:%d/metrics" % port, timeout=10) as f: | ||
50 | for line in f.read().decode("ascii").splitlines(): | ||
51 | key, value = line.rsplit(" ", 1) | ||
52 | if key == "thread_busy{role=\"scan\"}": | ||
53 | latest = int(value) | ||
54 | self.logger.info("Waiting for %d scan jobs to finish" % latest) | ||
55 | if latest == 0: | ||
56 | return | ||
57 | except urllib.error.URLError as e: | ||
58 | # TODO: how to catch just timeouts? | ||
59 | self.logger.error(e) | ||
60 | |||
61 | countdown -= 1 | ||
62 | |||
63 | raise TimeoutError("Cannot connect debuginfod, still %d scan jobs running" % latest) | ||
64 | |||
65 | def start_debuginfod(self): | ||
66 | # We assume that the caller has already bitbake'd elfutils-native:do_addto_recipe_sysroot | ||
67 | |||
68 | # Save some useful paths for later | ||
69 | native_sysroot = pathlib.Path(get_bb_var("RECIPE_SYSROOT_NATIVE", "elfutils-native")) | ||
70 | native_bindir = native_sysroot / "usr" / "bin" | ||
71 | self.debuginfod = native_bindir / "debuginfod" | ||
72 | self.debuginfod_find = native_bindir / "debuginfod-find" | ||
73 | |||
74 | cmd = [ | ||
75 | self.debuginfod, | ||
76 | "--verbose", | ||
77 | # In-memory database, this is a one-shot test | ||
78 | "--database=:memory:", | ||
79 | # Don't use all the host cores | ||
80 | "--concurrency=8", | ||
81 | "--connection-pool=8", | ||
82 | # Disable rescanning, this is a one-shot test | ||
83 | "--rescan-time=0", | ||
84 | "--groom-time=0", | ||
85 | get_bb_var("DEPLOY_DIR"), | ||
86 | ] | ||
87 | |||
88 | format = get_bb_var("PACKAGE_CLASSES").split()[0] | ||
89 | if format == "package_deb": | ||
90 | cmd.append("--scan-deb-dir") | ||
91 | elif format == "package_ipk": | ||
92 | cmd.append("--scan-deb-dir") | ||
93 | elif format == "package_rpm": | ||
94 | cmd.append("--scan-rpm-dir") | ||
95 | else: | ||
96 | self.fail("Unknown package class %s" % format) | ||
97 | |||
98 | # Find a free port. Racey but the window is small. | ||
99 | with socketserver.TCPServer(("localhost", 0), None) as s: | ||
100 | self.port = s.server_address[1] | ||
101 | cmd.append("--port=%d" % self.port) | ||
102 | |||
103 | self.logger.info(f"Starting server {cmd}") | ||
104 | self.debuginfod = subprocess.Popen(cmd, env={}) | ||
105 | self.wait_for_debuginfod(self.port) | ||
106 | |||
107 | |||
108 | def test_debuginfod_native(self): | ||
109 | """ | ||
110 | Test debuginfod outside of qemu, by building a package and looking up a | ||
111 | binary's debuginfo using elfutils-native. | ||
112 | """ | ||
113 | |||
114 | self.write_config(""" | ||
115 | TMPDIR = "${TOPDIR}/tmp-debuginfod" | ||
116 | DISTRO_FEATURES:append = " debuginfod" | ||
117 | """) | ||
118 | bitbake("elfutils-native:do_addto_recipe_sysroot xz xz:do_package") | ||
119 | |||
120 | try: | ||
121 | self.start_debuginfod() | ||
122 | |||
123 | env = os.environ.copy() | ||
124 | env["DEBUGINFOD_URLS"] = "http://localhost:%d/" % self.port | ||
125 | |||
126 | pkgs = pathlib.Path(get_bb_var("PKGDEST", "xz")) | ||
127 | cmd = (self.debuginfod_find, "debuginfo", pkgs / "xz" / "usr" / "bin" / "xz.xz") | ||
128 | self.logger.info(f"Starting client {cmd}") | ||
129 | output = subprocess.check_output(cmd, env=env, text=True) | ||
130 | # This should be more comprehensive | ||
131 | self.assertIn("/.cache/debuginfod_client/", output) | ||
132 | finally: | ||
133 | self.debuginfod.kill() | ||
134 | |||
135 | @OETestTag("runqemu") | ||
136 | def test_debuginfod_qemu(self): | ||
137 | """ | ||
138 | Test debuginfod-find inside a qemu, talking to a debuginfod on the host. | ||
139 | """ | ||
140 | |||
141 | self.write_config(""" | ||
142 | TMPDIR = "${TOPDIR}/tmp-debuginfod" | ||
143 | DISTRO_FEATURES:append = " debuginfod" | ||
144 | CORE_IMAGE_EXTRA_INSTALL += "elfutils xz" | ||
145 | """) | ||
146 | bitbake("core-image-minimal elfutils-native:do_addto_recipe_sysroot") | ||
147 | |||
148 | try: | ||
149 | self.start_debuginfod() | ||
150 | |||
151 | with runqemu("core-image-minimal", runqemuparams="nographic") as qemu: | ||
152 | cmd = "DEBUGINFOD_URLS=http://%s:%d/ debuginfod-find debuginfo /usr/bin/xz" % (qemu.server_ip, self.port) | ||
153 | self.logger.info(f"Starting client {cmd}") | ||
154 | status, output = qemu.run_serial(cmd) | ||
155 | # This should be more comprehensive | ||
156 | self.assertIn("/.cache/debuginfod_client/", output) | ||
157 | finally: | ||
158 | self.debuginfod.kill() | ||