1#!/usr/bin/env python3 2 3# Copyright (c) 2018,2020 Intel Corporation 4# Copyright (c) 2022 Nordic Semiconductor ASA 5# SPDX-License-Identifier: Apache-2.0 6 7import argparse 8import collections 9from email.utils import parseaddr 10import logging 11import os 12from pathlib import Path 13import re 14import subprocess 15import sys 16import tempfile 17import traceback 18import shlex 19 20from yamllint import config, linter 21 22from junitparser import TestCase, TestSuite, JUnitXml, Skipped, Error, Failure 23import magic 24 25from west.manifest import Manifest 26from west.manifest import ManifestProject 27 28sys.path.insert(0, str(Path(__file__).resolve().parents[1])) 29from get_maintainer import Maintainers, MaintainersError 30 31logger = None 32 33def git(*args, cwd=None, ignore_non_zero=False): 34 # Helper for running a Git command. Returns the rstrip()ed stdout output. 35 # Called like git("diff"). Exits with SystemError (raised by sys.exit()) on 36 # errors if 'ignore_non_zero' is set to False (default: False). 'cwd' is the 37 # working directory to use (default: current directory). 38 39 git_cmd = ("git",) + args 40 try: 41 cp = subprocess.run(git_cmd, capture_output=True, cwd=cwd) 42 except OSError as e: 43 err(f"failed to run '{cmd2str(git_cmd)}': {e}") 44 45 if not ignore_non_zero and (cp.returncode or cp.stderr): 46 err(f"'{cmd2str(git_cmd)}' exited with status {cp.returncode} and/or " 47 f"wrote to stderr.\n" 48 f"==stdout==\n" 49 f"{cp.stdout.decode('utf-8')}\n" 50 f"==stderr==\n" 51 f"{cp.stderr.decode('utf-8')}\n") 52 53 return cp.stdout.decode("utf-8").rstrip() 54 55def get_shas(refspec): 56 """ 57 Returns the list of Git SHAs for 'refspec'. 58 59 :param refspec: 60 :return: 61 """ 62 return git('rev-list', 63 f'--max-count={-1 if "." in refspec else 1}', refspec).split() 64 65def get_files(filter=None, paths=None): 66 filter_arg = (f'--diff-filter={filter}',) if filter else () 67 paths_arg = ('--', *paths) if paths else () 68 out = git('diff', '--name-only', *filter_arg, COMMIT_RANGE, *paths_arg) 69 files = out.splitlines() 70 for file in list(files): 71 if not os.path.isfile(os.path.join(GIT_TOP, file)): 72 # Drop submodule directories from the list. 73 files.remove(file) 74 return files 75 76class FmtdFailure(Failure): 77 78 def __init__(self, severity, title, file, line=None, col=None, desc=""): 79 self.severity = severity 80 self.title = title 81 self.file = file 82 self.line = line 83 self.col = col 84 self.desc = desc 85 description = f':{desc}' if desc else '' 86 msg_body = desc or title 87 88 txt = f'\n{title}{description}\nFile:{file}' + \ 89 (f'\nLine:{line}' if line else '') + \ 90 (f'\nColumn:{col}' if col else '') 91 msg = f'{file}' + (f':{line}' if line else '') + f' {msg_body}' 92 typ = severity.lower() 93 94 super().__init__(msg, typ) 95 96 self.text = txt 97 98 99class ComplianceTest: 100 """ 101 Base class for tests. Inheriting classes should have a run() method and set 102 these class variables: 103 104 name: 105 Test name 106 107 doc: 108 Link to documentation related to what's being tested 109 110 path_hint: 111 The path the test runs itself in. This is just informative and used in 112 the message that gets printed when running the test. 113 114 There are two magic strings that can be used instead of a path: 115 - The magic string "<zephyr-base>" can be used to refer to the 116 environment variable ZEPHYR_BASE or, when missing, the calculated base of 117 the zephyr tree 118 - The magic string "<git-top>" refers to the top-level repository 119 directory. This avoids running 'git' to find the top-level directory 120 before main() runs (class variable assignments run when the 'class ...' 121 statement runs). That avoids swallowing errors, because main() reports 122 them to GitHub 123 """ 124 def __init__(self): 125 self.case = TestCase(type(self).name, "Guidelines") 126 # This is necessary because Failure can be subclassed, but since it is 127 # always restored form the element tree, the subclass is lost upon 128 # restoring 129 self.fmtd_failures = [] 130 131 def _result(self, res, text): 132 res.text = text.rstrip() 133 self.case.result += [res] 134 135 def error(self, text, msg=None, type_="error"): 136 """ 137 Signals a problem with running the test, with message 'msg'. 138 139 Raises an exception internally, so you do not need to put a 'return' 140 after error(). 141 """ 142 err = Error(msg or f'{type(self).name} error', type_) 143 self._result(err, text) 144 145 raise EndTest 146 147 def skip(self, text, msg=None, type_="skip"): 148 """ 149 Signals that the test should be skipped, with message 'msg'. 150 151 Raises an exception internally, so you do not need to put a 'return' 152 after skip(). 153 """ 154 skpd = Skipped(msg or f'{type(self).name} skipped', type_) 155 self._result(skpd, text) 156 157 raise EndTest 158 159 def failure(self, text, msg=None, type_="failure"): 160 """ 161 Signals that the test failed, with message 'msg'. Can be called many 162 times within the same test to report multiple failures. 163 """ 164 fail = Failure(msg or f'{type(self).name} issues', type_) 165 self._result(fail, text) 166 167 def fmtd_failure(self, severity, title, file, line=None, col=None, desc=""): 168 """ 169 Signals that the test failed, and store the information in a formatted 170 standardized manner. Can be called many times within the same test to 171 report multiple failures. 172 """ 173 fail = FmtdFailure(severity, title, file, line, col, desc) 174 self._result(fail, fail.text) 175 self.fmtd_failures.append(fail) 176 177 178class EndTest(Exception): 179 """ 180 Raised by ComplianceTest.error()/skip() to end the test. 181 182 Tests can raise EndTest themselves to immediately end the test, e.g. from 183 within a nested function call. 184 """ 185 186 187class CheckPatch(ComplianceTest): 188 """ 189 Runs checkpatch and reports found issues 190 191 """ 192 name = "Checkpatch" 193 doc = "See https://docs.zephyrproject.org/latest/contribute/guidelines.html#coding-style for more details." 194 path_hint = "<git-top>" 195 196 def run(self): 197 checkpatch = os.path.join(ZEPHYR_BASE, 'scripts', 'checkpatch.pl') 198 if not os.path.exists(checkpatch): 199 self.skip(f'{checkpatch} not found') 200 201 diff = subprocess.Popen(('git', 'diff', COMMIT_RANGE), 202 stdout=subprocess.PIPE, 203 cwd=GIT_TOP) 204 try: 205 subprocess.run((checkpatch, '--mailback', '--no-tree', '-'), 206 check=True, 207 stdin=diff.stdout, 208 stdout=subprocess.PIPE, 209 stderr=subprocess.STDOUT, 210 shell=True, cwd=GIT_TOP) 211 212 except subprocess.CalledProcessError as ex: 213 output = ex.output.decode("utf-8") 214 regex = r'^\s*\S+:(\d+):\s*(ERROR|WARNING):(.+?):(.+)(?:\n|\r\n?)+' \ 215 r'^\s*#(\d+):\s*FILE:\s*(.+):(\d+):' 216 217 matches = re.findall(regex, output, re.MULTILINE) 218 for m in matches: 219 self.fmtd_failure(m[1].lower(), m[2], m[5], m[6], col=None, 220 desc=m[3]) 221 222 # If the regex has not matched add the whole output as a failure 223 if len(matches) == 0: 224 self.failure(output) 225 226 227class DevicetreeBindingsCheck(ComplianceTest): 228 """ 229 Checks if we are introducing any unwanted properties in Devicetree Bindings. 230 """ 231 name = "DevicetreeBindings" 232 doc = "See https://docs.zephyrproject.org/latest/build/dts/bindings.html for more details." 233 path_hint = "<zephyr-base>" 234 235 def run(self, full=True): 236 dts_bindings = self.parse_dt_bindings() 237 238 for dts_binding in dts_bindings: 239 self.required_false_check(dts_binding) 240 241 def parse_dt_bindings(self): 242 """ 243 Returns a list of dts/bindings/**/*.yaml files 244 """ 245 246 dt_bindings = [] 247 for file_name in get_files(filter="d"): 248 if 'dts/bindings/' in file_name and file_name.endswith('.yaml'): 249 dt_bindings.append(file_name) 250 251 return dt_bindings 252 253 def required_false_check(self, dts_binding): 254 with open(dts_binding) as file: 255 line_number = 0 256 for line in file: 257 line_number += 1 258 if 'required: false' in line: 259 self.fmtd_failure( 260 'warning', 'Devicetree Bindings', dts_binding, 261 line_number, col=None, 262 desc="'required: false' is redundant, please remove") 263 264 265class KconfigCheck(ComplianceTest): 266 """ 267 Checks is we are introducing any new warnings/errors with Kconfig, 268 for example using undefined Kconfig variables. 269 """ 270 name = "Kconfig" 271 doc = "See https://docs.zephyrproject.org/latest/build/kconfig/tips.html for more details." 272 path_hint = "<zephyr-base>" 273 274 def run(self, full=True, no_modules=False): 275 self.no_modules = no_modules 276 277 kconf = self.parse_kconfig() 278 279 self.check_top_menu_not_too_long(kconf) 280 self.check_no_pointless_menuconfigs(kconf) 281 self.check_no_undef_within_kconfig(kconf) 282 self.check_no_redefined_in_defconfig(kconf) 283 self.check_no_enable_in_boolean_prompt(kconf) 284 if full: 285 self.check_no_undef_outside_kconfig(kconf) 286 287 def get_modules(self, modules_file): 288 """ 289 Get a list of modules and put them in a file that is parsed by 290 Kconfig 291 292 This is needed to complete Kconfig sanity tests. 293 294 """ 295 if self.no_modules: 296 with open(modules_file, 'w') as fp_module_file: 297 fp_module_file.write("# Empty\n") 298 return 299 300 # Invoke the script directly using the Python executable since this is 301 # not a module nor a pip-installed Python utility 302 zephyr_module_path = os.path.join(ZEPHYR_BASE, "scripts", 303 "zephyr_module.py") 304 cmd = [sys.executable, zephyr_module_path, 305 '--kconfig-out', modules_file] 306 try: 307 subprocess.run(cmd, check=True, stdout=subprocess.PIPE, 308 stderr=subprocess.STDOUT) 309 except subprocess.CalledProcessError as ex: 310 self.error(ex.output.decode("utf-8")) 311 312 modules_dir = ZEPHYR_BASE + '/modules' 313 modules = [name for name in os.listdir(modules_dir) if 314 os.path.exists(os.path.join(modules_dir, name, 'Kconfig'))] 315 316 with open(modules_file, 'r') as fp_module_file: 317 content = fp_module_file.read() 318 319 with open(modules_file, 'w') as fp_module_file: 320 for module in modules: 321 fp_module_file.write("ZEPHYR_{}_KCONFIG = {}\n".format( 322 re.sub('[^a-zA-Z0-9]', '_', module).upper(), 323 modules_dir + '/' + module + '/Kconfig' 324 )) 325 fp_module_file.write(content) 326 327 def get_kconfig_dts(self, kconfig_dts_file): 328 """ 329 Generate the Kconfig.dts using dts/bindings as the source. 330 331 This is needed to complete Kconfig compliance tests. 332 333 """ 334 # Invoke the script directly using the Python executable since this is 335 # not a module nor a pip-installed Python utility 336 zephyr_drv_kconfig_path = os.path.join(ZEPHYR_BASE, "scripts", "dts", 337 "gen_driver_kconfig_dts.py") 338 binding_path = os.path.join(ZEPHYR_BASE, "dts", "bindings") 339 cmd = [sys.executable, zephyr_drv_kconfig_path, 340 '--kconfig-out', kconfig_dts_file, '--bindings-dirs', binding_path] 341 try: 342 subprocess.run(cmd, check=True, stdout=subprocess.PIPE, 343 stderr=subprocess.STDOUT) 344 except subprocess.CalledProcessError as ex: 345 self.error(ex.output.decode("utf-8")) 346 347 348 def parse_kconfig(self): 349 """ 350 Returns a kconfiglib.Kconfig object for the Kconfig files. We reuse 351 this object for all tests to avoid having to reparse for each test. 352 """ 353 # Put the Kconfiglib path first to make sure no local Kconfiglib version is 354 # used 355 kconfig_path = os.path.join(ZEPHYR_BASE, "scripts", "kconfig") 356 if not os.path.exists(kconfig_path): 357 self.error(kconfig_path + " not found") 358 359 sys.path.insert(0, kconfig_path) 360 # Import globally so that e.g. kconfiglib.Symbol can be referenced in 361 # tests 362 global kconfiglib 363 import kconfiglib 364 365 # Look up Kconfig files relative to ZEPHYR_BASE 366 os.environ["srctree"] = ZEPHYR_BASE 367 368 # Parse the entire Kconfig tree, to make sure we see all symbols 369 os.environ["SOC_DIR"] = "soc/" 370 os.environ["ARCH_DIR"] = "arch/" 371 os.environ["BOARD_DIR"] = "boards/*/*" 372 os.environ["ARCH"] = "*" 373 os.environ["KCONFIG_BINARY_DIR"] = tempfile.gettempdir() 374 os.environ['DEVICETREE_CONF'] = "dummy" 375 os.environ['TOOLCHAIN_HAS_NEWLIB'] = "y" 376 377 # Older name for DEVICETREE_CONF, for compatibility with older Zephyr 378 # versions that don't have the renaming 379 os.environ["GENERATED_DTS_BOARD_CONF"] = "dummy" 380 381 # For multi repo support 382 self.get_modules(os.path.join(tempfile.gettempdir(), "Kconfig.modules")) 383 384 # For Kconfig.dts support 385 self.get_kconfig_dts(os.path.join(tempfile.gettempdir(), "Kconfig.dts")) 386 387 # Tells Kconfiglib to generate warnings for all references to undefined 388 # symbols within Kconfig files 389 os.environ["KCONFIG_WARN_UNDEF"] = "y" 390 391 try: 392 # Note this will both print warnings to stderr _and_ return 393 # them: so some warnings might get printed 394 # twice. "warn_to_stderr=False" could unfortunately cause 395 # some (other) warnings to never be printed. 396 return kconfiglib.Kconfig() 397 except kconfiglib.KconfigError as e: 398 self.failure(str(e)) 399 raise EndTest 400 401 def get_defined_syms(self, kconf): 402 # Returns a set() with the names of all defined Kconfig symbols (with no 403 # 'CONFIG_' prefix). This is complicated by samples and tests defining 404 # their own Kconfig trees. For those, just grep for 'config FOO' to find 405 # definitions. Doing it "properly" with Kconfiglib is still useful for 406 # the main tree, because some symbols are defined using preprocessor 407 # macros. 408 409 # Warning: Needs to work with both --perl-regexp and the 're' module. 410 # (?:...) is a non-capturing group. 411 regex = r"^\s*(?:menu)?config\s*([A-Z0-9_]+)\s*(?:#|$)" 412 413 # Grep samples/ and tests/ for symbol definitions 414 grep_stdout = git("grep", "-I", "-h", "--perl-regexp", regex, "--", 415 ":samples", ":tests", cwd=ZEPHYR_BASE) 416 417 # Generate combined list of configs and choices from the main Kconfig tree. 418 kconf_syms = kconf.unique_defined_syms + kconf.unique_choices 419 420 # Symbols from the main Kconfig tree + grepped definitions from samples 421 # and tests 422 return set([sym.name for sym in kconf_syms] 423 + re.findall(regex, grep_stdout, re.MULTILINE)) 424 425 426 def check_top_menu_not_too_long(self, kconf): 427 """ 428 Checks that there aren't too many items in the top-level menu (which 429 might be a sign that stuff accidentally got added there) 430 """ 431 max_top_items = 50 432 433 n_top_items = 0 434 node = kconf.top_node.list 435 while node: 436 # Only count items with prompts. Other items will never be 437 # shown in the menuconfig (outside show-all mode). 438 if node.prompt: 439 n_top_items += 1 440 node = node.next 441 442 if n_top_items > max_top_items: 443 self.failure(f""" 444Expected no more than {max_top_items} potentially visible items (items with 445prompts) in the top-level Kconfig menu, found {n_top_items} items. If you're 446deliberately adding new entries, then bump the 'max_top_items' variable in 447{__file__}.""") 448 449 def check_no_redefined_in_defconfig(self, kconf): 450 # Checks that no symbols are (re)defined in defconfigs. 451 452 for node in kconf.node_iter(): 453 # 'kconfiglib' is global 454 # pylint: disable=undefined-variable 455 if "defconfig" in node.filename and (node.prompt or node.help): 456 name = (node.item.name if node.item not in 457 (kconfiglib.MENU, kconfiglib.COMMENT) else str(node)) 458 self.failure(f""" 459Kconfig node '{name}' found with prompt or help in {node.filename}. 460Options must not be defined in defconfig files. 461""") 462 continue 463 464 def check_no_enable_in_boolean_prompt(self, kconf): 465 # Checks that boolean's prompt does not start with "Enable...". 466 467 for node in kconf.node_iter(): 468 # skip Kconfig nodes not in-tree (will present an absolute path) 469 if os.path.isabs(node.filename): 470 continue 471 472 # 'kconfiglib' is global 473 # pylint: disable=undefined-variable 474 475 # only process boolean symbols with a prompt 476 if (not isinstance(node.item, kconfiglib.Symbol) or 477 node.item.type != kconfiglib.BOOL or 478 not node.prompt or 479 not node.prompt[0]): 480 continue 481 482 if re.match(r"^[Ee]nable.*", node.prompt[0]): 483 self.failure(f""" 484Boolean option '{node.item.name}' prompt must not start with 'Enable...'. Please 485check Kconfig guidelines. 486""") 487 continue 488 489 def check_no_pointless_menuconfigs(self, kconf): 490 # Checks that there are no pointless 'menuconfig' symbols without 491 # children in the Kconfig files 492 493 bad_mconfs = [] 494 for node in kconf.node_iter(): 495 # 'kconfiglib' is global 496 # pylint: disable=undefined-variable 497 498 # Avoid flagging empty regular menus and choices, in case people do 499 # something with 'osource' (could happen for 'menuconfig' symbols 500 # too, though it's less likely) 501 if node.is_menuconfig and not node.list and \ 502 isinstance(node.item, kconfiglib.Symbol): 503 504 bad_mconfs.append(node) 505 506 if bad_mconfs: 507 self.failure("""\ 508Found pointless 'menuconfig' symbols without children. Use regular 'config' 509symbols instead. See 510https://docs.zephyrproject.org/latest/guides/kconfig/tips.html#menuconfig-symbols. 511 512""" + "\n".join(f"{node.item.name:35} {node.filename}:{node.linenr}" 513 for node in bad_mconfs)) 514 515 def check_no_undef_within_kconfig(self, kconf): 516 """ 517 Checks that there are no references to undefined Kconfig symbols within 518 the Kconfig files 519 """ 520 undef_ref_warnings = "\n\n\n".join(warning for warning in kconf.warnings 521 if "undefined symbol" in warning) 522 523 if undef_ref_warnings: 524 self.failure(f"Undefined Kconfig symbols:\n\n {undef_ref_warnings}") 525 526 def check_no_undef_outside_kconfig(self, kconf): 527 """ 528 Checks that there are no references to undefined Kconfig symbols 529 outside Kconfig files (any CONFIG_FOO where no FOO symbol exists) 530 """ 531 # Grep for symbol references. 532 # 533 # Example output line for a reference to CONFIG_FOO at line 17 of 534 # foo/bar.c: 535 # 536 # foo/bar.c<null>17<null>#ifdef CONFIG_FOO 537 # 538 # 'git grep --only-matching' would get rid of the surrounding context 539 # ('#ifdef '), but it was added fairly recently (second half of 2018), 540 # so we extract the references from each line ourselves instead. 541 # 542 # The regex uses word boundaries (\b) to isolate the reference, and 543 # negative lookahead to automatically whitelist the following: 544 # 545 # - ##, for token pasting (CONFIG_FOO_##X) 546 # 547 # - $, e.g. for CMake variable expansion (CONFIG_FOO_${VAR}) 548 # 549 # - @, e.g. for CMakes's configure_file() (CONFIG_FOO_@VAR@) 550 # 551 # - {, e.g. for Python scripts ("CONFIG_FOO_{}_BAR".format(...)") 552 # 553 # - *, meant for comments like '#endif /* CONFIG_FOO_* */ 554 555 defined_syms = self.get_defined_syms(kconf) 556 557 # Maps each undefined symbol to a list <filename>:<linenr> strings 558 undef_to_locs = collections.defaultdict(list) 559 560 # Warning: Needs to work with both --perl-regexp and the 're' module 561 regex = r"\bCONFIG_[A-Z0-9_]+\b(?!\s*##|[$@{*])" 562 563 # Skip doc/releases, which often references removed symbols 564 grep_stdout = git("grep", "--line-number", "-I", "--null", 565 "--perl-regexp", regex, "--", ":!/doc/releases", 566 cwd=Path(GIT_TOP)) 567 568 # splitlines() supports various line terminators 569 for grep_line in grep_stdout.splitlines(): 570 path, lineno, line = grep_line.split("\0") 571 572 # Extract symbol references (might be more than one) within the 573 # line 574 for sym_name in re.findall(regex, line): 575 sym_name = sym_name[7:] # Strip CONFIG_ 576 if sym_name not in defined_syms and \ 577 sym_name not in self.UNDEF_KCONFIG_WHITELIST: 578 579 undef_to_locs[sym_name].append(f"{path}:{lineno}") 580 581 if not undef_to_locs: 582 return 583 584 # String that describes all referenced but undefined Kconfig symbols, 585 # in alphabetical order, along with the locations where they're 586 # referenced. Example: 587 # 588 # CONFIG_ALSO_MISSING arch/xtensa/core/fatal.c:273 589 # CONFIG_MISSING arch/xtensa/core/fatal.c:264, subsys/fb/cfb.c:20 590 undef_desc = "\n".join(f"CONFIG_{sym_name:35} {', '.join(locs)}" 591 for sym_name, locs in sorted(undef_to_locs.items())) 592 593 self.failure(f""" 594Found references to undefined Kconfig symbols. If any of these are false 595positives, then add them to UNDEF_KCONFIG_WHITELIST in {__file__}. 596 597If the reference is for a comment like /* CONFIG_FOO_* */ (or 598/* CONFIG_FOO_*_... */), then please use exactly that form (with the '*'). The 599CI check knows not to flag it. 600 601More generally, a reference followed by $, @, {{, *, or ## will never be 602flagged. 603 604{undef_desc}""") 605 606 # Many of these are symbols used as examples. Note that the list is sorted 607 # alphabetically, and skips the CONFIG_ prefix. 608 UNDEF_KCONFIG_WHITELIST = { 609 "ALSO_MISSING", 610 "APP_LINK_WITH_", 611 "APP_LOG_LEVEL", # Application log level is not detected correctly as 612 # the option is defined using a template, so it can't 613 # be grepped 614 "ARMCLANG_STD_LIBC", # The ARMCLANG_STD_LIBC is defined in the 615 # toolchain Kconfig which is sourced based on 616 # Zephyr toolchain variant and therefore not 617 # visible to compliance. 618 "BOOT_ENCRYPTION_KEY_FILE", # Used in sysbuild 619 "BOOT_ENCRYPT_IMAGE", # Used in sysbuild 620 "BINDESC_", # Used in documentation as a prefix 621 "BOOT_UPGRADE_ONLY", # Used in example adjusting MCUboot config, but 622 # symbol is defined in MCUboot itself. 623 "BOOT_SERIAL_BOOT_MODE", # Used in (sysbuild-based) test/ 624 # documentation 625 "BOOT_SERIAL_CDC_ACM", # Used in (sysbuild-based) test 626 "BOOT_SERIAL_ENTRANCE_GPIO", # Used in (sysbuild-based) test 627 "BOOT_SERIAL_IMG_GRP_HASH", # Used in documentation 628 "BOOT_SHARE_DATA", # Used in Kconfig text 629 "BOOT_SHARE_DATA_BOOTINFO", # Used in (sysbuild-based) test 630 "BOOT_SHARE_BACKEND_RETENTION", # Used in Kconfig text 631 "BOOT_SIGNATURE_KEY_FILE", # MCUboot setting used by sysbuild 632 "BOOT_SIGNATURE_TYPE_ECDSA_P256", # MCUboot setting used by sysbuild 633 "BOOT_SIGNATURE_TYPE_ED25519", # MCUboot setting used by sysbuild 634 "BOOT_SIGNATURE_TYPE_NONE", # MCUboot setting used by sysbuild 635 "BOOT_SIGNATURE_TYPE_RSA", # MCUboot setting used by sysbuild 636 "BOOT_VALIDATE_SLOT0", # Used in (sysbuild-based) test 637 "BOOT_WATCHDOG_FEED", # Used in (sysbuild-based) test 638 "BTTESTER_LOG_LEVEL", # Used in tests/bluetooth/tester 639 "BTTESTER_LOG_LEVEL_DBG", # Used in tests/bluetooth/tester 640 "CDC_ACM_PORT_NAME_", 641 "CLOCK_STM32_SYSCLK_SRC_", 642 "CMU", 643 "COMPILER_RT_RTLIB", 644 "BT_6LOWPAN", # Defined in Linux, mentioned in docs 645 "CMD_CACHE", # Defined in U-Boot, mentioned in docs 646 "COUNTER_RTC_STM32_CLOCK_SRC", 647 "CRC", # Used in TI CC13x2 / CC26x2 SDK comment 648 "DEEP_SLEEP", # #defined by RV32M1 in ext/ 649 "DESCRIPTION", 650 "ERR", 651 "ESP_DIF_LIBRARY", # Referenced in CMake comment 652 "EXPERIMENTAL", 653 "FFT", # Used as an example in cmake/extensions.cmake 654 "FLAG", # Used as an example 655 "FOO", 656 "FOO_LOG_LEVEL", 657 "FOO_SETTING_1", 658 "FOO_SETTING_2", 659 "LSM6DSO_INT_PIN", 660 "LIBGCC_RTLIB", 661 "LLVM_USE_LD", # Both LLVM_USE_* are in cmake/toolchain/llvm/Kconfig 662 "LLVM_USE_LLD", # which are only included if LLVM is selected but 663 # not other toolchains. Compliance check would complain, 664 # for example, if you are using GCC. 665 "MCUBOOT_LOG_LEVEL_WRN", # Used in example adjusting MCUboot 666 # config, 667 "MCUBOOT_LOG_LEVEL_INF", 668 "MCUBOOT_DOWNGRADE_PREVENTION", # but symbols are defined in MCUboot 669 # itself. 670 "MCUBOOT_ACTION_HOOKS", # Used in (sysbuild-based) test 671 "MCUBOOT_CLEANUP_ARM_CORE", # Used in (sysbuild-based) test 672 "MCUBOOT_SERIAL", # Used in (sysbuild-based) test/ 673 # documentation 674 "MCUMGR_GRP_EXAMPLE", # Used in documentation 675 "MCUMGR_GRP_EXAMPLE_LOG_LEVEL", # Used in documentation 676 "MCUMGR_GRP_EXAMPLE_OTHER_HOOK", # Used in documentation 677 "MISSING", 678 "MODULES", 679 "MYFEATURE", 680 "MY_DRIVER_0", 681 "NORMAL_SLEEP", # #defined by RV32M1 in ext/ 682 "OPT", 683 "OPT_0", 684 "PEDO_THS_MIN", 685 "REG1", 686 "REG2", 687 "SAMPLE_MODULE_LOG_LEVEL", # Used as an example in samples/subsys/logging 688 "SAMPLE_MODULE_LOG_LEVEL_DBG", # Used in tests/subsys/logging/log_api 689 "LOG_BACKEND_MOCK_OUTPUT_DEFAULT", #Referenced in tests/subsys/logging/log_syst 690 "LOG_BACKEND_MOCK_OUTPUT_SYST", #Referenced in testcase.yaml of log_syst test 691 "SEL", 692 "SHIFT", 693 "SOC_WATCH", # Issue 13749 694 "SOME_BOOL", 695 "SOME_INT", 696 "SOME_OTHER_BOOL", 697 "SOME_STRING", 698 "SRAM2", # Referenced in a comment in samples/application_development 699 "STACK_SIZE", # Used as an example in the Kconfig docs 700 "STD_CPP", # Referenced in CMake comment 701 "TAGOIO_HTTP_POST_LOG_LEVEL", # Used as in samples/net/cloud/tagoio 702 "TEST1", 703 "TOOLCHAIN_ARCMWDT_SUPPORTS_THREAD_LOCAL_STORAGE", # The symbol is defined in the toolchain 704 # Kconfig which is sourced based on Zephyr 705 # toolchain variant and therefore not visible 706 # to compliance. 707 "TYPE_BOOLEAN", 708 "USB_CONSOLE", 709 "USE_STDC_", 710 "WHATEVER", 711 "EXTRA_FIRMWARE_DIR", # Linux, in boards/xtensa/intel_adsp_cavs25/doc 712 "HUGETLBFS", # Linux, in boards/xtensa/intel_adsp_cavs25/doc 713 "MODVERSIONS", # Linux, in boards/xtensa/intel_adsp_cavs25/doc 714 "SECURITY_LOADPIN", # Linux, in boards/xtensa/intel_adsp_cavs25/doc 715 "ZEPHYR_TRY_MASS_ERASE", # MCUBoot setting described in sysbuild 716 # documentation 717 "ZTEST_FAIL_TEST_", # regex in tests/ztest/fail/CMakeLists.txt 718 } 719 720 721class KconfigBasicCheck(KconfigCheck): 722 """ 723 Checks if we are introducing any new warnings/errors with Kconfig, 724 for example using undefined Kconfig variables. 725 This runs the basic Kconfig test, which is checking only for undefined 726 references inside the Kconfig tree. 727 """ 728 name = "KconfigBasic" 729 doc = "See https://docs.zephyrproject.org/latest/build/kconfig/tips.html for more details." 730 path_hint = "<zephyr-base>" 731 732 def run(self): 733 super().run(full=False) 734 735class KconfigBasicNoModulesCheck(KconfigCheck): 736 """ 737 Checks if we are introducing any new warnings/errors with Kconfig when no 738 modules are available. Catches symbols used in the main repository but 739 defined only in a module. 740 """ 741 name = "KconfigBasicNoModules" 742 doc = "See https://docs.zephyrproject.org/latest/build/kconfig/tips.html for more details." 743 path_hint = "<zephyr-base>" 744 def run(self): 745 super().run(full=False, no_modules=True) 746 747 748class Nits(ComplianceTest): 749 """ 750 Checks various nits in added/modified files. Doesn't check stuff that's 751 already covered by e.g. checkpatch.pl and pylint. 752 """ 753 name = "Nits" 754 doc = "See https://docs.zephyrproject.org/latest/contribute/guidelines.html#coding-style for more details." 755 path_hint = "<git-top>" 756 757 def run(self): 758 # Loop through added/modified files 759 for fname in get_files(filter="d"): 760 if "Kconfig" in fname: 761 self.check_kconfig_header(fname) 762 self.check_redundant_zephyr_source(fname) 763 764 if fname.startswith("dts/bindings/"): 765 self.check_redundant_document_separator(fname) 766 767 if fname.endswith((".c", ".conf", ".cpp", ".dts", ".overlay", 768 ".h", ".ld", ".py", ".rst", ".txt", ".yaml", 769 ".yml")) or \ 770 "Kconfig" in fname or \ 771 "defconfig" in fname or \ 772 fname == "README": 773 774 self.check_source_file(fname) 775 776 def check_kconfig_header(self, fname): 777 # Checks for a spammy copy-pasted header format 778 779 with open(os.path.join(GIT_TOP, fname), encoding="utf-8") as f: 780 contents = f.read() 781 782 # 'Kconfig - yada yada' has a copy-pasted redundant filename at the 783 # top. This probably means all of the header was copy-pasted. 784 if re.match(r"\s*#\s*(K|k)config[\w.-]*\s*-", contents): 785 self.failure(f""" 786Please use this format for the header in '{fname}' (see 787https://docs.zephyrproject.org/latest/build/kconfig/tips.html#header-comments-and-other-nits): 788 789 # <Overview of symbols defined in the file, preferably in plain English> 790 (Blank line) 791 # Copyright (c) 2019 ... 792 # SPDX-License-Identifier: <License> 793 (Blank line) 794 (Kconfig definitions) 795 796Skip the "Kconfig - " part of the first line, since it's clear that the comment 797is about Kconfig from context. The "# Kconfig - " is what triggers this 798failure. 799""") 800 801 def check_redundant_zephyr_source(self, fname): 802 # Checks for 'source "$(ZEPHYR_BASE)/Kconfig[.zephyr]"', which can be 803 # be simplified to 'source "Kconfig[.zephyr]"' 804 805 with open(os.path.join(GIT_TOP, fname), encoding="utf-8") as f: 806 # Look for e.g. rsource as well, for completeness 807 match = re.search( 808 r'^\s*(?:o|r|or)?source\s*"\$\(?ZEPHYR_BASE\)?/(Kconfig(?:\.zephyr)?)"', 809 f.read(), re.MULTILINE) 810 811 if match: 812 self.failure(""" 813Redundant 'source "$(ZEPHYR_BASE)/{0}" in '{1}'. Just do 'source "{0}"' 814instead. The $srctree environment variable already points to the Zephyr root, 815and all 'source's are relative to it.""".format(match.group(1), fname)) 816 817 def check_redundant_document_separator(self, fname): 818 # Looks for redundant '...' document separators in bindings 819 820 with open(os.path.join(GIT_TOP, fname), encoding="utf-8") as f: 821 if re.search(r"^\.\.\.", f.read(), re.MULTILINE): 822 self.failure(f"""\ 823Redundant '...' document separator in {fname}. Binding YAML files are never 824concatenated together, so no document separators are needed.""") 825 826 def check_source_file(self, fname): 827 # Generic nits related to various source files 828 829 with open(os.path.join(GIT_TOP, fname), encoding="utf-8") as f: 830 contents = f.read() 831 832 if not contents.endswith("\n"): 833 self.failure(f"Missing newline at end of '{fname}'. Check your text " 834 f"editor settings.") 835 836 if contents.startswith("\n"): 837 self.failure(f"Please remove blank lines at start of '{fname}'") 838 839 if contents.endswith("\n\n"): 840 self.failure(f"Please remove blank lines at end of '{fname}'") 841 842 843class GitDiffCheck(ComplianceTest): 844 """ 845 Checks for conflict markers or whitespace errors with git diff --check 846 """ 847 name = "GitDiffCheck" 848 doc = "Git conflict markers and whitespace errors are not allowed in added changes" 849 path_hint = "<git-top>" 850 851 def run(self): 852 offending_lines = [] 853 # Use regex to filter out unnecessay output 854 # Reason: `--check` is mutually exclusive with `--name-only` and `-s` 855 p = re.compile(r"\S+\: .*\.") 856 857 for shaidx in get_shas(COMMIT_RANGE): 858 # Ignore non-zero return status code 859 # Reason: `git diff --check` sets the return code to the number of offending lines 860 diff = git("diff", f"{shaidx}^!", "--check", ignore_non_zero=True) 861 862 lines = p.findall(diff) 863 lines = map(lambda x: f"{shaidx}: {x}", lines) 864 offending_lines.extend(lines) 865 866 if len(offending_lines) > 0: 867 self.failure("\n".join(offending_lines)) 868 869 870class GitLint(ComplianceTest): 871 """ 872 Runs gitlint on the commits and finds issues with style and syntax 873 874 """ 875 name = "Gitlint" 876 doc = "See https://docs.zephyrproject.org/latest/contribute/guidelines.html#commit-guidelines for more details" 877 path_hint = "<git-top>" 878 879 def run(self): 880 # By default gitlint looks for .gitlint configuration only in 881 # the current directory 882 try: 883 subprocess.run('gitlint --commits ' + COMMIT_RANGE, 884 check=True, 885 stdout=subprocess.PIPE, 886 stderr=subprocess.STDOUT, 887 shell=True, cwd=GIT_TOP) 888 889 except subprocess.CalledProcessError as ex: 890 self.failure(ex.output.decode("utf-8")) 891 892 893class PyLint(ComplianceTest): 894 """ 895 Runs pylint on all .py files, with a limited set of checks enabled. The 896 configuration is in the pylintrc file. 897 """ 898 name = "Pylint" 899 doc = "See https://www.pylint.org/ for more details" 900 path_hint = "<git-top>" 901 902 def run(self): 903 # Path to pylint configuration file 904 pylintrc = os.path.abspath(os.path.join(os.path.dirname(__file__), 905 "pylintrc")) 906 907 # Path to additional pylint check scripts 908 check_script_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 909 "../pylint/checkers")) 910 911 # List of files added/modified by the commit(s). 912 files = get_files(filter="d") 913 914 # Filter out everything but Python files. Keep filenames 915 # relative (to GIT_TOP) to stay farther from any command line 916 # limit. 917 py_files = filter_py(GIT_TOP, files) 918 if not py_files: 919 return 920 921 python_environment = os.environ.copy() 922 if "PYTHONPATH" in python_environment: 923 python_environment["PYTHONPATH"] = check_script_dir + ":" + \ 924 python_environment["PYTHONPATH"] 925 else: 926 python_environment["PYTHONPATH"] = check_script_dir 927 928 pylintcmd = ["pylint", "--rcfile=" + pylintrc, 929 "--load-plugins=argparse-checker"] + py_files 930 logger.info(cmd2str(pylintcmd)) 931 try: 932 subprocess.run(pylintcmd, 933 check=True, 934 stdout=subprocess.PIPE, 935 stderr=subprocess.STDOUT, 936 cwd=GIT_TOP, 937 env=python_environment) 938 except subprocess.CalledProcessError as ex: 939 output = ex.output.decode("utf-8") 940 regex = r'^\s*(\S+):(\d+):(\d+):\s*([A-Z]\d{4}):\s*(.*)$' 941 942 matches = re.findall(regex, output, re.MULTILINE) 943 for m in matches: 944 # https://pylint.pycqa.org/en/latest/user_guide/messages/messages_overview.html# 945 severity = 'unknown' 946 if m[3][0] in ('F', 'E'): 947 severity = 'error' 948 elif m[3][0] in ('W','C', 'R', 'I'): 949 severity = 'warning' 950 self.fmtd_failure(severity, m[3], m[0], m[1], col=m[2], 951 desc=m[4]) 952 953 # If the regex has not matched add the whole output as a failure 954 if len(matches) == 0: 955 self.failure(output) 956 957 958def filter_py(root, fnames): 959 # PyLint check helper. Returns all Python script filenames among the 960 # filenames in 'fnames', relative to directory 'root'. 961 # 962 # Uses the python-magic library, so that we can detect Python 963 # files that don't end in .py as well. python-magic is a frontend 964 # to libmagic, which is also used by 'file'. 965 return [fname for fname in fnames 966 if (fname.endswith(".py") or 967 magic.from_file(os.path.join(root, fname), 968 mime=True) == "text/x-python")] 969 970 971class Identity(ComplianceTest): 972 """ 973 Checks if Emails of author and signed-off messages are consistent. 974 """ 975 name = "Identity" 976 doc = "See https://docs.zephyrproject.org/latest/contribute/guidelines.html#commit-guidelines for more details" 977 # git rev-list and git log don't depend on the current (sub)directory 978 # unless explicited 979 path_hint = "<git-top>" 980 981 def run(self): 982 for shaidx in get_shas(COMMIT_RANGE): 983 commit = git("log", "--decorate=short", "-n 1", shaidx) 984 signed = [] 985 author = "" 986 sha = "" 987 parsed_addr = None 988 for line in commit.split("\n"): 989 match = re.search(r"^commit\s([^\s]*)", line) 990 if match: 991 sha = match.group(1) 992 match = re.search(r"^Author:\s(.*)", line) 993 if match: 994 author = match.group(1) 995 parsed_addr = parseaddr(author) 996 match = re.search(r"signed-off-by:\s(.*)", line, re.IGNORECASE) 997 if match: 998 signed.append(match.group(1)) 999 1000 error1 = f"{sha}: author email ({author}) needs to match one of " \ 1001 f"the signed-off-by entries." 1002 error2 = f"{sha}: author email ({author}) does not follow the " \ 1003 f"syntax: First Last <email>." 1004 error3 = f"{sha}: author email ({author}) must be a real email " \ 1005 f"and cannot end in @users.noreply.github.com" 1006 failure = None 1007 if author not in signed: 1008 failure = error1 1009 1010 if not parsed_addr or len(parsed_addr[0].split(" ")) < 2: 1011 if not failure: 1012 1013 failure = error2 1014 else: 1015 failure = failure + "\n" + error2 1016 elif parsed_addr[1].endswith("@users.noreply.github.com"): 1017 failure = error3 1018 1019 if failure: 1020 self.failure(failure) 1021 1022 1023class BinaryFiles(ComplianceTest): 1024 """ 1025 Check that the diff contains no binary files. 1026 """ 1027 name = "BinaryFiles" 1028 doc = "No binary files allowed." 1029 path_hint = "<git-top>" 1030 1031 def run(self): 1032 BINARY_ALLOW_PATHS = ("doc/", "boards/", "samples/") 1033 # svg files are always detected as binary, see .gitattributes 1034 BINARY_ALLOW_EXT = (".jpg", ".jpeg", ".png", ".svg", ".webp") 1035 1036 for stat in git("diff", "--numstat", "--diff-filter=A", 1037 COMMIT_RANGE).splitlines(): 1038 added, deleted, fname = stat.split("\t") 1039 if added == "-" and deleted == "-": 1040 if (fname.startswith(BINARY_ALLOW_PATHS) and 1041 fname.endswith(BINARY_ALLOW_EXT)): 1042 continue 1043 self.failure(f"Binary file not allowed: {fname}") 1044 1045 1046class ImageSize(ComplianceTest): 1047 """ 1048 Check that any added image is limited in size. 1049 """ 1050 name = "ImageSize" 1051 doc = "Check the size of image files." 1052 path_hint = "<git-top>" 1053 1054 def run(self): 1055 SIZE_LIMIT = 250 << 10 1056 BOARD_SIZE_LIMIT = 100 << 10 1057 1058 for file in get_files(filter="d"): 1059 full_path = os.path.join(GIT_TOP, file) 1060 mime_type = magic.from_file(full_path, mime=True) 1061 1062 if not mime_type.startswith("image/"): 1063 continue 1064 1065 size = os.path.getsize(full_path) 1066 1067 limit = SIZE_LIMIT 1068 if file.startswith("boards/"): 1069 limit = BOARD_SIZE_LIMIT 1070 1071 if size > limit: 1072 self.failure(f"Image file too large: {file} reduce size to " 1073 f"less than {limit >> 10}kB") 1074 1075 1076class MaintainersFormat(ComplianceTest): 1077 """ 1078 Check that MAINTAINERS file parses correctly. 1079 """ 1080 name = "MaintainersFormat" 1081 doc = "Check that MAINTAINERS file parses correctly." 1082 path_hint = "<git-top>" 1083 1084 def run(self): 1085 MAINTAINERS_FILES = ["MAINTAINERS.yml", "MAINTAINERS.yaml"] 1086 1087 for file in MAINTAINERS_FILES: 1088 if not os.path.exists(file): 1089 continue 1090 1091 try: 1092 Maintainers(file) 1093 except MaintainersError as ex: 1094 self.failure(f"Error parsing {file}: {ex}") 1095 1096class ModulesMaintainers(ComplianceTest): 1097 """ 1098 Check that all modules have a MAINTAINERS entry. 1099 """ 1100 name = "ModulesMaintainers" 1101 doc = "Check that all modules have a MAINTAINERS entry." 1102 path_hint = "<git-top>" 1103 1104 def run(self): 1105 MAINTAINERS_FILES = ["MAINTAINERS.yml", "MAINTAINERS.yaml"] 1106 1107 manifest = Manifest.from_file() 1108 1109 maintainers_file = None 1110 for file in MAINTAINERS_FILES: 1111 if os.path.exists(file): 1112 maintainers_file = file 1113 break 1114 if not maintainers_file: 1115 return 1116 1117 maintainers = Maintainers(maintainers_file) 1118 1119 for project in manifest.get_projects([]): 1120 if not manifest.is_active(project): 1121 continue 1122 1123 if isinstance(project, ManifestProject): 1124 continue 1125 1126 area = f"West project: {project.name}" 1127 if area not in maintainers.areas: 1128 self.failure(f"Missing {maintainers_file} entry for: \"{area}\"") 1129 1130 1131class YAMLLint(ComplianceTest): 1132 """ 1133 YAMLLint 1134 """ 1135 name = "YAMLLint" 1136 doc = "Check YAML files with YAMLLint." 1137 path_hint = "<git-top>" 1138 1139 def run(self): 1140 config_file = os.path.join(ZEPHYR_BASE, ".yamllint") 1141 1142 for file in get_files(filter="d"): 1143 if Path(file).suffix not in ['.yaml', '.yml']: 1144 continue 1145 1146 yaml_config = config.YamlLintConfig(file=config_file) 1147 1148 if file.startswith(".github/"): 1149 # Tweak few rules for workflow files. 1150 yaml_config.rules["line-length"] = False 1151 yaml_config.rules["truthy"]["allowed-values"].extend(['on', 'off']) 1152 elif file == ".codecov.yml": 1153 yaml_config.rules["truthy"]["allowed-values"].extend(['yes', 'no']) 1154 1155 with open(file, 'r') as fp: 1156 for p in linter.run(fp, yaml_config): 1157 self.fmtd_failure('warning', f'YAMLLint ({p.rule})', file, 1158 p.line, col=p.column, desc=p.desc) 1159 1160 1161def init_logs(cli_arg): 1162 # Initializes logging 1163 1164 global logger 1165 1166 level = os.environ.get('LOG_LEVEL', "WARN") 1167 1168 console = logging.StreamHandler() 1169 console.setFormatter(logging.Formatter('%(levelname)-8s: %(message)s')) 1170 1171 logger = logging.getLogger('') 1172 logger.addHandler(console) 1173 logger.setLevel(cli_arg or level) 1174 1175 logger.info("Log init completed, level=%s", 1176 logging.getLevelName(logger.getEffectiveLevel())) 1177 1178 1179def inheritors(klass): 1180 subclasses = set() 1181 work = [klass] 1182 while work: 1183 parent = work.pop() 1184 for child in parent.__subclasses__(): 1185 if child not in subclasses: 1186 subclasses.add(child) 1187 work.append(child) 1188 return subclasses 1189 1190 1191def annotate(res): 1192 """ 1193 https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#about-workflow-commands 1194 """ 1195 notice = f'::{res.severity} file={res.file}' + \ 1196 (f',line={res.line}' if res.line else '') + \ 1197 (f',col={res.col}' if res.col else '') + \ 1198 f',title={res.title}::{res.message}' 1199 print(notice) 1200 1201 1202def resolve_path_hint(hint): 1203 if hint == "<zephyr-base>": 1204 return ZEPHYR_BASE 1205 elif hint == "<git-top>": 1206 return GIT_TOP 1207 else: 1208 return hint 1209 1210 1211def parse_args(argv): 1212 1213 default_range = 'HEAD~1..HEAD' 1214 parser = argparse.ArgumentParser( 1215 description="Check for coding style and documentation warnings.", allow_abbrev=False) 1216 parser.add_argument('-c', '--commits', default=default_range, 1217 help=f'''Commit range in the form: a..[b], default is 1218 {default_range}''') 1219 parser.add_argument('-o', '--output', default="compliance.xml", 1220 help='''Name of outfile in JUnit format, 1221 default is ./compliance.xml''') 1222 parser.add_argument('-n', '--no-case-output', action="store_true", 1223 help="Do not store the individual test case output.") 1224 parser.add_argument('-l', '--list', action="store_true", 1225 help="List all checks and exit") 1226 parser.add_argument("-v", "--loglevel", choices=['DEBUG', 'INFO', 'WARNING', 1227 'ERROR', 'CRITICAL'], 1228 help="python logging level") 1229 parser.add_argument('-m', '--module', action="append", default=[], 1230 help="Checks to run. All checks by default. (case " \ 1231 "insensitive)") 1232 parser.add_argument('-e', '--exclude-module', action="append", default=[], 1233 help="Do not run the specified checks (case " \ 1234 "insensitive)") 1235 parser.add_argument('-j', '--previous-run', default=None, 1236 help='''Pre-load JUnit results in XML format 1237 from a previous run and combine with new results.''') 1238 parser.add_argument('--annotate', action="store_true", 1239 help="Print GitHub Actions-compatible annotations.") 1240 1241 return parser.parse_args(argv) 1242 1243def _main(args): 1244 # The "real" main(), which is wrapped to catch exceptions and report them 1245 # to GitHub. Returns the number of test failures. 1246 1247 global ZEPHYR_BASE 1248 ZEPHYR_BASE = os.environ.get('ZEPHYR_BASE') 1249 if not ZEPHYR_BASE: 1250 # Let the user run this script as ./scripts/ci/check_compliance.py without 1251 # making them set ZEPHYR_BASE. 1252 ZEPHYR_BASE = str(Path(__file__).resolve().parents[2]) 1253 1254 # Propagate this decision to child processes. 1255 os.environ['ZEPHYR_BASE'] = ZEPHYR_BASE 1256 1257 # The absolute path of the top-level git directory. Initialize it here so 1258 # that issues running Git can be reported to GitHub. 1259 global GIT_TOP 1260 GIT_TOP = git("rev-parse", "--show-toplevel") 1261 1262 # The commit range passed in --commit, e.g. "HEAD~3" 1263 global COMMIT_RANGE 1264 COMMIT_RANGE = args.commits 1265 1266 init_logs(args.loglevel) 1267 1268 logger.info(f'Running tests on commit range {COMMIT_RANGE}') 1269 1270 if args.list: 1271 for testcase in inheritors(ComplianceTest): 1272 print(testcase.name) 1273 return 0 1274 1275 # Load saved test results from an earlier run, if requested 1276 if args.previous_run: 1277 if not os.path.exists(args.previous_run): 1278 # This probably means that an earlier pass had an internal error 1279 # (the script is currently run multiple times by the ci-pipelines 1280 # repo). Since that earlier pass might've posted an error to 1281 # GitHub, avoid generating a GitHub comment here, by avoiding 1282 # sys.exit() (which gets caught in main()). 1283 print(f"error: '{args.previous_run}' not found", 1284 file=sys.stderr) 1285 return 1 1286 1287 logging.info(f"Loading previous results from {args.previous_run}") 1288 for loaded_suite in JUnitXml.fromfile(args.previous_run): 1289 suite = loaded_suite 1290 break 1291 else: 1292 suite = TestSuite("Compliance") 1293 1294 included = list(map(lambda x: x.lower(), args.module)) 1295 excluded = list(map(lambda x: x.lower(), args.exclude_module)) 1296 1297 for testcase in inheritors(ComplianceTest): 1298 # "Modules" and "testcases" are the same thing. Better flags would have 1299 # been --tests and --exclude-tests or the like, but it's awkward to 1300 # change now. 1301 1302 if included and testcase.name.lower() not in included: 1303 continue 1304 1305 if testcase.name.lower() in excluded: 1306 print("Skipping " + testcase.name) 1307 continue 1308 1309 test = testcase() 1310 try: 1311 print(f"Running {test.name:16} tests in " 1312 f"{resolve_path_hint(test.path_hint)} ...") 1313 test.run() 1314 except EndTest: 1315 pass 1316 1317 # Annotate if required 1318 if args.annotate: 1319 for res in test.fmtd_failures: 1320 annotate(res) 1321 1322 suite.add_testcase(test.case) 1323 1324 if args.output: 1325 xml = JUnitXml() 1326 xml.add_testsuite(suite) 1327 xml.update_statistics() 1328 xml.write(args.output, pretty=True) 1329 1330 failed_cases = [] 1331 name2doc = {testcase.name: testcase.doc 1332 for testcase in inheritors(ComplianceTest)} 1333 1334 for case in suite: 1335 if case.result: 1336 if case.is_skipped: 1337 logging.warning(f"Skipped {case.name}") 1338 else: 1339 failed_cases.append(case) 1340 else: 1341 # Some checks like codeowners can produce no .result 1342 logging.info(f"No JUnit result for {case.name}") 1343 1344 n_fails = len(failed_cases) 1345 1346 if n_fails: 1347 print(f"{n_fails} checks failed") 1348 for case in failed_cases: 1349 for res in case.result: 1350 errmsg = res.text.strip() 1351 logging.error(f"Test {case.name} failed: \n{errmsg}") 1352 if args.no_case_output: 1353 continue 1354 with open(f"{case.name}.txt", "w") as f: 1355 docs = name2doc.get(case.name) 1356 f.write(f"{docs}\n") 1357 for res in case.result: 1358 errmsg = res.text.strip() 1359 f.write(f'\n {errmsg}') 1360 1361 if args.output: 1362 print(f"\nComplete results in {args.output}") 1363 return n_fails 1364 1365 1366def main(argv=None): 1367 args = parse_args(argv) 1368 1369 try: 1370 # pylint: disable=unused-import 1371 from lxml import etree 1372 except ImportError: 1373 print("\nERROR: Python module lxml not installed, unable to proceed") 1374 print("See https://github.com/weiwei/junitparser/issues/99") 1375 return 1 1376 1377 try: 1378 n_fails = _main(args) 1379 except BaseException: 1380 # Catch BaseException instead of Exception to include stuff like 1381 # SystemExit (raised by sys.exit()) 1382 print(f"Python exception in `{__file__}`:\n\n" 1383 f"```\n{traceback.format_exc()}\n```") 1384 1385 raise 1386 1387 sys.exit(n_fails) 1388 1389 1390def cmd2str(cmd): 1391 # Formats the command-line arguments in the iterable 'cmd' into a string, 1392 # for error messages and the like 1393 1394 return " ".join(shlex.quote(word) for word in cmd) 1395 1396 1397def err(msg): 1398 cmd = sys.argv[0] # Empty if missing 1399 if cmd: 1400 cmd += ": " 1401 sys.exit(f"{cmd} error: {msg}") 1402 1403 1404if __name__ == "__main__": 1405 main(sys.argv[1:]) 1406