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