blob: c6489389399936e193667965a6ea7529a9bd2eae [file] [log] [blame]
#!/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()