xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/werror_logs_test.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1#!/usr/bin/env python3
2# Copyright 2024 The ChromiumOS Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Tests for werror_logs.py."""
7
8import io
9import logging
10import os
11from pathlib import Path
12import shutil
13import subprocess
14import tempfile
15import textwrap
16from typing import Dict
17import unittest
18from unittest import mock
19
20import werror_logs
21
22
23class SilenceLogs:
24    """Used by Test.silence_logs to ignore all logging output."""
25
26    def filter(self, _record):
27        return False
28
29
30def create_warning_info(packages: Dict[str, int]) -> werror_logs.WarningInfo:
31    """Constructs a WarningInfo conveniently in one line.
32
33    Mostly useful because `WarningInfo` has a defaultdict field, and those
34    don't `assertEqual` to regular dict fields.
35    """
36    x = werror_logs.WarningInfo()
37    x.packages.update(packages)
38    return x
39
40
41class Test(unittest.TestCase):
42    """Tests for werror_logs."""
43
44    def silence_logs(self):
45        f = SilenceLogs()
46        log = logging.getLogger()
47        log.addFilter(f)
48        self.addCleanup(log.removeFilter, f)
49
50    def make_tempdir(self) -> Path:
51        tempdir = tempfile.mkdtemp("werror_logs_test_")
52        self.addCleanup(shutil.rmtree, tempdir)
53        return Path(tempdir)
54
55    def test_clang_warning_parsing_parses_flag_errors(self):
56        self.assertEqual(
57            werror_logs.ClangWarning.try_parse_line(
58                "clang-17: error: optimization flag -foo is not supported "
59                "[-Werror,-Wfoo]"
60            ),
61            werror_logs.ClangWarning(
62                name="-Wfoo",
63                message="optimization flag -foo is not supported",
64                location=None,
65            ),
66        )
67
68    def test_clang_warning_parsing_doesnt_care_about_werror_order(self):
69        self.assertEqual(
70            werror_logs.ClangWarning.try_parse_line(
71                "clang-17: error: optimization flag -foo is not supported "
72                "[-Wfoo,-Werror]"
73            ),
74            werror_logs.ClangWarning(
75                name="-Wfoo",
76                message="optimization flag -foo is not supported",
77                location=None,
78            ),
79        )
80
81    def test_clang_warning_parsing_parses_code_errors(self):
82        self.assertEqual(
83            werror_logs.ClangWarning.try_parse_line(
84                "/path/to/foo/bar/baz.cc:12:34: error: don't do this "
85                "[-Werror,-Wbar]"
86            ),
87            werror_logs.ClangWarning(
88                name="-Wbar",
89                message="don't do this",
90                location=werror_logs.ClangWarningLocation(
91                    file="/path/to/foo/bar/baz.cc",
92                    line=12,
93                    column=34,
94                ),
95            ),
96        )
97
98    def test_clang_warning_parsing_parses_implicit_errors(self):
99        self.assertEqual(
100            werror_logs.ClangWarning.try_parse_line(
101                # N.B., "-Werror" is missing in this message
102                "/path/to/foo/bar/baz.cc:12:34: error: don't do this "
103                "[-Wbar]"
104            ),
105            werror_logs.ClangWarning(
106                name="-Wbar",
107                message="don't do this",
108                location=werror_logs.ClangWarningLocation(
109                    file="/path/to/foo/bar/baz.cc",
110                    line=12,
111                    column=34,
112                ),
113            ),
114        )
115
116    def test_clang_warning_parsing_canonicalizes_correctly(self):
117        canonical_forms = (
118            ("/build/foo/bar/baz.cc", "/build/{board}/bar/baz.cc"),
119            ("///build//foo///bar//baz.cc", "/build/{board}/bar/baz.cc"),
120            ("/build/baz.cc", "/build/baz.cc"),
121            ("/build.cc", "/build.cc"),
122            (".", "."),
123        )
124
125        for before, after in canonical_forms:
126            self.assertEqual(
127                werror_logs.ClangWarning.try_parse_line(
128                    f"{before}:12:34: error: don't do this [-Werror,-Wbar]",
129                    canonicalize_board_root=True,
130                ),
131                werror_logs.ClangWarning(
132                    name="-Wbar",
133                    message="don't do this",
134                    location=werror_logs.ClangWarningLocation(
135                        file=after,
136                        line=12,
137                        column=34,
138                    ),
139                ),
140            )
141
142    def test_clang_warning_parsing_doesnt_canonicalize_if_not_asked(self):
143        self.assertEqual(
144            werror_logs.ClangWarning.try_parse_line(
145                "/build/foo/bar/baz.cc:12:34: error: don't do this "
146                "[-Werror,-Wbar]",
147                canonicalize_board_root=False,
148            ),
149            werror_logs.ClangWarning(
150                name="-Wbar",
151                message="don't do this",
152                location=werror_logs.ClangWarningLocation(
153                    file="/build/foo/bar/baz.cc",
154                    line=12,
155                    column=34,
156                ),
157            ),
158        )
159
160    def test_clang_warning_parsing_skips_uninteresting_lines(self):
161        self.silence_logs()
162
163        pointless = (
164            "",
165            "foo",
166            "error: something's wrong",
167            "clang-14: warning: something's wrong [-Wsomething]",
168            "clang-14: error: something's wrong",
169        )
170        for line in pointless:
171            self.assertIsNone(
172                werror_logs.ClangWarning.try_parse_line(line), line
173            )
174
175    def test_aggregation_correctly_scrapes_warnings(self):
176        aggregated = werror_logs.AggregatedWarnings()
177        aggregated.add_report_json(
178            {
179                "cwd": "/var/tmp/portage/sys-devel/llvm/foo/bar",
180                "stdout": textwrap.dedent(
181                    """\
182                    Foo
183                    clang-17: error: failed to blah [-Werror,-Wblah]
184                    /path/to/file.cc:1:2: error: other error [-Werror,-Wother]
185                    """
186                ),
187            }
188        )
189        aggregated.add_report_json(
190            {
191                "cwd": "/var/tmp/portage/sys-devel/llvm/foo/bar",
192                "stdout": textwrap.dedent(
193                    """\
194                    Foo
195                    clang-17: error: failed to blah [-Werror,-Wblah]
196                    /path/to/file.cc:1:3: error: other error [-Werror,-Wother]
197                    Bar
198                    """
199                ),
200            }
201        )
202
203        self.assertEqual(aggregated.num_reports, 2)
204        self.assertEqual(
205            dict(aggregated.warnings),
206            {
207                werror_logs.ClangWarning(
208                    name="-Wblah",
209                    message="failed to blah",
210                    location=None,
211                ): create_warning_info(
212                    packages={"sys-devel/llvm": 2},
213                ),
214                werror_logs.ClangWarning(
215                    name="-Wother",
216                    message="other error",
217                    location=werror_logs.ClangWarningLocation(
218                        file="/path/to/file.cc",
219                        line=1,
220                        column=2,
221                    ),
222                ): create_warning_info(
223                    packages={"sys-devel/llvm": 1},
224                ),
225                werror_logs.ClangWarning(
226                    name="-Wother",
227                    message="other error",
228                    location=werror_logs.ClangWarningLocation(
229                        file="/path/to/file.cc",
230                        line=1,
231                        column=3,
232                    ),
233                ): create_warning_info(
234                    packages={"sys-devel/llvm": 1},
235                ),
236            },
237        )
238
239    def test_aggregation_guesses_packages_correctly(self):
240        aggregated = werror_logs.AggregatedWarnings()
241        cwds = (
242            "/var/tmp/portage/sys-devel/llvm/foo/bar",
243            "/var/cache/portage/sys-devel/llvm/foo/bar",
244            "/build/amd64-host/var/tmp/portage/sys-devel/llvm/foo/bar",
245            "/build/amd64-host/var/cache/portage/sys-devel/llvm/foo/bar",
246        )
247        for d in cwds:
248            # If the directory isn't recognized, this will raise.
249            aggregated.add_report_json(
250                {
251                    "cwd": d,
252                    "stdout": "clang-17: error: foo [-Werror,-Wfoo]",
253                }
254            )
255
256        self.assertEqual(len(aggregated.warnings), 1)
257        warning, warning_info = next(iter(aggregated.warnings.items()))
258        self.assertEqual(warning.name, "-Wfoo")
259        self.assertEqual(
260            warning_info, create_warning_info({"sys-devel/llvm": len(cwds)})
261        )
262
263    def test_aggregation_raises_if_package_name_cant_be_guessed(self):
264        aggregated = werror_logs.AggregatedWarnings()
265        with self.assertRaises(werror_logs.UnknownPackageNameError):
266            aggregated.add_report_json({})
267
268    def test_warning_by_flag_summarization_works_in_simple_case(self):
269        string_io = io.StringIO()
270        werror_logs.summarize_warnings_by_flag(
271            {
272                werror_logs.ClangWarning(
273                    name="-Wother",
274                    message="other error",
275                    location=werror_logs.ClangWarningLocation(
276                        file="/path/to/some/file.cc",
277                        line=1,
278                        column=2,
279                    ),
280                ): create_warning_info(
281                    {
282                        "sys-devel/llvm": 3000,
283                        "sys-devel/gcc": 1,
284                    }
285                ),
286                werror_logs.ClangWarning(
287                    name="-Wother",
288                    message="other error",
289                    location=werror_logs.ClangWarningLocation(
290                        file="/path/to/some/file.cc",
291                        line=1,
292                        column=3,
293                    ),
294                ): create_warning_info(
295                    {
296                        "sys-devel/llvm": 1,
297                    }
298                ),
299            },
300            file=string_io,
301        )
302        result = string_io.getvalue()
303        self.assertEqual(
304            result,
305            textwrap.dedent(
306                """\
307                ## Instances of each fatal warning:
308                \t-Wother: 3,002
309                """
310            ),
311        )
312
313    def test_warning_by_package_summarization_works_in_simple_case(self):
314        string_io = io.StringIO()
315        werror_logs.summarize_per_package_warnings(
316            (
317                create_warning_info(
318                    {
319                        "sys-devel/llvm": 3000,
320                        "sys-devel/gcc": 1,
321                    }
322                ),
323                create_warning_info(
324                    {
325                        "sys-devel/llvm": 1,
326                    }
327                ),
328            ),
329            file=string_io,
330        )
331        result = string_io.getvalue()
332        self.assertEqual(
333            result,
334            textwrap.dedent(
335                """\
336                ## Per-package warning counts:
337                \tsys-devel/llvm: 3,001
338                \t sys-devel/gcc:     1
339                """
340            ),
341        )
342
343    def test_cq_builder_determination_works(self):
344        self.assertEqual(
345            werror_logs.cq_builder_name_from_werror_logs_path(
346                "gs://chromeos-image-archive/staryu-cq/"
347                "R123-15771.0.0-94466-8756713501925941617/"
348                "staryu.20240207.fatal_clang_warnings.tar.xz"
349            ),
350            "staryu-cq",
351        )
352
353    @mock.patch.object(subprocess, "run")
354    def test_tarball_downloading_works(self, run_mock):
355        tempdir = self.make_tempdir()
356        unpack_dir = tempdir / "unpack"
357        download_dir = tempdir / "download"
358
359        gs_urls = [
360            "gs://foo/bar-cq/build-number/123.fatal_clang_warnings.tar.xz",
361            "gs://foo/baz-cq/build-number/124.fatal_clang_warnings.tar.xz",
362            "gs://foo/qux-cq/build-number/125.fatal_clang_warnings.tar.xz",
363        ]
364        named_gs_urls = [
365            (werror_logs.cq_builder_name_from_werror_logs_path(x), x)
366            for x in gs_urls
367        ]
368        werror_logs.download_and_unpack_werror_tarballs(
369            unpack_dir, download_dir, gs_urls
370        )
371
372        # Just verify that this executed the correct commands. Normally this is
373        # a bit fragile, but given that this function internally is pretty
374        # complex (starting up a threadpool, etc), extra checking is nice.
375        want_gsutil_commands = [
376            [
377                "gsutil",
378                "cp",
379                gs_url,
380                download_dir / name / os.path.basename(gs_url),
381            ]
382            for name, gs_url in named_gs_urls
383        ]
384        want_untar_commands = [
385            ["tar", "xaf", gsutil_command[-1]]
386            for gsutil_command in want_gsutil_commands
387        ]
388
389        cmds = []
390        for call_args in run_mock.call_args_list:
391            call_positional_args = call_args[0]
392            cmd = call_positional_args[0]
393            cmds.append(cmd)
394        cmds.sort()
395        self.assertEqual(
396            cmds, sorted(want_gsutil_commands + want_untar_commands)
397        )
398
399    @mock.patch.object(subprocess, "run")
400    def test_tarball_downloading_fails_if_exceptions_are_raised(self, run_mock):
401        self.silence_logs()
402
403        def raise_exception(*_args, check=False, **_kwargs):
404            self.assertTrue(check)
405            raise subprocess.CalledProcessError(returncode=1, cmd=[])
406
407        run_mock.side_effect = raise_exception
408        tempdir = self.make_tempdir()
409        unpack_dir = tempdir / "unpack"
410        download_dir = tempdir / "download"
411
412        gs_urls = [
413            "gs://foo/bar-cq/build-number/123.fatal_clang_warnings.tar.xz",
414            "gs://foo/baz-cq/build-number/124.fatal_clang_warnings.tar.xz",
415            "gs://foo/qux-cq/build-number/125.fatal_clang_warnings.tar.xz",
416        ]
417        with self.assertRaisesRegex(ValueError, r"3 download\(s\) failed"):
418            werror_logs.download_and_unpack_werror_tarballs(
419                unpack_dir, download_dir, gs_urls
420            )
421        self.assertEqual(run_mock.call_count, 3)
422
423
424if __name__ == "__main__":
425    unittest.main()
426