Coverage for britney2/policies/lintian.py: 90%

106 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-10-30 09:44 +0000

1import optparse 

2import os 

3from typing import TYPE_CHECKING, Any, Optional 

4from urllib.parse import quote 

5 

6import yaml 

7 

8from britney2 import PackageId, SuiteClass 

9from britney2.hints import HintAnnotate, HintType 

10from britney2.migrationitem import MigrationItem 

11from britney2.policies import ApplySrcPolicy, PolicyVerdict 

12from britney2.policies.policy import AbstractBasePolicy 

13from britney2.utils import iter_except, parse_option 

14 

15if TYPE_CHECKING: 15 ↛ 16line 15 didn't jump to line 16 because the condition on line 15 was never true

16 from .. import SourcePackage, Suites 

17 from ..britney import Britney 

18 from ..excuse import Excuse 

19 from ..hints import HintParser 

20 

21 

22class LintianPolicy(AbstractBasePolicy): 

23 def __init__(self, options: optparse.Values, suite_info: "Suites") -> None: 

24 super().__init__( 

25 "lintian", 

26 options, 

27 suite_info, 

28 {SuiteClass.PRIMARY_SOURCE_SUITE}, 

29 ) 

30 self._lintian: dict[PackageId, tuple[str, set[str]]] = {} 

31 

32 # Default values for this policy's options 

33 parse_option(options, "lintian_url") 

34 

35 def register_hints(self, hint_parser: "HintParser") -> None: 

36 hint_parser.register_hint_type( 

37 HintType( 

38 "ignore-lintian", 

39 versioned=HintAnnotate.OPTIONAL, 

40 ) 

41 ) 

42 

43 def initialise(self, britney: "Britney") -> None: 

44 super().initialise(britney) 

45 source_suite = self.suite_info.primary_source_suite 

46 try: 

47 filename = os.path.join(self.state_dir, "lintian.yaml") 

48 except AttributeError as e: # pragma: no cover 

49 raise RuntimeError( 

50 "Please set STATE_DIR in the britney configuration" 

51 ) from e 

52 

53 self._read_lintian_status(filename) 

54 

55 def apply_src_policy_impl( 

56 self, 

57 lintian_info: dict[str, Any], 

58 item: MigrationItem, 

59 source_data_tdist: Optional["SourcePackage"], 

60 source_data_srcdist: "SourcePackage", 

61 excuse: "Excuse", 

62 ) -> PolicyVerdict: 

63 verdict = PolicyVerdict.PASS 

64 

65 source_name = item.package 

66 src_suite_bins = self.suite_info.primary_source_suite.all_binaries_in_suite 

67 src_archs = set() 

68 for pkg_id in source_data_srcdist.binaries: 

69 src_archs.add(src_suite_bins[pkg_id].architecture) 

70 

71 if self.options.lintian_url: 71 ↛ 75line 71 didn't jump to line 75 because the condition on line 71 was always true

72 url = self.options.lintian_url.format(package=quote(source_name)) 

73 url_html = f' - <a href="{url}">info</a>' 

74 else: 

75 url = None 

76 url_html = "" 

77 

78 # skip until a new package is built somewhere 

79 if not source_data_srcdist.binaries: 

80 verdict = PolicyVerdict.REJECTED_TEMPORARILY 

81 self.logger.debug( 

82 "%s hasn't been built anywhere, skipping lintian policy", 

83 source_name, 

84 ) 

85 excuse.add_verdict_info( 

86 verdict, "Lintian check deferred: missing all builds" 

87 ) 

88 lintian_info["result"] = "not built" 

89 else: 

90 assert item.version # for type checking 

91 src_pkg_id = PackageId(item.package, item.version, "source") 

92 try: 

93 results = self._lintian[src_pkg_id] 

94 except KeyError: 

95 verdict = PolicyVerdict.REJECTED_TEMPORARILY 

96 self.logger.debug( 

97 "%s doesn't have lintian results yet", 

98 source_name, 

99 ) 

100 excuse.add_verdict_info( 

101 verdict, 

102 f"Lintian check waiting for test results{url_html}", 

103 ) 

104 lintian_info["result"] = "no data available" 

105 

106 if not verdict.is_rejected: 

107 if results[0] == "FAILED": 

108 verdict = PolicyVerdict.REJECTED_PERMANENTLY 

109 self.logger.debug( 

110 "lintian failed on %s", 

111 source_name, 

112 ) 

113 excuse.add_verdict_info( 

114 verdict, 

115 f"Lintian crashed and produced no output, please contact " 

116 f"{self.options.distribution}-release for a hint{url_html}", 

117 ) 

118 lintian_info["result"] = "lintian failed" 

119 elif results[0] == "TAGS": 

120 verdict = PolicyVerdict.REJECTED_PERMANENTLY 

121 lintian_info["result"] = "triggered tags" 

122 lintian_info["tags"] = sorted(results[1]) 

123 self.logger.debug( 

124 "%s triggered lintian tags", 

125 source_name, 

126 ) 

127 excuse.add_verdict_info( 

128 verdict, 

129 f"Lintian triggered tags: {', '.join(results[1])}{url_html}", 

130 ) 

131 elif src_archs - results[1]: 

132 # britney has more architecture 

133 verdict = PolicyVerdict.REJECTED_TEMPORARILY 

134 archs_missing = src_archs - results[1] 

135 lintian_info["result"] = "lintian misses archs" 

136 lintian_info["archs"] = ", ".join(sorted(archs_missing)) 

137 self.logger.debug( 

138 "%s lintian misses architectures", 

139 source_name, 

140 ) 

141 excuse.add_verdict_info( 

142 verdict, 

143 f"Lintian check waiting for test results on {', '.join(archs_missing)}{url_html}", 

144 ) 

145 elif results[1] - src_archs: 145 ↛ 147line 145 didn't jump to line 147 because the condition on line 145 was never true

146 # lintian had more architecutes than britney, e.g. because of new/old architecture 

147 lintian_info["result"] = "lintian saw more archs" 

148 lintian_info["archs"] = ", ".join(results[1] - src_archs) 

149 else: 

150 pass 

151 

152 if verdict.is_rejected: 

153 assert self.hints is not None 

154 hints = self.hints.search( 

155 "ignore-lintian", 

156 package=source_name, 

157 version=source_data_srcdist.version, 

158 ) 

159 if hints: 

160 verdict = PolicyVerdict.PASS_HINTED 

161 lintian_info.setdefault("ignored-lintian", {}).setdefault( 

162 "issued-by", hints[0].user 

163 ) 

164 excuse.addinfo( 

165 f"Lintian issues ignored as requested by {hints[0].user}" 

166 ) 

167 

168 if verdict != PolicyVerdict.PASS: 

169 lintian_info["url"] = url 

170 

171 return verdict 

172 

173 def _read_lintian_status( 

174 self, filename: str 

175 ) -> dict[PackageId, tuple[str, set[str]]]: 

176 summary = self._lintian 

177 self.logger.debug("Loading lintian status from %s", filename) 

178 with open(filename) as fd: 178 ↛ exitline 178 didn't return from function '_read_lintian_status' because the return on line 180 wasn't executed

179 if os.fstat(fd.fileno()).st_size < 1: 179 ↛ 180line 179 didn't jump to line 180 because the condition on line 179 was never true

180 return summary 

181 data = yaml.safe_load(fd) 

182 

183 for pkgver, info in data.items(): 

184 pkg, ver = pkgver.split("/") 

185 pkg_id = PackageId(pkg, ver, "source") 

186 # TODO: might want to enumerate potential values 

187 if "status" in info.keys() and info["status"] == "lintian failed": 

188 summary[pkg_id] = ("FAILED", set()) 

189 elif info["tags"]: 

190 summary[pkg_id] = ("TAGS", set(info["tags"].keys())) 

191 else: 

192 archs = info["architectures"] 

193 assert "source" in archs, ( 

194 "LintianPolicy expects at least source as architecure in lintian.yaml, " 

195 f"missing for {pkgver}" 

196 ) 

197 summary[pkg_id] = ("ARCHS", set(archs.split()) - {"source"}) 

198 

199 return summary