Coverage for lobster/tools/core/ci_report/ci_report.py: 0%

43 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-09-22 04:43 +0000

1#!/usr/bin/env python3 

2# 

3# lobster_ci_report - Visualise LOBSTER issues for CI 

4# Copyright (C) 2023-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) 

5# 

6# This program is free software: you can redistribute it and/or modify 

7# it under the terms of the GNU Affero General Public License as 

8# published by the Free Software Foundation, either version 3 of the 

9# License, or (at your option) any later version. 

10# 

11# This program is distributed in the hope that it will be useful, but 

12# WITHOUT ANY WARRANTY; without even the implied warranty of 

13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 

14# Affero General Public License for more details. 

15# 

16# You should have received a copy of the GNU Affero General Public 

17# License along with this program. If not, see 

18# <https://www.gnu.org/licenses/>. 

19 

20from argparse import ArgumentParser, Namespace 

21import os 

22from typing import List, Optional, Sequence 

23 

24from lobster.common.report import Report 

25from lobster.common.items import Tracing_Status 

26from lobster.common.meta_data_tool_base import MetaDataToolBase 

27 

28 

29def resolve_report_path(lobster_report: str) -> str: 

30 # relative paths are given from where `bazel run` was invoked, not 

31 # from the runfiles directory the binary actually starts in 

32 if os.path.isabs(lobster_report): 

33 return lobster_report 

34 workdir = os.environ.get("BUILD_WORKING_DIRECTORY") 

35 return os.path.join(workdir, lobster_report) if workdir else lobster_report 

36 

37 

38def ensure_report_file_exists( 

39 argument_parser: ArgumentParser, 

40 lobster_report_arg: str, 

41 resolved_path: str, 

42) -> None: 

43 """Exits via argument_parser.error() (SystemExit) IF resolved_path does not 

44 name an existing file, distinguishing whether the user gave that path 

45 explicitly or whether it is the unmodified default value.""" 

46 if not os.path.isfile(resolved_path): 

47 if lobster_report_arg == "report.lobster": 

48 argument_parser.error("specify report file") 

49 else: 

50 argument_parser.error(f"{lobster_report_arg} is not a file") 

51 

52 

53def report_errors_for_untraced_items(report: Report) -> None: 

54 """Emits an error (via report.mh) for each message of every item whose 

55 tracing status is neither OK nor JUSTIFIED.""" 

56 for uid in sorted(report.items): 

57 item = report.items[uid] 

58 if item.tracing_status not in (Tracing_Status.OK, 

59 Tracing_Status.JUSTIFIED): 

60 for message in item.messages: 

61 report.mh.error(item.location, 

62 message, 

63 fatal = False) 

64 

65 

66def format_coverage_lines(report: Report) -> List[str]: 

67 """Returns one formatted coverage summary line per report level.""" 

68 return [ 

69 f"coverage: {level}: {coverage.coverage:.1f}% " 

70 f"({coverage.ok} of {coverage.items} items)" 

71 for level, coverage in report.coverage.items() 

72 ] 

73 

74 

75class CiReportTool(MetaDataToolBase): 

76 def __init__(self): 

77 super().__init__( 

78 name="ci-report", 

79 description="Command line tool to check a LOBSTER report", 

80 official=True, 

81 ) 

82 

83 self._argument_parser.add_argument( 

84 "lobster_report", 

85 metavar="file", 

86 nargs="?", 

87 default="report.lobster", 

88 help="Path to the LOBSTER report file (default: report.lobster)", 

89 ) 

90 self._argument_parser.add_argument( 

91 "--show-coverage", 

92 action="store_true", 

93 help="Print the per-level coverage summary of the report.", 

94 ) 

95 

96 def _run_impl(self, options: Namespace) -> int: 

97 lobster_report = resolve_report_path(options.lobster_report) 

98 ensure_report_file_exists( 

99 self._argument_parser, options.lobster_report, lobster_report, 

100 ) 

101 

102 report = Report() 

103 report.load_report(lobster_report) 

104 

105 report_errors_for_untraced_items(report) 

106 

107 if options.show_coverage: 

108 for line in format_coverage_lines(report): 

109 print(line) 

110 

111 if report.mh.errors: 

112 return 1 

113 return 0 

114 

115 

116def main(args: Optional[Sequence[str]] = None) -> int: 

117 return CiReportTool().run(args)