| #!/usr/bin/env python3 |
| # Copyright 2024 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Tests for werror_logs.py.""" |
| |
| import io |
| import logging |
| import os |
| from pathlib import Path |
| import shutil |
| import subprocess |
| import tempfile |
| import textwrap |
| from typing import Dict |
| import unittest |
| from unittest import mock |
| |
| import werror_logs |
| |
| |
| class SilenceLogs: |
| """Used by Test.silence_logs to ignore all logging output.""" |
| |
| def filter(self, _record): |
| return False |
| |
| |
| def create_warning_info(packages: Dict[str, int]) -> werror_logs.WarningInfo: |
| """Constructs a WarningInfo conveniently in one line. |
| |
| Mostly useful because `WarningInfo` has a defaultdict field, and those |
| don't `assertEqual` to regular dict fields. |
| """ |
| x = werror_logs.WarningInfo() |
| x.packages.update(packages) |
| return x |
| |
| |
| class Test(unittest.TestCase): |
| """Tests for werror_logs.""" |
| |
| def silence_logs(self): |
| f = SilenceLogs() |
| log = logging.getLogger() |
| log.addFilter(f) |
| self.addCleanup(log.removeFilter, f) |
| |
| def make_tempdir(self) -> Path: |
| tempdir = tempfile.mkdtemp("werror_logs_test_") |
| self.addCleanup(shutil.rmtree, tempdir) |
| return Path(tempdir) |
| |
| def test_clang_warning_parsing_parses_flag_errors(self): |
| self.assertEqual( |
| werror_logs.ClangWarning.try_parse_line( |
| "clang-17: error: optimization flag -foo is not supported " |
| "[-Werror,-Wfoo]" |
| ), |
| werror_logs.ClangWarning( |
| name="-Wfoo", |
| message="optimization flag -foo is not supported", |
| location=None, |
| ), |
| ) |
| |
| def test_clang_warning_parsing_doesnt_care_about_werror_order(self): |
| self.assertEqual( |
| werror_logs.ClangWarning.try_parse_line( |
| "clang-17: error: optimization flag -foo is not supported " |
| "[-Wfoo,-Werror]" |
| ), |
| werror_logs.ClangWarning( |
| name="-Wfoo", |
| message="optimization flag -foo is not supported", |
| location=None, |
| ), |
| ) |
| |
| def test_clang_warning_parsing_parses_code_errors(self): |
| self.assertEqual( |
| werror_logs.ClangWarning.try_parse_line( |
| "/path/to/foo/bar/baz.cc:12:34: error: don't do this " |
| "[-Werror,-Wbar]" |
| ), |
| werror_logs.ClangWarning( |
| name="-Wbar", |
| message="don't do this", |
| location=werror_logs.ClangWarningLocation( |
| file="/path/to/foo/bar/baz.cc", |
| line=12, |
| column=34, |
| ), |
| ), |
| ) |
| |
| def test_clang_warning_parsing_parses_implicit_errors(self): |
| self.assertEqual( |
| werror_logs.ClangWarning.try_parse_line( |
| # N.B., "-Werror" is missing in this message |
| "/path/to/foo/bar/baz.cc:12:34: error: don't do this " |
| "[-Wbar]" |
| ), |
| werror_logs.ClangWarning( |
| name="-Wbar", |
| message="don't do this", |
| location=werror_logs.ClangWarningLocation( |
| file="/path/to/foo/bar/baz.cc", |
| line=12, |
| column=34, |
| ), |
| ), |
| ) |
| |
| def test_clang_warning_parsing_canonicalizes_correctly(self): |
| canonical_forms = ( |
| ("/build/foo/bar/baz.cc", "/build/{board}/bar/baz.cc"), |
| ("///build//foo///bar//baz.cc", "/build/{board}/bar/baz.cc"), |
| ("/build/baz.cc", "/build/baz.cc"), |
| ("/build.cc", "/build.cc"), |
| (".", "."), |
| ) |
| |
| for before, after in canonical_forms: |
| self.assertEqual( |
| werror_logs.ClangWarning.try_parse_line( |
| f"{before}:12:34: error: don't do this [-Werror,-Wbar]", |
| canonicalize_board_root=True, |
| ), |
| werror_logs.ClangWarning( |
| name="-Wbar", |
| message="don't do this", |
| location=werror_logs.ClangWarningLocation( |
| file=after, |
| line=12, |
| column=34, |
| ), |
| ), |
| ) |
| |
| def test_clang_warning_parsing_doesnt_canonicalize_if_not_asked(self): |
| self.assertEqual( |
| werror_logs.ClangWarning.try_parse_line( |
| "/build/foo/bar/baz.cc:12:34: error: don't do this " |
| "[-Werror,-Wbar]", |
| canonicalize_board_root=False, |
| ), |
| werror_logs.ClangWarning( |
| name="-Wbar", |
| message="don't do this", |
| location=werror_logs.ClangWarningLocation( |
| file="/build/foo/bar/baz.cc", |
| line=12, |
| column=34, |
| ), |
| ), |
| ) |
| |
| def test_clang_warning_parsing_skips_uninteresting_lines(self): |
| self.silence_logs() |
| |
| pointless = ( |
| "", |
| "foo", |
| "error: something's wrong", |
| "clang-14: warning: something's wrong [-Wsomething]", |
| "clang-14: error: something's wrong", |
| ) |
| for line in pointless: |
| self.assertIsNone( |
| werror_logs.ClangWarning.try_parse_line(line), line |
| ) |
| |
| def test_aggregation_correctly_scrapes_warnings(self): |
| aggregated = werror_logs.AggregatedWarnings() |
| aggregated.add_report_json( |
| { |
| "cwd": "/var/tmp/portage/sys-devel/llvm/foo/bar", |
| "stdout": textwrap.dedent( |
| """\ |
| Foo |
| clang-17: error: failed to blah [-Werror,-Wblah] |
| /path/to/file.cc:1:2: error: other error [-Werror,-Wother] |
| """ |
| ), |
| } |
| ) |
| aggregated.add_report_json( |
| { |
| "cwd": "/var/tmp/portage/sys-devel/llvm/foo/bar", |
| "stdout": textwrap.dedent( |
| """\ |
| Foo |
| clang-17: error: failed to blah [-Werror,-Wblah] |
| /path/to/file.cc:1:3: error: other error [-Werror,-Wother] |
| Bar |
| """ |
| ), |
| } |
| ) |
| |
| self.assertEqual(aggregated.num_reports, 2) |
| self.assertEqual( |
| dict(aggregated.warnings), |
| { |
| werror_logs.ClangWarning( |
| name="-Wblah", |
| message="failed to blah", |
| location=None, |
| ): create_warning_info( |
| packages={"sys-devel/llvm": 2}, |
| ), |
| werror_logs.ClangWarning( |
| name="-Wother", |
| message="other error", |
| location=werror_logs.ClangWarningLocation( |
| file="/path/to/file.cc", |
| line=1, |
| column=2, |
| ), |
| ): create_warning_info( |
| packages={"sys-devel/llvm": 1}, |
| ), |
| werror_logs.ClangWarning( |
| name="-Wother", |
| message="other error", |
| location=werror_logs.ClangWarningLocation( |
| file="/path/to/file.cc", |
| line=1, |
| column=3, |
| ), |
| ): create_warning_info( |
| packages={"sys-devel/llvm": 1}, |
| ), |
| }, |
| ) |
| |
| def test_aggregation_guesses_packages_correctly(self): |
| aggregated = werror_logs.AggregatedWarnings() |
| cwds = ( |
| "/var/tmp/portage/sys-devel/llvm/foo/bar", |
| "/var/cache/portage/sys-devel/llvm/foo/bar", |
| "/build/amd64-host/var/tmp/portage/sys-devel/llvm/foo/bar", |
| "/build/amd64-host/var/cache/portage/sys-devel/llvm/foo/bar", |
| ) |
| for d in cwds: |
| # If the directory isn't recognized, this will raise. |
| aggregated.add_report_json( |
| { |
| "cwd": d, |
| "stdout": "clang-17: error: foo [-Werror,-Wfoo]", |
| } |
| ) |
| |
| self.assertEqual(len(aggregated.warnings), 1) |
| warning, warning_info = next(iter(aggregated.warnings.items())) |
| self.assertEqual(warning.name, "-Wfoo") |
| self.assertEqual( |
| warning_info, create_warning_info({"sys-devel/llvm": len(cwds)}) |
| ) |
| |
| def test_aggregation_raises_if_package_name_cant_be_guessed(self): |
| aggregated = werror_logs.AggregatedWarnings() |
| with self.assertRaises(werror_logs.UnknownPackageNameError): |
| aggregated.add_report_json({}) |
| |
| def test_warning_by_flag_summarization_works_in_simple_case(self): |
| string_io = io.StringIO() |
| werror_logs.summarize_warnings_by_flag( |
| { |
| werror_logs.ClangWarning( |
| name="-Wother", |
| message="other error", |
| location=werror_logs.ClangWarningLocation( |
| file="/path/to/some/file.cc", |
| line=1, |
| column=2, |
| ), |
| ): create_warning_info( |
| { |
| "sys-devel/llvm": 3000, |
| "sys-devel/gcc": 1, |
| } |
| ), |
| werror_logs.ClangWarning( |
| name="-Wother", |
| message="other error", |
| location=werror_logs.ClangWarningLocation( |
| file="/path/to/some/file.cc", |
| line=1, |
| column=3, |
| ), |
| ): create_warning_info( |
| { |
| "sys-devel/llvm": 1, |
| } |
| ), |
| }, |
| file=string_io, |
| ) |
| result = string_io.getvalue() |
| self.assertEqual( |
| result, |
| textwrap.dedent( |
| """\ |
| ## Instances of each fatal warning: |
| \t-Wother: 3,002 |
| """ |
| ), |
| ) |
| |
| def test_warning_by_package_summarization_works_in_simple_case(self): |
| string_io = io.StringIO() |
| werror_logs.summarize_per_package_warnings( |
| ( |
| create_warning_info( |
| { |
| "sys-devel/llvm": 3000, |
| "sys-devel/gcc": 1, |
| } |
| ), |
| create_warning_info( |
| { |
| "sys-devel/llvm": 1, |
| } |
| ), |
| ), |
| file=string_io, |
| ) |
| result = string_io.getvalue() |
| self.assertEqual( |
| result, |
| textwrap.dedent( |
| """\ |
| ## Per-package warning counts: |
| \tsys-devel/llvm: 3,001 |
| \t sys-devel/gcc: 1 |
| """ |
| ), |
| ) |
| |
| def test_cq_builder_determination_works(self): |
| self.assertEqual( |
| werror_logs.cq_builder_name_from_werror_logs_path( |
| "gs://chromeos-image-archive/staryu-cq/" |
| "R123-15771.0.0-94466-8756713501925941617/" |
| "staryu.20240207.fatal_clang_warnings.tar.xz" |
| ), |
| "staryu-cq", |
| ) |
| |
| @mock.patch.object(subprocess, "run") |
| def test_tarball_downloading_works(self, run_mock): |
| tempdir = self.make_tempdir() |
| unpack_dir = tempdir / "unpack" |
| download_dir = tempdir / "download" |
| |
| gs_urls = [ |
| "gs://foo/bar-cq/build-number/123.fatal_clang_warnings.tar.xz", |
| "gs://foo/baz-cq/build-number/124.fatal_clang_warnings.tar.xz", |
| "gs://foo/qux-cq/build-number/125.fatal_clang_warnings.tar.xz", |
| ] |
| named_gs_urls = [ |
| (werror_logs.cq_builder_name_from_werror_logs_path(x), x) |
| for x in gs_urls |
| ] |
| werror_logs.download_and_unpack_werror_tarballs( |
| unpack_dir, download_dir, gs_urls |
| ) |
| |
| # Just verify that this executed the correct commands. Normally this is |
| # a bit fragile, but given that this function internally is pretty |
| # complex (starting up a threadpool, etc), extra checking is nice. |
| want_gsutil_commands = [ |
| [ |
| "gsutil", |
| "cp", |
| gs_url, |
| download_dir / name / os.path.basename(gs_url), |
| ] |
| for name, gs_url in named_gs_urls |
| ] |
| want_untar_commands = [ |
| ["tar", "xaf", gsutil_command[-1]] |
| for gsutil_command in want_gsutil_commands |
| ] |
| |
| cmds = [] |
| for call_args in run_mock.call_args_list: |
| call_positional_args = call_args[0] |
| cmd = call_positional_args[0] |
| cmds.append(cmd) |
| cmds.sort() |
| self.assertEqual( |
| cmds, sorted(want_gsutil_commands + want_untar_commands) |
| ) |
| |
| @mock.patch.object(subprocess, "run") |
| def test_tarball_downloading_fails_if_exceptions_are_raised(self, run_mock): |
| self.silence_logs() |
| |
| def raise_exception(*_args, check=False, **_kwargs): |
| self.assertTrue(check) |
| raise subprocess.CalledProcessError(returncode=1, cmd=[]) |
| |
| run_mock.side_effect = raise_exception |
| tempdir = self.make_tempdir() |
| unpack_dir = tempdir / "unpack" |
| download_dir = tempdir / "download" |
| |
| gs_urls = [ |
| "gs://foo/bar-cq/build-number/123.fatal_clang_warnings.tar.xz", |
| "gs://foo/baz-cq/build-number/124.fatal_clang_warnings.tar.xz", |
| "gs://foo/qux-cq/build-number/125.fatal_clang_warnings.tar.xz", |
| ] |
| with self.assertRaisesRegex(ValueError, r"3 download\(s\) failed"): |
| werror_logs.download_and_unpack_werror_tarballs( |
| unpack_dir, download_dir, gs_urls |
| ) |
| self.assertEqual(run_mock.call_count, 3) |
| |
| |
| if __name__ == "__main__": |
| unittest.main() |