Coverage for britney2/policies/lintian.py: 93%
117 statements
« prev ^ index » next coverage.py v7.6.0, created at 2026-06-17 09:00 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2026-06-17 09:00 +0000
1import optparse
2import os
3from enum import Enum, StrEnum, auto
4from typing import TYPE_CHECKING, Any, Optional
5from urllib.parse import quote
7import yaml
9from britney2 import PackageId, SuiteClass
10from britney2.hints import HintAnnotate, HintType
11from britney2.migrationitem import MigrationItem
12from britney2.policies import PolicyVerdict
13from britney2.policies.policy import AbstractBasePolicy
14from britney2.utils import (
15 binaries_from_source_version,
16 filter_out_faux_gen,
17 parse_option,
18)
20if TYPE_CHECKING: 20 ↛ 21line 20 didn't jump to line 21 because the condition on line 20 was never true
21 from .. import SourcePackage, Suites
22 from ..britney import Britney
23 from ..excuse import Excuse
24 from ..hints import HintParser
27class LintianResult(Enum):
28 FAILED = auto()
29 TAGS = auto()
30 ARCH = auto()
33class Result(StrEnum):
34 NOT_BUILT = "not built"
35 NO_DATA = "no data available"
36 TRIGGERED_TAGS = "triggered tags"
37 MISSES_ARCHS = "lintian misses archs"
38 SAW_MORE_ARCHS = "lintian saw more archs"
41class LintianPolicy(AbstractBasePolicy):
42 def __init__(self, options: optparse.Values, suite_info: "Suites") -> None:
43 super().__init__(
44 "lintian",
45 options,
46 suite_info,
47 {SuiteClass.PRIMARY_SOURCE_SUITE},
48 )
49 self._lintian: dict[PackageId, tuple[LintianResult, set[str]]] = {}
51 # Default values for this policy's options
52 parse_option(options, "lintian_url")
54 def register_hints(self, hint_parser: "HintParser") -> None:
55 hint_parser.register_hint_type(
56 HintType(
57 "ignore-lintian",
58 versioned=HintAnnotate.OPTIONAL,
59 )
60 )
62 def initialise(self, britney: "Britney") -> None:
63 super().initialise(britney)
64 try:
65 filename = os.path.join(self.state_dir, "lintian.yaml")
66 except AttributeError as e: # pragma: no cover
67 raise RuntimeError(
68 "Please set STATE_DIR in the britney configuration"
69 ) from e
71 self._read_lintian_status(filename)
73 def apply_src_policy_impl(
74 self,
75 lintian_info: dict[str, Any],
76 source_data_tdist: Optional["SourcePackage"],
77 source_data_srcdist: "SourcePackage",
78 excuse: "Excuse",
79 ) -> PolicyVerdict:
80 verdict = PolicyVerdict.PASS
82 item = excuse.item
83 source_name = item.package
84 src_suite_bins = self.suite_info.primary_source_suite.all_binaries_in_suite
85 src_archs = set()
86 for pkg_id in filter_out_faux_gen(source_data_srcdist.binaries):
87 src_archs.add(src_suite_bins[pkg_id].architecture)
89 if self.options.lintian_url:
90 url = self.options.lintian_url.format(package=quote(source_name))
91 url_html = f' - <a href="{url}">info</a>'
92 else:
93 url = None
94 url_html = ""
96 # skip until a new package is built somewhere
97 if not binaries_from_source_version(source_data_srcdist, self.suite_info)[0]:
98 verdict = PolicyVerdict.REJECTED_TEMPORARILY
99 self.logger.debug(
100 "%s hasn't been built anywhere, skipping lintian policy",
101 source_name,
102 )
103 excuse.add_verdict_info(verdict, "Lintian check deferred: missing builds")
104 lintian_info["result"] = Result.NOT_BUILT
105 else:
106 assert item.version # for type checking
107 src_pkg_id = PackageId(item.package, item.version, "source")
108 try:
109 results = self._lintian[src_pkg_id]
110 except KeyError:
111 verdict = PolicyVerdict.REJECTED_TEMPORARILY
112 self.logger.debug(
113 "%s doesn't have lintian results yet",
114 source_name,
115 )
116 excuse.add_verdict_info(
117 verdict,
118 f"Lintian check waiting for test results{url_html}",
119 )
120 lintian_info["result"] = Result.NO_DATA
122 if not verdict.is_rejected:
123 if results[0] is LintianResult.FAILED:
124 verdict = PolicyVerdict.REJECTED_PERMANENTLY
125 self.logger.debug(
126 "lintian failed on %s",
127 source_name,
128 )
129 excuse.add_verdict_info(
130 verdict,
131 f"Lintian crashed and produced no output, please contact "
132 f"{self.options.distribution}-release for a hint{url_html}",
133 )
134 lintian_info["result"] = "lintian failed"
135 elif results[0] is LintianResult.TAGS:
136 verdict = PolicyVerdict.REJECTED_PERMANENTLY
137 lintian_info["result"] = Result.TRIGGERED_TAGS
138 lintian_info["tags"] = sorted(results[1])
139 self.logger.debug(
140 "%s triggered lintian tags",
141 source_name,
142 )
143 excuse.add_verdict_info(
144 verdict,
145 f"Lintian triggered tags: {', '.join(results[1])}{url_html}",
146 )
147 elif src_archs - results[1]:
148 # britney has more architecture
149 verdict = PolicyVerdict.REJECTED_TEMPORARILY
150 archs_missing = src_archs - results[1]
151 lintian_info["result"] = Result.MISSES_ARCHS
152 lintian_info["archs"] = ", ".join(sorted(archs_missing))
153 self.logger.debug(
154 "%s lintian misses architectures",
155 source_name,
156 )
157 excuse.add_verdict_info(
158 verdict,
159 f"Lintian check waiting for test results on {', '.join(archs_missing)}{url_html}",
160 )
161 elif results[1] - src_archs: 161 ↛ 163line 161 didn't jump to line 163 because the condition on line 161 was never true
162 # lintian had more architecutes than britney, e.g. because of new/old architecture
163 lintian_info["result"] = Result.SAW_MORE_ARCHS
164 lintian_info["archs"] = ", ".join(results[1] - src_archs)
165 else:
166 pass
168 if verdict.is_rejected:
169 assert self.hints is not None
170 if (
171 hint := self.hints.search_first(
172 "ignore-lintian",
173 package=source_name,
174 version=source_data_srcdist.version,
175 )
176 ) is not None:
177 verdict = PolicyVerdict.PASS_HINTED
178 lintian_info.setdefault("ignored-lintian", {}).setdefault(
179 "issued-by", hint.user
180 )
181 excuse.addinfo(f"Lintian issues ignored as requested by {hint.user}")
183 if verdict is not PolicyVerdict.PASS:
184 lintian_info["url"] = url
186 return verdict
188 def _read_lintian_status(
189 self, filename: str
190 ) -> dict[PackageId, tuple[LintianResult, set[str]]]:
191 summary = self._lintian
192 self.logger.debug("Loading lintian status from %s", filename)
193 with open(filename) as fd: 193 ↛ exitline 193 didn't return from function '_read_lintian_status' because the return on line 195 wasn't executed
194 if os.fstat(fd.fileno()).st_size < 1: 194 ↛ 195line 194 didn't jump to line 195 because the condition on line 194 was never true
195 return summary
196 data = yaml.safe_load(fd)
198 empty_set: set[str] = set()
199 for pkgver, info in data.items():
200 pkg, ver = pkgver.split("/")
201 pkg_id = PackageId(pkg, ver, "source")
202 # TODO: might want to enumerate potential values
203 if "status" in info.keys() and info["status"] == "lintian failed":
204 summary[pkg_id] = (LintianResult.FAILED, empty_set)
205 elif info["tags"]:
206 summary[pkg_id] = (LintianResult.TAGS, set(info["tags"].keys()))
207 else:
208 archs = info["architectures"]
209 assert "source" in archs, (
210 "LintianPolicy expects at least source as architecure in lintian.yaml, "
211 f"missing for {pkgver}"
212 )
213 summary[pkg_id] = (
214 LintianResult.ARCH,
215 {arch for arch in archs.split() if arch != "source"},
216 )
218 return summary